I'm using LDAP (RFC 4510) to maintain a centralized address book at home. Here are my setup notes, mostly following Gentoo's LDAP howto.

Install OpenLDAP with the ldap USE flag enabled:

# emerge -av openldap

If you get complaints about a cyrus-saslopenldap dependency cycle, you should temporarily (or permanently) disable the ldap USE flag for cyrus-sasl:

# echo 'dev-libs/cyrus-sasl -ldap' > /etc/portage/package.use/ldap
# -ldap" emerge -av1 cyrus-sasl
# emerge -av openldap

Generate an administrative password:

$ slappasswd 
New password: 
Re-enter new password: 
{SSHA}EzP6I82DZRnW+ou6lyiXHGxSpSOw2XO4

Configure the slapd LDAP server. Here is a very minimal configuration, read the OpenLDAP Admin Guide for details:

# emacs /etc/openldap/slapd.conf
# cat /etc/openldap/slapd.conf
include         /etc/openldap/schema/core.schema
include         /etc/openldap/schema/cosine.schema
include         /etc/openldap/schema/inetorgperson.schema
pidfile         /var/run/openldap/slapd.pid
argsfile        /var/run/openldap/slapd.args
database        hdb
suffix          "dc=example,dc=com"
checkpoint      32      30
rootdn          "cn=Manager,dc=example,dc=com"
rootpw          {SSHA}EzP6I82DZRnW+ou6lyiXHGxSpSOw2XO4
directory       /var/lib/openldap-data
index   objectClass     eq

inetOrgPerson is huge, but it's standardized. I think it's better to pick a big standard right off, than to outgrow something smaller and need to migrate.

Gentoo creates the default database directory for you, so you can ignore warnings about needing to create it yourself.

Configure LDAP client access. Again, read the docs for details on adapting this to your particular situation:

# emacs /etc/openldap/ldap.conf
$ cat /etc/openldap/ldap.conf
BASE    dc=example,dc=com
URI     ldap://ldapserver.example.com

You can edit '/etc/conf.d/slapd' if you want command line options passed to slapd when the service starts, but the defaults looked fine to me.

Start slapd:

# /etc/init.d/slapd start

Add it to your default runlevel:

# eselect rc add /etc/init.d/slapd default

Test the server with

$ ldapsearch -x -b '' -s base '(objectclass=*)'

Build a hierarchy in your database (this will depend on your organizational structure):

$ emacs /tmp/people.ldif
$ cat /tmp/people.ldif
version: 1

dn: dc=example, dc=com
objectClass: dcObject
objectClass: organization
o: Example, Inc.
dc: example

dn: ou=people, dc=example,dc=com
objectClass: organizationalUnit
ou: people
description: All people in organisation

dn: cn=Manager, dc=example,dc=com
objectClass: organizationalRole
cn: Manager
description: Directory Manager
$ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/people.ldif
$ rm /tmp/people.ldif

abook

If you currently keep your addresses in abook, you can export them to LDIF with:

$ abook --convert --infile ~/.abook/addressbook --outformat ldif \
  | abook-ldif-cleanup.py --basedn 'ou=people,dc=example,dc=com' > dump.ldif

where abook-ldif-cleanup.py does some compatibility processing using the python-ldap module.

Add the people to your LDAP database:

$ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f dump.ldif

To check if that worked, you can list all the entries in your database:

$ ldapsearch -x -b 'dc=example,dc=com' '(objectclass=*)'

Then remove the temporary files:

$ rm -rf dump.ldif

Aliases

Ok, we've put lots of people into the people OU, but what if we want to assign them to another department? We can use aliases (RFC 4512), the symlinks of the LDAP world. To see how this works, lets create a test OU to play with:

$ emacs /tmp/test.ldif
$ cat /tmp/test.ldif
version: 1
dn: ou=test, dc=example,dc=com
objectClass: organizationalUnit
ou: testing
$ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/test.ldif
$ rm /tmp/test.ldif

Now assign one of your people to that group:

$ emacs /tmp/alias.ldif
$ cat /tmp/alias.ldif
version: 1
dn: cn=Jane Doe, ou=test,dc=example,dc=com
objectClass: alias
aliasedObjectName: cn=Jane Doe, ou=people,dc=example,dc=com
$ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/alias.ldif
$ rm /tmp/alias.ldif

The extensibleObject class allows us to add the DN field, without it you get:

$ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/alias.ldif
Enter LDAP Password: 
adding new entry "cn=Jane Doe, ou=test,dc=example,dc=com"
ldap_add: Object class violation (65)
        additional info: attribute 'cn' not allowed

You can search for all entries (including aliases) with

$ ldapsearch -x -b 'ou=test, dc=example,dc=com' '(objectclass=*)'
…
dn: cn=Jane Doe,ou=test,dc=example,dc=com
objectClass: alias
objectClass: extensibleObject
aliasedObjectName:: Y249TWljaGVsIFZhbGxpw6hyZXMsb3U9cGVvcGxlLGRjPXRyZW1pbHksZGM9dXM=
…

You can control dereferencing with the -a option:

$ ldapsearch -x -a always -b 'ou=test, dc=example,dc=com' '(objectclass=*)'
…
dn: cn=Jane Doe,ou=people,dc=example,dc=com
cn: Jane Doe
sn: Doe
…

Once you've played around, you can remove the test OU and its descendants:

$ ldapdelete -D "cn=Manager,dc=example,dc=com" -xW -r ou=test,dc=example,dc=com

shelldap

There are a number of tools to make it easier to manage LDAP databases. Command line junkies will probably like shelldap:

$ shelldap --server ldapserver.example.com
~ > ls
cn=Manager
ou=people
~ > cat cn=Manager 

dn: cn=Manager,dc=example,dc=com
objectClass: organizationalRole
cn: Manager

~ > cd ou=people 
ou=people,~ > ls

Shelldap's edit command spawns your EDITOR on a temporary file populated by the entry you're editing. You can either alter the entry as you see fit, or try something fancier in LDIF.

JPEG photos and binary data

inetOrgPerson has a jpegPhoto attribute, which holds a base64 encoded JPEG. The easiest way to set this attribute is to use the :< delimiter mentioned in ldif(5) and RFC 2849:

$ cat thumb.ldif
version: 1
dn: cn=Jane Doe,ou=people,dc=example,dc=com
changetype: modify
add: jpegPhoto
jpegPhoto:< file:///tmp/jdoe.jpeg
-
$ ldapmodify -f thumb.ldif

You can extract the thumbnail from the database using:

$ ldapsearch -tT /tmp "cn=Jane Doe"
…
jpegPhoto:< file:///tmp/ldapsearch-jpegPhoto-Vvg2Ot
…

Which dumps non-printable values (like our jpegPhoto) to temporary files.

If you just want to look up someone's picture, take a look at my ldap-jpeg.py script. It searches for a query string in any of cn, uid, or mail, and for matching entries with a jpegPhoto attribute, it uses your mailcap-specified viewer to display the photo.

Mutt

If you use the Mutt email client (or just want a simple way to query email addresses from the command line) there are a number of scripts available. Pick whichever sounds most appealing to you. I wrote up mutt-ldap.py, which has since seen contributions from others and been pulled out into its own repository.

Apple Address Book

You can configure Apple's Address Book to search an LDAP directory. See Humanizing OS X for details.

SSL/TLS

It took me a bit of work to get SSL/TLS working with my GnuTLS-linked OpenLDAP. First, you'll probably need to generate new SSL/TLS keys (/etc/openldap/ssl/*) with certtool (see X.509 certificates). Then add the following lines to /etc/openldap/slapd.conf:

TLSCipherSuite NORMAL
TLSCACertificateFile /etc/openldap/ssl/ca.crt
TLSCertificateFile /etc/openldap/ssl/ldap.crt
TLSCertificateKeyFile /etc/openldap/ssl/ldap.key
TLSVerifyClient never

Where ca.crt, ldap.crt, and ldap.key are your new CA, certificate, and private key. If you want to disable unencrypted connections completely, remove the ldap:// entry from your slapd command line by editing (on Gentoo) /etc/conf.d/slapd so it has

OPTS="-h 'ldaps:// ldapi://%2fvar%2frun%2fopenldap%2fslapd.sock'"

Now you should be able to restart slapd so it will use the new configuration.

Have clients running on your server use the local socket by editing /etc/openldap/ldap.conf to set:

URI     ldapi://%2fvar%2frun%2fopenldap%2fslapd.sock

Test your server setup by running (on the server)

$ ldapsearch -x -b '' -s base '(objectclass=*)'

Copy your CA over to any client machines (I put it in /etc/openldap/ssl/ldapserver.crt), and set them up with the following two lines in /etc/openldap/ldap.conf:

