This document covers setting up LDAP based authentication (using PAM), and mail routing based on LDAP directory information. I've used Debian GNU/Linux, but with minor modifications it should be applicable to every distribution out there.
Get a basic clue on how LDAP works. It's easy to find information about this on google, and there are people out there who understand this database stuff a lot better than myself.
LDAP is used by almost all commercial directory systems, like Microsoft Active Directory or Novell Directory Services. LDAP is a widespread standard, which is supported by many applications. It is possible to use an LDAP based address book in almost every MUA.
LDAP has a much cleaner design than the plain old NIS, which is based on the sun rpc standards. There is no reason to use NIS in a new installation.
Using LDAP for central authentication has several advancements. You can combine the authentication information with addresses, contacts, telephone numbers, or any other data you want. You can also save other information in your directory, like mail routing information, which can then be used by your MTA.
Its also possible to add other data, like dns records, to your ldap directory. This makes administration of a big dns database much easier than having to use sed/perl/awk in plaintext files.
After all those good things, using LDAP for central authentication has its downsides:
This allows us to get this neat feature called 'single sign on', and 'single point of administration'. This means, we don't have data cluttered all over multiple systems, we have one directory service were everything is kept. This reduces TCO^Wthe work that has to be done by the sysadmin team, and it allows a much easier maintenance of account data.
First, install the necessary software by running 'apt-get install slapd ldap-utils'. This will install all software thats necessary for your initial setup. Just ignore the questions asked by debconf, we will do all configuration using the plain configuration files.
After installation, add a system user to run the ldap daemons. This is needed, because debian usually runs them as root, which is a very bad thing[tm] to do. Clean the preinitialized db by debian, by removing everything in /var/lib/ldap.
Create appropiate SSL/TLS certificates for usage with your LDAP server, and put them to /etc/ldap/certs.
Create /etc/ldap/slapd.conf:
# cat <<'EOF' >/etc/ldap/slapd.conf
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/nis.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/misc.schema
schemacheck on
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd.args
loglevel 0
TLSCACertificateFile /etc/ldap/certs/cacert.pem
TLSCertificateFile /etc/ldap/certs/ds.projectdream.org.crt
TLSCertificateKeyFile /etc/ldap/certs/ds.projectdream.org.key
modulepath /usr/lib/ldap
moduleload back_bdb
backend bdb
database bdb
suffix "dc=ds,dc=projectdream,dc=org"
directory "/var/lib/ldap"
checkpoint 512 60
lastmod on
replogfile "/var/lib/ldap/replog"
# DB recovery (pw is foo)
#rootdn cn=admin,dc=ds,dc=suug,dc=ch
#rootpw {SSHA}tdy0M7k6OixErh5ny0ArnMoqYvtSlFvb
# WARNING! ########################################################## WARNING!
#
# Always run slapindex(8) after changing indices!
#
# WARNING! ########################################################## WARNING!
# Index #1 (match based on objectclass)
index objectClass pres,eq
# Index #2 (uid / gid lookups)
index uidNumber eq
index gidNumber eq
index uid eq
index memberUid eq
# Index #3 (mail addr lookups)
index mailLocalAddress eq
# Index #4 (name lookups)
index cn pres,eq,sub
index sn pres,eq,sub
password-hash {MD5}
# users and admin can change passwords
access to attribute=userPassword
by dn="cn=admin,dc=ds,dc=projectdream,dc=org" write
by anonymous auth
by self write
by self read
by * none
# allow users to organge their shell/name/address
access to attribute=loginShell,shadowLastChange,cn,title,givenName,sn,street,po
by dn="cn=admin,dc=ds,dc=projectdream,dc=org" write
by self write
by self read
by * read
# read only access allowed
access to dn.base="" by * read
# our admin user
access to *
by dn="cn=admin,dc=ds,dc=projectdream,dc=org" write
by * read
EOF
Add basic information to your directory, using the offline db tool slapadd. The online variant of these tools is called 'ldap*', 'slap*' are the offline tools. Remember that OpenLDAP does not lock the database. If you use the offline database tools while slapd is running, your database will be hosed.
slapadd <<'EOF' dn: dc=ds,dc=projectdream,dc=org objectClass: top objectClass: dcObject objectClass: organization o: project: d.r.e.a.m dc: ds dn: cn=admin,dc=ds,dc=projectdream,dc=org objectClass: simpleSecurityObject objectClass: organizationalRole cn: admin description: LDAP administrator userPassword: foo EOF
Make sure that all files in /var/lib/ldap belong to the user that slapd will run as. Start your LDAP daemon, either using /etc/init.d/slapd.start, or runit scripts.
Test your ldap daemon:
The '-LLL' switch turns on a brief output mode, and the -x switch tells
ldapsearch to use basic authentication (and in this case without a dn and
password, anonymous binding).
# ldapsearch -LLL -x cn=admin dn: cn=admin,dc=ds,dc=projectdream,dc=org objectClass: simpleSecurityObject objectClass: organizationalRole cn: admin description: LDAP administrator
Change the password of the admin user to something sensible: The -D switch tells ldappasswd which dn should be used to bind to the server, the -W switch prompts for the old password, and the -S switch prompts for the new password.
# ldappasswd -D cn=admin,dc=ds,dc=projectdream,dc=org -W -x -S New password: Re-enter new password: Enter LDAP Password: foo Result: Success (0)
Add your first user:
As you can see, we use the online, 'ldap*', tools because slapd is running.
# ldapadd -D cn=admin,dc=ds,dc=projectdream,dc=org -W -x <<'EOF' dn: ou=People,dc=ds,dc=projectdream,dc=org ou: People objectClass: top objectClass: organizationalUnit dn: uid=lb,ou=People,dc=ds,dc=projectdream,dc=org ou: People uid: lb c: CH objectClass: top objectClass: person objectClass: organizationalPerson objectClass: posixAccount objectClass: shadowAccount objectClass: inetLocalMailRecipient objectClass: inetOrgPerson shadowMax: 99999 shadowWarning: 7 uidNumber: 10000 gidNumber: 10000 homeDirectory: /import/home/lb mail: lukas.beeler@projectdream.org mailLocalAddress: lukas.beeler@projectdream.org mailRoutingAddress: lb@mahoro.projectdream.org shadowLastChange: 12478 loginShell: /bin/zsh cn: Lukas Beeler title: Mr. givenName: Lukas sn: Beeler street: Seitzstr. 5 postalCode: 9000 postalAddress: St. Gallen gecos: Lukas Beeler,,, EOF
Install the package by typing 'apt-get install libnss-ldap'. Answer the questions asked by debconf. You don't need a proxy user, and you want LDAPv3. My debian package had a bug, it was impossible to specify that i don't want a proxy user. So i had to edit /etc/libnss-ldap.conf on my own.
Modify your /etc/nssswitch.conf to read like this:
passwd: compat ldap group: compat ldap shadow: compat ldap
Check if it works:
# id lb uid=10000(lb) gid=10000 groups=10000
First, give your user a password:
# ldappasswd -D cn=admin,dc=ds,dc=projectdream,dc=org -x -W -S uid=lb,ou=People,dc=ds,dc=projectdream,dc=org New password: Re-enter new password: Enter LDAP Password: Result: Success (0)
Install the package by typin 'apt-get install libpam-ldap'. You do want to make the local root db admin, but you don't need a proxy user. Ęgain, editing /etc/pam_ldap.conf is needed because the package has a bug.
Next step is to modify files in /etc/pam.d/:
# grep -v ^# common-auth auth sufficient pam_ldap.so auth required pam_unix.so # grep -v ^# common-account account sufficient pam_ldap.so account required pam_unix.so # grep -v ^# common-password password sufficient pam_ldap.so password required pam_unix.so md5
Check if it works:
# svc -t /service/openssh # reinit pam # ssh lb@localhost lb@localhost's password: Could not chdir to home directory /import/home/lb: No such file or directory mahoro% id uid=10000(lb) gid=10000 groups=10000
Thats it! Your system is now able to authenticate users against an LDAP database. You may want to write yourself a script to add/remove users from the Database. I wrote ldapadm for this purpose. It contains some site local stuff, so please edit it to fit your needs.
Replication with OpenLDAP is fairly easy. Add the following entry to slapd.conf on the server that will be master:
replica uri=ldap://naru.projectdream.org binddn="cn=Replicator,dc=ds,dc=projectdream,dc=org" bindmethod=simple credentials=foobar
Setup the slave LDAP server by reusing the configuration file from master, but adding the following lines: (note: you don't need to add the Replicator object to your ldap database)
rootdn cn=Replicator,dc=ds,dc=projectdrema,dc=org
# create password using slappasswd(1)
rootpw {SSHA}SaKMI6SDIBxbG/GP6epR/aM/VRC+vhKI
updatedn cn=Replicator,dc=ds,dc=projectdrema,dc=org
updateref ldap://mahoro.projectdream.org
The updateref will be used to redirect clients that want to write to the database to the master server (which is the only one that can write entries).
Next step is to copy your database from the master to the slave. Shutdown the master to ensure a consistent database and then dump it to ldif using slapcat
# umask 077 # slapcat >db # scp db naru.projectdream.org:~
Initialize the db on the slave:
# slapadd <~db
Fire up the slave ldapd, and perform querys against it.
When you're done, start slurpd on the master using /etc/init.d/slapd, or runit runfiles.
Test your replication setup thoroughly, by doing modifications on the master, and looking if the replication works. Enable slurpd debugging if you run into problems.
Install and configure postfix as usual, then, add the following entries to main.cf:
local_recipient_maps = ldap:ldap $alias_maps
virtual_maps = ldap:ldap
ldap_server_host = 10.10.2.103
ldap_search_base = ou=People,dc=ds,dc=projectdream,dc=org
ldap_domain = projectdream.org
ldap_query_filter =
(&(objectclass=inetLocalMailRecipient)(mailLocalAddress=%s))
ldap_result_attribute = mailRoutingAddress,mailForwardAddress
ldap_bind = no
ldap_version = 3
There are two options that need explenation:
ldap_query_filter tells postfix on how to look up mail addresses. %s will be
replaced with the email address that was in the rcpt to:. Postfix will
then route the message to the addresses specified in the ldap_result_attribute
line. It will look for mailRoutingAddress and mailForwardAdress, and send the
mail to those addresses. Thus, when sending
mail to "lukas.beeler@projectdream.org", the query sent to the ldap server will
be the same as this:
# ldapsearch -LLL -x '(&(objectclass=inetLocalMailRecipient)(mailLocalAddress=lukas.beeler@projectdream.org))' mailRoutingAddress mailForwardAddress dn: uid=lb,ou=People,dc=ds,dc=projectdream,dc=org mailRoutingAddress: lb@mahoro.projectdream.org
The headers of such an email will look like this:
Return-Path: <lb@mahoro.projectdream.org>
X-Original-To: lukas.beeler@projectdream.org
Delivered-To: lb@mahoro.projectdream.org
Received: by mahoro.projectdream.org (Postfix, from userid 10000)
id F39571C0BB; Wed, 3 Mar 2004 11:22:15 +0100 (CET)
Users are managed using a tool called ldapadm. Under normal circumstances, it is not needed to ``manually' interact with the entries in the ldap directory. But of course, it may be necessary.
LDAP is an object oriented database. Each object has a dn (distinguished name) which is unique to the object. Each object is member of several object classes, which define which attributes the object is allowed to have.
A dn consists of the name of the object itself (for example, uid=lb), none to several organizational units (for example, ou=People), and a base dn (dc=ds,dc=suug,dc=ch). All those things together are the dn of a specific object, in our case uid=lb,ou=People,dc=ds,dc=suug,dc=ch.
Objectclasses are used to define for what an object is used. Lets start with a very basic entry:
dn: uid=lb,ou=People,dc=ds,dc=suug,dc=ch objectClass: posixAccount cn: Lukas Beeler uid: lb uidNumber: 10000 gidNumber: 10000 homeDirectory: /import/home/lb
Our object (uid=lb,ou=People,dc=ds,dc=suug,dc=ch) has the objectClass posixAccount assigned. This allows it to use several other attributes, like uidNumber, and homeDirectory. objectClasses are defined in Schemas. These schemas can be found in /etc/ldap/schema/. For your reference, here is a short excerpt, which defines the objectClass posixAccount:
objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY
DESC 'Abstraction of an account with POSIX attributes'
MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
MAY ( userPassword $ loginShell $ gecos $ description ) )
An objectClass can define required and optional attributes (see MUST and MAY).
You can run searches using the ldapsearch tool. In order to be able to see all attributes of a user, you must not do a search with an anonymous binding. Administrator privileges are needed in order to see all attributes of an user.
ldapsearch -x -P 3 -D cn=admin,dc=ds,dc=suug,dc=ch -W uid=lb
A simple query for all attributes which have an attribute called uid with the value lb.
| ldapadm arguments | Result |
| (&(uid=lb)(objectClass=suugMember)) | Object with a uid attribute which as the value lb, and an objectClass value of suugMember (it may have other objectClass-es) |
| (|(uid=lb)(uid=tgr)) | uid=lb OR uid=tgr |
| -b ou=Hosts,dc=ds,dc=suug,dc=ch aRecord=195.134.158.23 | Limits searches to a dn of ou=Hosts,dc=ds,dc=suug,dc=ch |
| (|(uid=lb)(uid=tgr)) mailLocalAddress | queries only for the mailLocalAddress of uid=lb or uid=tgr. |
We needed some special attributes to include our member database in the ldap directory. Thus, we created a new objectClass named 'suugMember'. All objects with this objectClass assigned are Members of the Swiss Unix User Group.
Here we have an excerpt from the suug.schema file, describing the suugMember objectClass:
objectclass ( 1.1.1.1
NAME 'suugMember'
DESC 'Member of the Swiss Unix User Group'
SUP top AUXILIARY
MAY ( mailPrivateAddress $ paidUntil $ membershipClass ) )
As you can see, there are three new attributes available if your object has the suugMember object class assigned:
| mailPrivateAddress | Private Mail Address auf the SUUG Member. Should only be visible to the administrator and the member itself. |
| paidUntil | A field containing the date until the member has paid his fee. This is also sensible information, it must not be visible to others. |
| membershipClass | Defines the membershipClass of a Member. This is a freeform string, which should adhere to our naming conventions. |
A complete Member entry is a bit more complex, lets look at a full blown example:
dn: uid=jdoe,ou=People,dc=ds,dc=suug,dc=ch uid: jdoe cn: John Doe title: Mr. givenName: Johnn sn: Doe street: Nostreet. 5 postalCode: 666 postalAddress: Noplace c: CH objectClass: top objectClass: person objectClass: organizationalPerson objectClass: posixAccount objectClass: shadowAccount objectClass: inetLocalMailRecipient objectClass: inetOrgPerson objectClass: suugMember paidUntil: 1970-01 membershipClass: Student shadowMax: 99999 shadowWarning: 7 uidNumber: 10023 gidNumber: 10023 gecos: John Doe,,, loginShell: /bin/bash homeDirectory: /import/home/jdoe mail: j.doe@suug.ch mailLocalAddress: jdoe@suug.ch mailLocalAddress: john.doe@suug.ch mailLocalAddress: doe.john@suug.ch mailLocalAddress: j.doe@suug.ch mailRoutingAddress: jdoe@postel.suug.ch mailPrivateAddress: jdoe@aol.com userPassword:: removed. shadowLastChange: 12532
Most of those field should be self explenatory, some of them are not. Those are explained below:
| Used by MUAs to determine where to send mail to. Not used by any MTA | |
| mailLocalAddress | Used by our MTA to find recipients. mailRoutingAddress is used as the result of a recipient lookup |
| mailRoutingAddress | Used by our MTA to find the final delivery point. It is used in a recipient lookup to find the final delivery point. |
# users and admin can change passwords
access to attribute=userPassword
by dn="cn=admin,dc=ds,dc=suug,dc=ch" write
by anonymous auth
by self write
by self read
by * none
# allow users to change their shell/name/address
# restrict access to personal data
access to attribute=loginShell,shadowLastChange,cn,title,givenName,sn,gecos
by dn="cn=admin,dc=ds,dc=suug,dc=ch" write
by self write
by * read
access to attribute=street,postalCode,postalAddress,c,mailPrivateAddress
by dn="cn=admin,dc=ds,dc=suug,dc=ch" write
by dn="cn=admin,dc=ds,dc=suug,dc=ch" read
by self write
by self read
by * none
# only admin can modify, only user can see
access to attribute=paidUntil
by dn="cn=admin,dc=ds,dc=suug,dc=ch" write
by dn="cn=admin,dc=ds,dc=suug,dc=ch" read
by self read
by * none
# read only access allowed
access to dn.base="" by * read
access to *
by dn="cn=admin,dc=ds,dc=suug,dc=ch" write
by * read
This is the ACL configuration section of our slapd. In english, it boils down to the following rights:
Per default, admin is allowed to write every attribute. User is allowed to read every attribute. userPassword: can be used for authentication. Users are able to change several attributes of their entry. Some of those entries are private to the user, and only visible to them and the administrator. The paiduntil attribute is private, but the user cannot modify it.
% ldapsearch ldap_sasl_interactive_bind_s: No such attribute (16)You forgot the -x switch. LDAP defaults to SASL authentication.
% slapcat zsh: segmentation fault slapcatslapcat must have read and write permission on /var/lib/ldap. This is an unhandled error in slapcat. The open fails, but the fd is accessed anyway.
% strace -eopen slapcat 2>&1 | tail -5
open("/var/lib/ldap/DB_CONFIG", O_RDONLY|O_LARGEFILE) = -1 ENOENT (No such file or directory)
open("/var/lib/ldap/__db.001", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0600) = -1 EEXIST (File exists)
open("/var/lib/ldap/__db.001", O_RDWR|O_LARGEFILE) = -1 EACCES (Permission denied)
--- SIGSEGV (Segmentation fault) @ 0 (0) ---
+++ killed by SIGSEGV +++
% ldapsearch -ZZ
ldap_start_tls: Connect error (91)
additional info: Error in the certificate.
The TLS libraries were unable to check the certificate or
the hostname you are connecting to does not equal the cn in the SSL cert.
Add a TLS_CACERT option to /etc/ldap/ldap.conf, and point it to the
CA certificate you are using.
Correct the URI option in /etc/ldap/ldap.conf to point to the cn of the
certificate you are using.
slapadd: could not add entry dn="dc=ds,dc=projectdream,dc=org" (line=7): txn_begin failed: Invalid argument (22)The directory /var/lib/ldap does not exist.