project: d.r.e.a.m

Setting up LDAP based authentication and mail routing on linux

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.

Why LDAP?

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:

Why LDAP for the SUUG?

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.

Setting up your directory

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

Setting up NSS

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

Setting up PAM

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.

Setting up replication

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.

Setting up LDAP mail routing with postfix

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)

The SUUG LDAP Database Layout

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.

Short LDAP Overview

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).

Searching with LDAP

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 argumentsResult
(&(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.23Limits searches to a dn of ou=Hosts,dc=ds,dc=suug,dc=ch
(|(uid=lb)(uid=tgr)) mailLocalAddressqueries only for the mailLocalAddress of uid=lb or uid=tgr.

Database Schema

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:

mail 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.

Permissions

# 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.

Troubleshooting