URI         ldaps://ldapserver.example.com
TLS_CACERT  /etc/openldap/ssl/ldapserver.crt

Test your client setup by running (on the client)

$ ldapsearch -x -b '' -s base '(objectclass=*)'

You can configure shelldap with the following lines in ~/.shelldap.rc:

server: ldaps://ldapserver.example.com
tls: yes
tls_cacert: /etc/openldap/ssl/ldapserver.crt

You can configure mutt-ldap.py with the following lines in ~/.mutt-ldap.rc:

port = 636
ssl = yes

Access control and authentication

There are a number of possible approaches to authentication for LDAP, so read the admin manual for details. I've got Kerberos setup on my home system, and I'll walk through this setup here.

Server side

I expose the LDAPS port to the external world through my router, and I don't want anonymous users to be able to download all my contact information. The solution to this is to implement access control. For my situation, the following /etc/openldap/slapd.conf directives seemed appropriate:

access to attrs=uid
    by anonymous auth
    by * read

access to *
    by self write
    by anonymous auth
    by * read

The first directive allows anonymous users to use the uid attribute when authenticating, and allows authenticated users to read anyone's uid attribute. This keeps users from being able to change their own uid.

The second directive allows authenticated users to update their own entry and to read every entry. Anonymous are allowed to authenticate themselves, but have no other privileges.

Alright, so how should user's go about authenticating? We'll want to set slapd up as a Kerberos service, and have clients authenticate using GSSAPI.

For the LDAP service, we'll need a ldap/<fqdn>@REALM principal. Because we want that service to start automatically at boot, we need to keep its key in a keytab file.

# kadmin.local -p jdoe/admin
Authenticating as principal jdoe/admin with password.
Password for jdoe/admin@R.EDU: 
kadmin.local:  add_principal -randkey ldap/ldapserver.example.com
WARNING: no policy specified for ldap/ldapserver.example.com@R.EDU; defaulting to no policy
Principal "ldap/ldapserver.example.com@R.EDU" created.
kadmin.local:  ktadd -k /etc/openldap/krb5-ldap.keytab ldap/ldapserver.example.com
Entry for principal kdap/ldapserver.example.com...
…
kadmin.local:  quit
# chown ldap:ldap /etc/openldap/krb5-ldap.keytab

You need use kadmin.local here (instead of kadmin) so the process has premission to create and edit the keytab file.

You'll need to point your slapd server to the new keytab. On Gentoo, you do this by uncommenting

KRB5_KTNAME=/etc/openldap/krb5-ldap.keytab

in /etc/conf.d/slapd. On Red Hat, you add

export KRB5_KTNAME=/etc/openldap/ldap.keytab

to /etc/sysconfig/ldap.

You should also configure your realm and hostname in /etc/openldap/slapd.conf:

sasl-realm      R.EDU
sasl-host       ldapserver.example.com

You'll also want to associate user's Kerberos principles to LDAP DNs. The template slapd uses is:

uid=<primary[/instance]>,cn=<realm>,cn=gssapi,cn=auth

so jdoe@R.EDU is associated with

uid=jdoe,cn=r.edu,cn=gssapi,cn=auth

and jdoe/admin@R.EDU is associated with

uid=jdoe/admin,cn=r.edu,cn=gssapi,cn=auth

You'll probably want to map these authentication DNs to the appropriate directory entry, for example:

cn=Jane Doe,ou=people,dc=r,dc=edu

There are a number of ways to this, but I chose

authz-regexp
    uid=([^,]*),cn=r.edu,cn=gssapi,cn=auth
    ldap:///ou=people,dc=r,dc=edu??one?(uid=$1)

From the manual:

This will initiate an internal search of the LDAP database inside the slapd server. If the search returns exactly one entry, it is accepted as being the DN of the user. If there are more than one entries returned, or if there are zero entries returned, the authentication fails and the user's connection is left bound as the authentication request DN.

Indexing sounds like a good idea, so we turn it on with

index   objectClass     eq
index   uid             eq
index   cn,mail         sub

If you change your index configuration, you'll have to stop slapd and run slapindex to regenerate the indexes.

Client side

Users will have to do the usual kinit to get their Ticket Granting Ticket (TGT), and then instruct their client software to use GSSAPI (-Y GSSAPI with the OpenLDAP client tools). If you don't want to type -Y GSSAPI, you can add

SASL_MECH GSSAPI

