Discuss jpegPhoto, access controls, Kerberos, and StartTLS in the LDAP post.
[blog.git] / posts / LDAP.mdwn
1 I'm using [LDAP][] ([RFC 4510][rfc4510]) to maintain a centralized
2 address book at home.  Here are my setup notes, mostly following
3 Gentoo's [LDAP howto][howto].
4
5 Install [OpenLDAP][] with the `ldap` USE flag enabled:
6
7     # emerge -av openldap
8
9 If you get complaints about a `cyrus-sasl` ↔ `openldap` dependency
10 cycle, you should temporarily (or permanently) disable the `ldap` USE
11 flag for `cyrus-sasl`:
12
13     # echo 'dev-libs/cyrus-sasl -ldap' > /etc/portage/package.use/ldap
14     # -ldap" emerge -av1 cyrus-sasl
15     # emerge -av openldap
16
17 Generate an administrative password:
18
19     $ slappasswd 
20     New password: 
21     Re-enter new password: 
22     {SSHA}EzP6I82DZRnW+ou6lyiXHGxSpSOw2XO4
23
24 Configure the `slapd` LDAP server.  Here is a very minimal
25 configuration, read the [OpenLDAP Admin Guide][admin] for details:
26
27     # emacs /etc/openldap/slapd.conf
28     # cat /etc/openldap/slapd.conf
29     include         /etc/openldap/schema/core.schema
30     include         /etc/openldap/schema/cosine.schema
31     include         /etc/openldap/schema/inetorgperson.schema
32     pidfile         /var/run/openldap/slapd.pid
33     argsfile        /var/run/openldap/slapd.args
34     database        hdb
35     suffix          "dc=example,dc=com"
36     checkpoint      32      30
37     rootdn          "cn=Manager,dc=example,dc=com"
38     rootpw          {SSHA}EzP6I82DZRnW+ou6lyiXHGxSpSOw2XO4
39     directory       /var/lib/openldap-data
40     index   objectClass     eq
41
42 [inetOrgPerson][] is huge, but it's standardized.  I think it's better
43 to pick a big standard right off, than to outgrow something smaller
44 and need to migrate.
45
46 Gentoo creates the default database directory for you, so you can
47 ignore warnings about needing to create it yourself.
48
49 Configure LDAP client access.  Again, read the docs for details on
50 adapting this to your particular situation:
51
52     # emacs /etc/openldap/ldap.conf
53     $ cat /etc/openldap/ldap.conf
54     BASE    dc=example,dc=com
55     URI     ldap://ldapserver.example.com
56
57 You can edit '/etc/conf.d/slapd' if you want command line options
58 passed to `slapd` when the service starts, but the defaults looked
59 fine to me.
60
61 Start `slapd`:
62
63     # /etc/init.d/slapd start
64
65 Add it to your default runlevel:
66
67     # eselect rc add /etc/init.d/slapd default
68
69 Test the server with
70
71     $ ldapsearch -x -b '' -s base '(objectclass=*)'
72
73 Build a hierarchy in your database (this will depend on your
74 organizational structure):
75
76     $ emacs /tmp/people.ldif
77     $ cat /tmp/people.ldif
78     version: 1
79
80     dn: dc=example, dc=com
81     objectClass: dcObject
82     objectClass: organization
83     o: Example, Inc.
84     dc: example
85
86     dn: ou=people, dc=example,dc=com
87     objectClass: organizationalUnit
88     ou: people
89     description: All people in organisation
90
91     dn: cn=Manager, dc=example,dc=com
92     objectClass: organizationalRole
93     cn: Manager
94     description: Directory Manager
95     $ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/people.ldif
96     $ rm /tmp/people.ldif
97
98 abook
99 -----
100
101 If you currently keep your addresses in [abook][], you can export them
102 to [LDIF][] with:
103
104     $ abook --convert --infile ~/.abook/addressbook --outformat ldif \
105       | abook-ldif-cleanup.py --basedn 'ou=people,dc=example,dc=com' > dump.ldif
106
107 where [[abook-ldif-cleanup.py]] does some compatibility processing
108 using the [python-ldap][] module.
109
110 Add the people to your LDAP database:
111
112     $ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f dump.ldif
113
114 To check if that worked, you can list all the entries in your
115 database:
116
117     $ ldapsearch -x -b 'dc=example,dc=com' '(objectclass=*)'
118
119 Then remove the temporary files:
120
121     $ rm -rf dump.ldif
122
123 Aliases
124 -------
125
126 Ok, we've put lots of people into the `people` OU, but what if we want
127 to assign them to another department?  We can use aliases ([RFC
128 4512][rfc4512]), the symlinks of the LDAP world.  To see how this
129 works, lets create a test OU to play with:
130
131     $ emacs /tmp/test.ldif
132     $ cat /tmp/test.ldif
133     version: 1
134     dn: ou=test, dc=example,dc=com
135     objectClass: organizationalUnit
136     ou: testing
137     $ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/test.ldif
138     $ rm /tmp/test.ldif
139
140 Now assign one of your people to that group:
141
142     $ emacs /tmp/alias.ldif
143     $ cat /tmp/alias.ldif
144     version: 1
145     dn: cn=Jane Doe, ou=test,dc=example,dc=com
146     objectClass: alias
147     aliasedObjectName: cn=Jane Doe, ou=people,dc=example,dc=com
148     $ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/alias.ldif
149     $ rm /tmp/alias.ldif
150
151 The `extensibleObject` class allows us to add the DN field, without it
152 you get:
153
154     $ ldapadd -D "cn=Manager,dc=example,dc=com" -xW -f /tmp/alias.ldif
155     Enter LDAP Password: 
156     adding new entry "cn=Jane Doe, ou=test,dc=example,dc=com"
157     ldap_add: Object class violation (65)
158             additional info: attribute 'cn' not allowed
159
160 You can search for all entries (including aliases) with
161
162     $ ldapsearch -x -b 'ou=test, dc=example,dc=com' '(objectclass=*)'
163     …
164     dn: cn=Jane Doe,ou=test,dc=example,dc=com
165     objectClass: alias
166     objectClass: extensibleObject
167     aliasedObjectName:: Y249TWljaGVsIFZhbGxpw6hyZXMsb3U9cGVvcGxlLGRjPXRyZW1pbHksZGM9dXM=
168     …
169
170 You can control dereferencing with the `-a` option:
171
172     $ ldapsearch -x -a always -b 'ou=test, dc=example,dc=com' '(objectclass=*)'
173     …
174     dn: cn=Jane Doe,ou=people,dc=example,dc=com
175     cn: Jane Doe
176     sn: Doe
177     …
178
179 Once you've played around, you can remove the `test` OU and its
180 descendants:
181
182     $ ldapdelete -D "cn=Manager,dc=example,dc=com" -xW -r ou=test,dc=example,dc=com
183
184 shelldap
185 --------
186
187 There are a number of tools to make it easier to manage LDAP
188 databases.  Command line junkies will probably like [shelldap][]:
189
190     $ shelldap --server ldapserver.example.com
191     ~ > ls
192     cn=Manager
193     ou=people
194     ~ > cat cn=Manager 
195     
196     dn: cn=Manager,dc=example,dc=com
197     objectClass: organizationalRole
198     cn: Manager
199     
200     ~ > cd ou=people 
201     ou=people,~ > ls
202
203 Shelldap's `edit` command spawns your `EDITOR` on a temporary file
204 populated by the entry you're editing.  You can either alter the entry
205 as you see fit, or try something fancier in [LDIF][].
206
207 JPEG photos and binary data
208 ---------------------------
209
210 [inetOrgPerson][] has a [jpegPhoto][] attribute, which holds a base64
211 encoded JPEG.  The easiest way to set this attribute is to use the
212 `:<` delimiter mentioned in `ldif(5)` and [RFC 2849][rfc2849]:
213
214     $ cat thumb.ldif
215     version: 1
216     dn: cn=Jane Doe,ou=people,dc=example,dc=com
217     changetype: modify
218     add: jpegPhoto
219     jpegPhoto:< file:///tmp/jdoe.jpeg
220     -
221     $ ldapmodify -f thumb.ldif
222
223 You can extract the thumbnail from the database using:
224
225     $ ldapsearch -tT /tmp "cn=Jane Doe"
226     …
227     jpegPhoto:< file:///tmp/ldapsearch-jpegPhoto-Vvg2Ot
228     …
229
230 Which dumps non-printable values (like our `jpegPhoto`) to temporary
231 files.
232
233 If you just want to look up someone's picture, take a look at my
234 [[ldap-jpeg.py]] script.  It searches for a query string in any of
235 [cn][], [uid][], or [mail][], and for matching entries with a
236 `jpegPhoto` attribute, it uses your [[mailcap]]-specified viewer to
237 display the photo.
238
239 Mutt
240 ----
241
242 If you use the [[Mutt]] email client (or just want a simple way to
243 query email addresses from the command line) there are a [number of
244 scripts][mutts] available.  Pick whichever sounds most appealing to
245 you.  I wrote up [[mutt-ldap.py]], which lets you configuration the
246 connection details via a config file (`~/.mutt-ldap.rc`) rather than
247 editing the script itself.  Usage details are available in the
248 docstring.
249
250 Apple Address Book
251 ------------------
252
253 You can configure Apple's [Address Book][aab] to search an LDAP
254 directory.  See [[Humanizing_OS_X]] for details.
255
256 SSL/TLS
257 -------
258
259 It took me a bit of work to get [SSL/TLS][] working with my
260 [[GnuTLS]]-linked OpenLDAP.  First, you'll probably need to generate
261 new SSL/TLS keys (`/etc/openldap/ssl/*`) with [certtool][] (see
262 [[X.509_certificates]]).  Then add the following lines to
263 `/etc/openldap/slapd.conf`:
264
265     TLSCipherSuite NORMAL
266     TLSCACertificateFile /etc/openldap/ssl/ca.crt
267     TLSCertificateFile /etc/openldap/ssl/ldap.crt
268     TLSCertificateKeyFile /etc/openldap/ssl/ldap.key
269     TLSVerifyClient never
270
271 Where `ca.crt`, `ldap.crt`, and `ldap.key` are your new CA,
272 certificate, and private key.  If you want to disable unencrypted
273 connections completely, remove the `ldap://` entry from your `slapd`
274 command line by editing (on Gentoo) `/etc/conf.d/slapd` so it has
275
276     OPTS="-h 'ldaps:// ldapi://%2fvar%2frun%2fopenldap%2fslapd.sock'"
277
278 Now you should be able to restart `slapd` so it will use the new
279 configuration.
280
281 Have clients running on your server use the local socket by editing
282 `/etc/openldap/ldap.conf` to set:
283
284     URI     ldapi://%2fvar%2frun%2fopenldap%2fslapd.sock
285
286 Test your server setup by running (on the server)
287
288     $ ldapsearch -x -b '' -s base '(objectclass=*)'
289
290 Copy your CA over to any client machines (I put it in
291 `/etc/openldap/ssl/ldapserver.crt`), and set them up with the
292 following two lines in `/etc/openldap/ldap.conf`:
293
294     URI         ldaps://ldapserver.example.com
295     TLS_CACERT  /etc/openldap/ssl/ldapserver.crt
296
297 Test your client setup by running (on the client)
298
299     $ ldapsearch -x -b '' -s base '(objectclass=*)'
300
301 You can configure `shelldap` with the following lines in
302 `~/.shelldap.rc`:
303
304     server: ldaps://ldapserver.example.com
305     tls: yes
306     tls_cacert: /etc/openldap/ssl/ldapserver.crt
307
308 You can configure `mutt-ldap.py` with the following lines in
309 `~/.mutt-ldap.rc`:
310
311     port = 636
312     ssl = yes
313
314 Access control and authentication
315 ---------------------------------
316
317 There are a number of possible approaches to authentication for LDAP,
318 so read the [admin manual][admin] for details.  I've got [[Kerberos]]
319 setup on my home system, and I'll walk through this setup here.
320
321 ### Server side
322
323 I expose the LDAPS port to the external world through my router, and I
324 don't want anonymous users to be able to download all my contact
325 information.  The solution to this is to implement [access
326 control][access].  For my situation, the following
327 `/etc/openldap/slapd.conf` directives seemed appropriate:
328
329     access to attrs=uid
330         by anonymous auth
331         by * read
332
333     access to *
334         by self write
335         by anonymous auth
336         by * read
337
338 The first directive allows anonymous users to use the [uid][]
339 attribute when authenticating, and allows authenticated users to read
340 anyone's `uid` attribute.  This keeps users from being able to change
341 their own `uid`.
342
343 The second directive allows authenticated users to update their own
344 entry and to read every entry.  Anonymous are allowed to authenticate
345 themselves, but have no other privileges.
346
347 Alright, so how should user's go about authenticating][security]?
348 We'll want to set `slapd` up as a Kerberos service, and have clients
349 authenticate using [GSSAPI][].
350
351 For the LDAP service, we'll need a `ldap/<fqdn>@REALM` principal.
352 Because we want that service to start automatically at boot, we need
353 to keep its key in a keytab file.
354
355     # kadmin.local -p jdoe/admin
356     Authenticating as principal jdoe/admin with password.
357     Password for jdoe/admin@R.EDU: 
358     kadmin.local:  add_principal -randkey ldap/ldapserver.example.com
359     WARNING: no policy specified for ldap/ldapserver.example.com@R.EDU; defaulting to no policy
360     Principal "ldap/ldapserver.example.com@R.EDU" created.
361     kadmin.local:  ktadd -k /etc/openldap/krb5-ldap.keytab ldap/ldapserver.example.com
362     Entry for principal kdap/ldapserver.example.com...
363     …
364     kadmin.local:  quit
365     # chown ldap:ldap /etc/openldap/krb5-ldap.keytab
366
367 You need use `kadmin.local` here (instead of `kadmin`) so the process
368 has premission to create and edit the keytab file.
369
370 You'll need to point your `slapd` server to the new keytab.  On
371 [[Gentoo]], you do this by uncommenting
372
373     KRB5_KTNAME=/etc/openldap/krb5-ldap.keytab
374
375 in `/etc/conf.d/slapd`.  On Red Hat, you add
376
377     export KRB5_KTNAME=/etc/openldap/ldap.keytab
378
379 to `/etc/sysconfig/ldap`.
380
381 You should also configure your realm and hostname in
382 `/etc/openldap/slapd.conf`:
383
384     sasl-realm      R.EDU
385     sasl-host       ldapserver.example.com
386
387 You'll also want to associate user's Kerberos principles to LDAP DNs.
388 The template `slapd` uses is:
389
390     uid=<primary[/instance]>,cn=<realm>,cn=gssapi,cn=auth
391
392 so `jdoe@R.EDU` is associated with
393
394     uid=jdoe,cn=r.edu,cn=gssapi,cn=auth
395
396 and `jdoe/admin@R.EDU` is associated with
397
398     uid=jdoe/admin,cn=r.edu,cn=gssapi,cn=auth
399
400 You'll probably want to [map these authentication DNs][map] to the
401 appropriate directory entry, for example:
402
403     cn=Jane Doe,ou=people,dc=r,dc=edu
404
405 There are a number of ways to this, but I chose
406
407     authz-regexp
408         uid=([^,]*),cn=r.edu,cn=gssapi,cn=auth
409         ldap:///ou=people,dc=r,dc=edu??one?(uid=$1)
410
411 From the manual:
412
413 > This will initiate an internal search of the LDAP database inside
414 > the slapd server. If the search returns exactly one entry, it is
415 > accepted as being the DN of the user. If there are more than one
416 > entries returned, or if there are zero entries returned, the
417 > authentication fails and the user's connection is left bound as the
418 > authentication request DN.
419
420 [Indexing][index] sounds like a good idea, so we turn it on with
421
422     index cn,sn,mail,uid eq
423     index cn,mail sub
424
425 If you change your index configuration, you'll have to stop `slapd`
426 and run `slapindex` to regenerate the indexes.
427
428 ### Client side
429
430 Users will have to do the usual `kinit` to get their Ticket Granting
431 Ticket (TGT), and then instruct their client software to use GSSAPI
432 (`-Y GSSAPI` with the OpenLDAP client tools).  If you don't want to
433 type `-Y GSSAPI`, you can add
434
435     SASL_MECH GSSAPI
436
437 to your `~/.ldaprc`.  If you're on Gentoo, you'll want the `kerberos`
438 and `sasl` `USE` flags set when you emerge `openldap`.
439
440 #### Reverse DNS issues
441
442 Because my SLAPD server runs on a dynamic IP address, I ran into
443 trouble with reverse DNS.  The client would resolve the server address
444 into an IP, then resolve that IP address to its canonical name, and
445 asks the ticket granting server (TGS) for authorization to use
446 `ldap/<canonical>@REALM`.  Because the dynamic canonical name doesn't
447 match the hostname, the TGS denies the request, leading to output
448 like:
449
450     $ ldapwhoami -Y GSSAPI
451     ldap_sasl_interactive_bind_s: Local error (-2)
452         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)
453
454 And messages like:
455
456     … 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
457
458 in the server's KDC log.
459
460 I tried disabling the reverse DNS lookup with both the `-N` command
461 line option to `ldapwhoami` and the `SASL_NOCANON true` option in
462 `~/.ldaprc`.  I also added:
463
464    [libdefaults]
465        rdns = false
466
467 to my client's `/etc/krb5.conf`.  Even with all of these, I was still
468 getting reverse DNS attempts, so I gave up and just added an entry to
469 `/etc/hosts` to ensure I got the right hostname when the client tried
470 to resolve it.
471
472 You can get more detailed messages from `ldapwhoami` by increasing the
473 debuglevel (for example, with the `-d 1` option), which helps when
474 you're troubleshooting these kinds of issues.  For example:
475
476     $ ldapwhoami -d 1 -Y GSSAPI -d 1
477     …
478     ldap_int_sasl_open: host=some.dynamic.canonical.host.net
479     …
480     $ ldapwhoami -d 1 -Y GSSAPI -N -d 1
481     …
482     ldap_int_sasl_open: host=ldapserver.example.com
483     …
484
485 Currently, `ldapwhoami` and friends will ignore the `SASL_NOCANON`
486 configuration option and only respect the `-N` command line option.
487 I've submitted [an OpenLDAP bug][7271] fixing this, but there is still
488 a reverse DNS call happening at some point.
489
490 Debian-based systems
491 --------------------
492
493 I wanted to mirror my home LDAP info on my public Ubuntu server.
494 Here's a quick rundown of the Ubuntu setup.  Install OpenLDAP:
495
496     $ sudo apt-get install slapd ldap-utils
497
498 Don't serve in the clear:
499
500     $ cat /etc/default/slapd
501     …
502     SLAPD_SERVICES="ldaps:/// ldapi:///"
503     …
504
505 Avoid `Unrecognized database type (hdb)` by loading the `hdb` backend
506 module before declaring `hdb` databases:
507
508     $ sudo cat /etc/ldap/slapd.conf
509     …
510     moduleload back_hdb
511     database hdb
512     …
513
514 Convert the old school `slapd.conf` to the new [slapd.d][]:
515
516     $ sudo mv slapd.d{,.bak}
517     $ sudo mkdir slapd.d
518     $ sudo slaptest -f slapd.conf -F slapd.d
519     …
520     hdb_db_open: database "dc=example,dc=com": db_open(/var/lib/slapd/id2entry.bdb) failed: No such file or directory (2).
521     …
522     slap_startup failed (test would succeed using the -u switch)
523     …
524     $ sudo chown -R openldap.openldap slapd.d
525
526 Don't worry about that `db_open` error, the conversion to `slapd.d`
527 will have completed successfully.
528
529 Set permissions on the database directory (note that the databases
530 should be under `/var/lib/ldap` to match Ubuntu's default apparmor
531 config.  Otherwise you'll see `invalid path: Permission denied` errors
532 when `slapd` tries to initialize the databaes).
533
534     $ sudo chown openldap.openldap /var/lib/ldap/
535     $ sudo chmod 750 /var/lib/ldap/
536
537 Configure your clients
538
539     $ cat /etc/ldap/ldap.conf
540     BASE    dc=example,dc=com
541     URI     ldaps://example.com
542     TLS_CACERT  /etc/ldap/ssl/ldapserver.crt
543
544 Start `slapd` and add it to your default runlevel:
545
546     $ sudo /etc/init.d/slapd start
547     $ sudo update-rc.d slapd defaults
548
549 Finally, import your directory data.  Dump the data on your master
550 server:
551
552     master$ sudo slapcat -b 'dc=example,dc=com' > database.ldif
553
554 Load the data on your slave:
555
556     $ sudo /etc/init.d/slapd stop
557     $ sudo slapadd -l database.ldif
558     $ sudo /etc/init.d/slapd start
559
560 References
561 ----------
562
563 There's a [good overview][schema] of schema and objectclasses by Brian
564 Jones on O'Reilly.  If you want to use inetOrgPerson but also include
565 the countryName attribute, ...
566
567 [LDAP]: http://en.wikipedia.org/wiki/LDAP
568 [rfc4510]: http://tools.ietf.org/html/rfc4510
569 [howto]: http://www.gentoo.org/doc/en/ldap-howto.xml
570 [OpenLDAP]: http://www.openldap.org/
571 [admin]: http://www.openldap.org/doc/admin/
572 [inetOrgPerson]: http://tools.ietf.org/html/rfc2798
573 [abook]: http://abook.sourceforge.net/
574 [LDIF]: http://en.wikipedia.org/wiki/LDAP_Data_Interchange_Format
575 [python-ldap]: http://www.python-ldap.org/
576 [rfc4512]: http://tools.ietf.org/html/rfc4512
577 [shelldap]: http://projects.martini.nu/shelldap/
578 [jpegPhoto]: http://tools.ietf.org/html/rfc2798#section-2.6
579 [cn]: http://tools.ietf.org/html/rfc2798#section-9.1.2
580 [uid]: http://tools.ietf.org/html/rfc2798#page-16
581 [mail]: http://tools.ietf.org/html/rfc2798#section-9.1.3
582 [jpegPhoto]: http://tools.ietf.org/html/rfc2798#section-2.6
583 [rfc2849]: http://tools.ietf.org/html/rfc2849
584 [mutts]: http://wiki.mutt.org/?QueryCommand
585 [aab]: http://support.apple.com/kb/ht2486
586 [SSL/TLS]: http://en.wikipedia.org/wiki/Transport_Layer_Security
587 [certtool]:http://www.gnu.org/software/gnutls/manual/html_node/Invoking-certtool.html#Invoking-certtool
588 [access]: http://www.openldap.org/doc/admin24/access-control.html
589 [security]: http://www.openldap.org/doc/admin24/security.html
590 [GSSAPI]: http://en.wikipedia.org/wiki/Generic_Security_Services_Application_Program_Interface
591 [map]: http://www.openldap.org/doc/admin24/sasl.html#Mapping%20Authentication%20Identities
592 [index]: http://www.openldap.org/doc/admin24/tuning.html#Indexes
593 [7271]: http://www.openldap.org/its/index.cgi?findid=7271
594 [slapd.d]: http://www.openldap.org/doc/admin24/slapdconf2.html
595 [schema]: http://www.oreillynet.com/pub/a/sysadmin/2006/11/09/demystifying-ldap-data.html
596
597 [[!tag tags/linux]]
598 [[!tag tags/tools]]