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-sasl
↔ openldap
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, ...