to your ~/.ldaprc. If you're on Gentoo, you'll want the kerberos and sasl USE flags set when you emerge openldap.

Reverse DNS issues

Because my SLAPD server runs on a dynamic IP address, I ran into trouble with reverse DNS. The client would resolve the server address into an IP, then resolve that IP address to its canonical name, and asks the ticket granting server (TGS) for authorization to use ldap/<canonical>@REALM. Because the dynamic canonical name doesn't match the hostname, the TGS denies the request, leading to output like:

$ ldapwhoami -Y GSSAPI
ldap_sasl_interactive_bind_s: Local error (-2)
    additional info: SASL(-1): generic failure: GSSAPI Error: Unspecified GSS failure.  Minor code may provide more information (Server krbtgt/EDU@R.EDU not found in Kerberos database)

And messages like:

… krb5kdc[15239](info): TGS_REQ (4 etypes {18 17 16 23}) …: UNKNOWN_SERVER: authtime 0,  jdoe@R.EDU for host/some.dynamic.canonical.host.net@R.EDU, Server not found in Kerberos database

in the server's KDC log.

I tried disabling the reverse DNS lookup with both the -N command line option to ldapwhoami and the SASL_NOCANON true option in ~/.ldaprc. I also added:

[libdefaults] rdns = false

to my client's /etc/krb5.conf. Even with all of these, I was still getting reverse DNS attempts, so I gave up and just added an entry to /etc/hosts to ensure I got the right hostname when the client tried to resolve it.

You can get more detailed messages from ldapwhoami by increasing the debuglevel (for example, with the -d 1 option), which helps when you're troubleshooting these kinds of issues. For example:

$ ldapwhoami -d 1 -Y GSSAPI
…
ldap_int_sasl_open: host=some.dynamic.canonical.host.net
…
$ ldapwhoami -d 1 -Y GSSAPI -N
…
ldap_int_sasl_open: host=ldapserver.example.com
…

Currently, ldapwhoami and friends will ignore the SASL_NOCANON configuration option and only respect the -N command line option. I've submitted an OpenLDAP bug fixing this (included in version 2.4.32, 2012-07-31), but there is still a reverse DNS call happening at some point.

Debian-based systems

I wanted to mirror my home LDAP info on my public Ubuntu server. Here's a quick rundown of the Ubuntu setup. Install OpenLDAP:

$ sudo apt-get install slapd ldap-utils

Don't serve in the clear:

$ cat /etc/default/slapd
…
SLAPD_SERVICES="ldaps:/// ldapi:///"
…

Avoid Unrecognized database type (hdb) by loading the hdb backend module before declaring hdb databases:

$ sudo cat /etc/ldap/slapd.conf
…
moduleload back_hdb
database hdb
…

Convert the old school slapd.conf to the new slapd.d:

$ sudo mv slapd.d{,.bak}
$ sudo mkdir slapd.d
$ sudo slaptest -f slapd.conf -F slapd.d
…
hdb_db_open: database "dc=example,dc=com": db_open(/var/lib/slapd/id2entry.bdb) failed: No such file or directory (2).
…
slap_startup failed (test would succeed using the -u switch)
…
$ sudo chown -R openldap.openldap slapd.d

Don't worry about that db_open error, the conversion to slapd.d will have completed successfully.

Set permissions on the database directory (note that the databases should be under /var/lib/ldap to match Ubuntu's default apparmor config. Otherwise you'll see invalid path: Permission denied errors when slapd tries to initialize the databaes).

$ sudo chown openldap.openldap /var/lib/ldap/
$ sudo chmod 750 /var/lib/ldap/

Configure your clients

$ cat /etc/ldap/ldap.conf
BASE    dc=example,dc=com
URI     ldaps://example.com
TLS_CACERT  /etc/ldap/ssl/ldapserver.crt

Start slapd and add it to your default runlevel:

$ sudo /etc/init.d/slapd start
$ sudo update-rc.d slapd defaults

Finally, import your directory data. Dump the data on your master server:

master$ sudo slapcat -b 'dc=example,dc=com' > database.ldif

Load the data on your slave:

$ sudo /etc/init.d/slapd stop
$ sudo slapadd -l database.ldif
$ sudo /etc/init.d/slapd start

References

There's a good overview of schema and objectclasses by Brian Jones on O'Reilly. If you want to use inetOrgPerson but also include the countryName attribute, ...