OpenLDAP

From HerzbubeWiki
Jump to: navigation, search

Contents

Debian packages

Install these packages:

slapd
ldap-utils
phpldapadmin
ldapvi

The following packages may also be required if you want SASL authentication (see corresponding chapter further down):

sasl2-bin
libsasl2-modules-plain


References

OpenLDAP Administrator's Guide 
http://www.openldap.org/doc/admin/
An interesting LDAP guide that sometimes contains in-depth know-how about the new slapd-config configuration scheme
http://www.zytrax.com/books/ldap/
LDAP HOWTO (no longer updated) 
http://howtos.linuxbroker.com/howtoreader.php?file=LDAP-HOWTO.html
RFC 2307 (definitions for nis.schema
ftp://ftp.rfc-editor.org/in-notes/rfc2307.txt


Glossary

See also: http://www.openldap.org/doc/admin/glossary.html.


LDAP 
Lightweight Directory Access Protocol (as opposed to the X.500 "heavyweight" DAP)
directory 
  • a specialized database optimized for reading, browsing and searching (i.e. not writing)
  • a directory consists of n entries that are organized in a hierarchical tree
DIT 
directory information tree
entry 
  • an entry stores information about something as a collection of attributes
  • within a directory an entry is uniquely identified by its DN; this DN consists of the concatenation of
  • the entry's RDN (e.g. cn=John Doe)
  • and the reference to the location within the directory where the entry is stored (e.g. ou=employee, dc=example, dc=org)
  • example of complete DN: dn: cn=Tom Syroid, ou=employee, dc=syroidmanor, dc=com
DN 
distinguished name
RDN 
relative distinguished name
attribute 
  • an attribute has a type
  • an attribute has one or more values
distinguished value 
the value of an attribute that contributes to the DN of an entry; this distinction is important only for attributes that can have multiple values
base DN 
references the root of the LDAP directory tree
OU 
Organizational unit
cn 
Common name
OID 
Object identifier
SASL 
Simple authentication and security layer


Schema definition

Overview

Every attribute of a directory entry has a type. The type definition of an attribute describes the kind of data that the attribute can store. For instance, a type definition could prescribe that the attribute value must have the form of an email address. A very useful feature is that a type definition may also allow an attribute to store multiple values for the same entry.

Every directory entry has a special attribute called

objectClass

The values of the objectClass attribute defines which other attributes are required and allowed in an entry. It can be said that the values of the objectClass attribute determine the schema rules the entry must obey.

A collection of attributetype and objectclass definitions together form a schema definition.


Existing schema definitions

Not everybody using an LDAP directory wants to reinvent the wheel, therefore a number of useful schema definitions already exist (some of them are derived from RFCs and other IETF documents). When you install the slapd package you will find these schema definitions in

/etc/ldap/schema

Note: This is true even after the conversion of the configuration scheme to slapd-config.


Custom schema definitions

When you consider the type of information you want to store in your directory, you may find that the existing schema definitions in /etc/ldap/schema do not suit your needs. For instance, if you want to store information about your friends and relatives, you will find that the InetOrgPerson object class is missing an attribute for storing birthdays.

To solve the problem, you can define your own custom schema that introduces a new objectClass and/or new attribute types. Your schema may define the objectClass from scratch, or it may derive the objectClass from an already existing objectClass and extend it by adding more attributes (if you are familiar with object-oriented programming, you might compare this to the concept of creating a subclass).

You should store your custom schema definitions in

/etc/ldap/schema

Note: If your LDAP server still uses the slapd.conf configuration file, you can write an include statement into the configuration file to pull in the schema file from here. This is not possible, though, if your LDAP server uses the slapd-config configuration scheme. In this case, the custom schema definition file merely exists as a human-readable convenience.


OIDs

Within LDAP schemas, each object class and each attribute type must have a unique OID (object identifier). OIDs are found not only in LDAP, they are also used in many other areas of computing (for instance the SNMP protocol). OIDs are used to globally and uniquely identify any (!!!) kind of object. An overview article about OIDs can be found at Wikipedia:

http://en.wikipedia.org/wiki/Object_identifier

An OID listing service can be found here:

http://www.alvestrand.no/objectid/

When you start your own custom schema definition, you might wonder what kind of OID you should use. To quote from the OpenLDAP Administrator's Guide:

Under no circumstances should you hijack OID namespace!

To obtain a registered OID at no cost, apply for an OID under the Internet Assigned Numbers Authority (IANA) maintained Private Enterprise arc. Any private enterprise (organization) may request an OID to be assigned under this arc. Just fill out the IANA form at http://www.iana.org/cgi-bin/enterprise.pl and your official OID will be sent to you usually within a few days. Your base OID will be something like 1.3.6.1.4.1.X where X is an integer.

I have followed this advice, and I have actually received my own private OID:

1.3.6.1.4.1.18427

Warning: On the application form you have to specify an email address. It is guaranteed that you will receive spam on that address since it will be visible to the world in a public listing.


naef.schema

This schema consists of two types of definitions:

  • The main part are attribute type definitions that are aggregated into the custom object class addressbookEntry. The main reason why I structured the object class like this is because I wanted to be able to import data from the Mac OS X "Address Book" application. This is now obsolete because I have moved my contacts to a CardDAV repository (see the DAViCal page). I retain the definitions here for historical reasons, but in production they could simply be left out.
  • Towards the end there are a number of auxiliary object classes that are used to enable user accounts for certain services. For instance, if the bugzillaAccount object class is attached to an existing user account entry in the directory, that user is then allowed to log in on the bugs.herzbube.ch site. Of course this does not work "automagically", it requires that the service in question is configured to query the LDAP directory accordingly.
# ----------------------------------------------------------------------
# Namespace structure below OID 1.3.6.1.4.1.18427
# 1.3.6.1.4.1.18427
#  +-- 1.3.6.1.4.1.18427.1.*   attributes for object class "addressbookEntry"
#  +-- 1.3.6.1.4.1.18427.2.1   object class "addressbookEntry"
#  +-- 1.3.6.1.4.1.18427.3.*   object class "bugzillaAccount" and its attributes
#  +-- 1.3.6.1.4.1.18427.4.*   object class "davicalAccount" and its attributes
#  +-- 1.3.6.1.4.1.18427.5.*   object class "drupalAccount" and its attributes
# ----------------------------------------------------------------------

attributetype (
   1.3.6.1.4.1.18427.1.1
   NAME 'workPhone'
   SUP telephoneNumber )

attributetype (
   1.3.6.1.4.1.18427.1.2
   NAME 'mobilePhone'
   SUP telephoneNumber )

attributetype (
   1.3.6.1.4.1.18427.1.24
   NAME 'pagerPhone'
   SUP telephoneNumber )

attributetype (
   1.3.6.1.4.1.18427.1.3
   NAME 'otherPhone'
   SUP telephoneNumber )

attributetype (
   1.3.6.1.4.1.18427.1.4
   NAME 'descOtherPhone'
   DESC 'A description that indicates the type of otherPhone'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )

attributetype (
   1.3.6.1.4.1.18427.1.25
   NAME 'preferredPhone'
   DESC 'Contains the name of an LDAP phone attribute (e.g. mobilePhone); this phone number type is the preferred one used by a person.'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512} )

attributetype (
   1.3.6.1.4.1.18427.1.5
   NAME 'homeFax'
   SUP facsimileTelephoneNumber )

attributetype (
   1.3.6.1.4.1.18427.1.6
   NAME 'workFax'
   SUP facsimileTelephoneNumber )

attributetype (
   1.3.6.1.4.1.18427.1.7
   NAME 'otherFax'
   SUP facsimileTelephoneNumber )

attributetype (
   1.3.6.1.4.1.18427.1.8
   NAME 'descOtherFax'
   DESC 'A description that indicates the type of otherFax'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )

attributetype (
   1.3.6.1.4.1.18427.1.26
   NAME 'preferredFax'
   DESC 'Contains the name of an LDAP fax attribute (e.g. workFax); this fax number type is the preferred one used by a person.'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512} )

attributetype (
   1.3.6.1.4.1.18427.1.9
   NAME 'homeEmail'
   SUP mail )

attributetype (
   1.3.6.1.4.1.18427.1.10
   NAME 'workEmail'
   SUP mail )

attributetype (
   1.3.6.1.4.1.18427.1.11
   NAME 'otherEmail'
   SUP mail )

attributetype (
   1.3.6.1.4.1.18427.1.12
   NAME 'descOtherEmail'
   DESC 'A description that indicates the type of otherEmail'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )

attributetype (
   1.3.6.1.4.1.18427.1.27
   NAME 'preferredEmail'
   DESC 'Contains the name of an LDAP email attribute (e.g. homeEmail); this email address type is the preferred one used by a person.'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512} )

attributetype (
   1.3.6.1.4.1.18427.1.13
   NAME 'homepage'
   SUP labeledURI )

attributetype (
   1.3.6.1.4.1.18427.1.14
   NAME 'nickName'
   SUP name )

attributetype (
   1.3.6.1.4.1.18427.1.15
   NAME 'dateOfBirth'
   DESC 'Must be in format yyyymmddHHMMZ. The trailing Z is a literal and means that the value is in GMT/UTC'
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
   EQUALITY generalizedTimeMatch
   ORDERING generalizedTimeOrderingMatch
   SINGLE-VALUE )

attributetype (
   1.3.6.1.4.1.18427.1.16
   NAME ( 'imICQ' 'instantMessagingNumberICQ' )
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

attributetype (
   1.3.6.1.4.1.18427.1.17
   NAME ( 'imAIM' 'instantMessagingNumberAIM' )
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

attributetype (
   1.3.6.1.4.1.18427.1.18
   NAME ( 'imJabber' 'instantMessagingNumberJabber' )
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

attributetype (
   1.3.6.1.4.1.18427.1.19
   NAME ( 'imMSN' 'instantMessagingNumberMSN' )
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

attributetype (
   1.3.6.1.4.1.18427.1.20
   NAME ( 'imYahoo' 'instantMessagingNumberYahoo' )
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

attributetype (
   1.3.6.1.4.1.18427.1.21
   NAME ( 'imOther' 'instantMessagingNumberOther' )
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

attributetype (
   1.3.6.1.4.1.18427.1.22
   NAME ( 'descIMOther' 'descInstantMessagingNumberOther' )
   DESC 'A description that indicates the type of instantMessagingNumberOther'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )

attributetype (
   1.3.6.1.4.1.18427.1.28
   NAME ( 'preferredIM' 'preferredInstantMessagingNumber' )
   DESC 'Contains the name of an LDAP IM number attribute (e.g. imICQ); this IM number type is the preferred one used by a person.'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{512} )

attributetype (
   1.3.6.1.4.1.18427.1.23
   NAME 'partner'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )

attributetype (
   1.3.6.1.4.1.18427.1.29
   NAME 'addressType'
   DESC 'Describes the type of the address for a person (e.g. home, work).'
   EQUALITY caseIgnoreMatch
   SUBSTR caseIgnoreSubstringsMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )

# As a reference, the three objectclasses that addressbookEntry is derived from
# are listed here.
#
# From core.schema:
# objectclass ( 2.5.6.6 NAME 'person'
#       DESC 'RFC2256: a person'
#       SUP top STRUCTURAL
#       MUST ( sn $ cn )
#       MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) )
#
# From core.schema:
# objectclass ( 2.5.6.7 NAME 'organizationalPerson'
#       DESC 'RFC2256: an organizational person'
#       SUP person STRUCTURAL
#       MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $
#               preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $
#               telephoneNumber $ internationaliSDNNumber $
#               facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $
#               postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) )
#
# From inetorgperson.schema:
# objectclass   ( 2.16.840.1.113730.3.2.2
#    NAME 'inetOrgPerson'
#       DESC 'RFC2798: Internet Organizational Person'
#    SUP organizationalPerson
#    STRUCTURAL
#       MAY (
#               audio $ businessCategory $ carLicense $ departmentNumber $
#               displayName $ employeeNumber $ employeeType $ givenName $
#               homePhone $ homePostalAddress $ initials $ jpegPhoto $
#               labeledURI $ mail $ manager $ mobile $ o $ pager $
#               photo $ roomNumber $ secretary $ uid $ userCertificate $
#               x500uniqueIdentifier $ preferredLanguage $
#               userSMIMECertificate $ userPKCS12 )
#       )

objectclass (
   1.3.6.1.4.1.18427.2.1
   NAME 'addressbookEntry'
   SUP inetOrgPerson
   DESC 'Data for one addressbook entry'
   STRUCTURAL
   MAY
   (
      nickName $ countryName $ addressType $
      homePhone $ workPhone $ mobilePhone $ pagerPhone $ otherPhone $ descOtherPhone $ preferredPhone $
      homeFax $ workFax $ otherFax $ descOtherFax $ preferredFax $
      homeEmail $ workEmail $ otherEmail $ descOtherEmail $ preferredEmail $
      imICQ $ imAIM $ imJabber $ imMSN $ imYahoo $ imOther $ descIMOther $ preferredIM $
      homepage $ dateOfBirth $ partner
   ))

# ----------------------------------------------------------------------
# Define an auxiliary object class that can be used to augment a
# directory entry with a single email address attribute. This email
# address attribute is intended for use by the Bugzilla bugtracker.
# ----------------------------------------------------------------------
attributetype (
   1.3.6.1.4.1.18427.3.2
   NAME 'bugzillaEmail'
   SUP mail )

objectclass (
   1.3.6.1.4.1.18427.3.1
   NAME 'bugzillaAccount'
   SUP top
   DESC 'Attributes that describe a Bugzilla account'
   AUXILIARY
   MUST bugzillaEmail
   MAY displayName )

# ----------------------------------------------------------------------
# Define an auxiliary object class that can be used to identify a
# directory entry as a DAViCal account.
# ----------------------------------------------------------------------
attributetype (
   1.3.6.1.4.1.18427.4.2
   NAME 'davicalEmail'
   SUP mail )

objectclass (
   1.3.6.1.4.1.18427.4.1
   NAME 'davicalAccount'
   SUP top
   DESC 'Attributes that describe a DAViCal account'
   AUXILIARY
   MUST davicalEmail
   MAY displayName )

# ----------------------------------------------------------------------
# Define an auxiliary object class that can be used to identify a
# directory entry as a Drupal account.
# ----------------------------------------------------------------------
attributetype (
   1.3.6.1.4.1.18427.5.2
   NAME 'drupalUid'
   SUP uid )

attributetype (
   1.3.6.1.4.1.18427.5.3
   NAME 'drupalMail'
   SUP mail )

objectclass (
   1.3.6.1.4.1.18427.5.1
   NAME 'drupalAccount'
   SUP top
   DESC 'Attributes that describe a Drupal account'
   AUXILIARY
   MUST ( drupalUid $ drupalMail )
   MAY displayName )


Notes about schema definition

  • The opening paranthesis of an objectclass or attributetype definition must be located on the same line as the keyword
  • The closing paranthesis of an objectclass or attributetype definition must be located on the last line of the definition. The closing paranthesis cannot be placed alone on the last line!!!


Organisation

Basics

Base DN:

dc=herzbube,dc=ch

Top-level entries:

ou=addressbook (obsolete)
ou=bookmarks (obsolete)
ou=groups
ou=hosts
ou=samba (obsolete)
ou=users


ou=users

Entries located below ou=users,dc=herzbube,dc=ch represent users or roles that are needed for authentication in various systems, services and applications. The minimal requirement is that the entries possess a name and a password attribute.


Name:

  • The actual attribute that provides the "name" value varies, depending on the system, service or application performing the authentication
  • For instance, LDAP authentication uses the cn attribute because cn is the RDN attribute
  • PAM on the other hand uses the uid attribute


Password:

  • The main attribute that provides the "password" value always is the userPassword attribute
  • Certain services may store the password in various representations in additional attributes (e.g. Samba stores an MD4 hash in sambaNTPassword)


Core rules that an ou=users entry must follow:

  • objectClass = organizationalRole
    • Provides basic attributes such as cn and description
    • cn is the RDN attribute of the entry
    • This is the structural object class
  • objectClass = simpleSecurityObject
    • Adds the attribute userPassword
    • The password stored in this attribute is used for binding to the LDAP directory
    • Binding occurs...
      • ... either with the goal to gain access to parts of the directory
      • ... or to perform authentication on behalf of an external system, service or application
      • In the latter case, the external system, service or application may also be interested in accessing additional attributes of the entry such as "displayName" to get the full user name
    • This is an auxiliary object class


More rules that depend on the system, service or application that should be able to use the entry:

  • objectClass = uidObject
    • Adds the attribute uid; the value must be unique (i.e. no two entries must have the same value)
    • Must be used on entries that are used to login to a service or application (entries typically represent human users or user roles)
    • May be used on entries that represent UNIX system users, although such entries have objectClass = posixAccount which also provides the uid attribute
    • Must not be used on entries that exist only due to IT requirements (e.g. an entry that a webmail application uses to bind for searching the address book)
    • The value of the uid attribute is the user name entered during a classic login procedure
    • This is an auxiliary object class
  • objectClass = posixAccount
    • Must be used on entries that represent UNIX system user accounts
    • Adds various attributes that describe the user account in a similar fashion as it would appear in /etc/passwd
    • See PAM page for details
    • This is an auxiliary object class
  • objectClass = shadowAccount
    • Must be used on entries that represent UNIX system user accounts
    • Adds various attributes that describe the user account in a similar fashion as it would appear in /etc/shadow
    • See PAM page for details
    • This is an auxiliary object class
  • objectClass = bugzillaAccount
    • Must be used on entries that are used to access Bugzilla
    • See Bugzilla page for details
    • This is an auxiliary object class
  • objectClass = sambaSamAccount
    • Must be used on entries that are used to access the Samba service
    • See Samba page for details
    • This is an auxiliary object class
  • objectClass = davicalAccount
    • Must be used on entries that are used to access DAViCal
    • See DAViCal page for details
    • This is an auxiliary object class
  • objectClass = drupalAccount
    • Must be used on entries that are used to access Drupal
    • See Drupal page for details
    • This is an auxiliary object class


ou=groups

Entries located below ou=groups,dc=herzbube,dc=ch represent UNIX system groups traditionally stored in /etc/group. They must follow these rules:

  • objectClass = posixGroup
    • The memberUid attribute must contain one value for each member of the group
    • A memberUid value must correspond to an uid attribute value of one of the entries below ou=groups,dc=herzbube,dc=ch
    • The gidNumber attribute value must be a unique numerical ID
    • See PAM page for details
    • This is the structural object class
  • objectClass = sambaGroupMapping
    • Must be used on entries that are used to access the Samba service
    • See Samba page for details
    • This is an auxiliary object class


ou=hosts

Entries located below ou=hosts,dc=herzbube,dc=ch represent IP hosts traditionally stored in /etc/hosts. They must follow these rules:

  • objectClass = ipHost
    • Adds the essential attributes cn and ipHostNumber
    • The value of cn that contributes to the DN (the distinguished value) is the canonical name of the host (see RFC2307)
    • Additional values of cn are the aliases of the host
    • This is an auxiliary object class
  • objectClass = device
    • The definition of object class ipHost in RFC2307 says that "device SHOULD be used as a structural class"
    • Note that this recommendation is not present in /etc/ldap/schema/nis.schema, it must be looked up in the RFC document itself
    • Currently I don't use any of the attributes of device, I just follow the recommendation of RFC2307
    • This is the structural object class


ou=samba

There is currently only one entry located below ou=samba,dc=herzbube,dc=ch which represents the Samba domain for the file server pelargir.

This entry is not manually crafted. It is automatically created by Samba, as sambaDomainName=PELARGIR,dc=herzbube,dc=ch, when Samba is restarted after its configuration has been changed to use LDAP.

To remove clutter from the base DN dc=herzbube,dc=ch, I created the ou=samba subtree and manually moved the sambaDomain entry to this subtree. It appears that this works because Samba does not search for the sambaDomain entry in a specific subtree, but instead starts its search in the LDAP directory root. I do not expect this to have much negative impact because I do not expect many Samba connections. Hopefully, proper indexing will also reduce search time.


Binding

The act of being authenticated by an LDAP directory is called "binding".

To bind, a client must specify a DN and a password. For successful binding, the entry matching the specified DN must contain an attribute userPassword whose value matches the specified password. The value of the attribute is stored as

{encoding type}<encoded password>

There are 4 types of authentication

  • Anonymous: An empty DN and password are transmitted
  • Simple: The password is transmitted in the clear
  • SSL/TLS: The password is transmitted over an encrypted transport layer
    • SSL = port 636
    • TLS = port 389
  • SASL: Authentication via negotiable scheme

Note on TLS: The client must explicitly ask for TLS encryption by issuing the LDAP command StartTLS.

Note on SASL: Authentication happens before the optional encrypted transport layer is negotiated, i.e. the password hash (or the clear-text password if the negotiated SASL scheme is PLAIN) is not transmitted over the encrypted layer!


Configuration Part 1: slapd.conf vs. slapd-config

Overview

In older versions of OpenLDAP, the daemon/server configuration was stored in a single file

/etc/ldap/slapd.conf

Support for this file is going to be withdrawn in some future version of OpenLDAP. The reason for this is that OpenLDAP 2.3 introduced a new type of configuration called slapd-config. Data for this configuration system is stored in a collection of LDIF files that are located in this folder

/etc/ldap/slapd.d


slapd-config

From the point of view of the LDAP server, the slapd-config configuration system is just another LDAP directory with the following properties

  • Its root entry has the DN cn=config
  • It is managed using standard LDAP operations
  • It has a special pre-defined schema
  • It is managed by a special backend which is responsible for storing the configuration data in LDIF files
  • Changing the configuration usually does not require a server restart for the changes to take effect


Useful references are


Access to slapd-config data

Although the slapd-config configuration system stores its data as LDIF files, these files must never be edited! Configuration changes must always be performed via LDAP operations, e.g. using the convenient ldapvi or the more basic ldapadd, ldapdelete, or ldapmodify.


The following command lists all entries in the configuration database. No password is required, but the command must be executed while logged in as root on the machine that runs the LDAP server.

ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=config"


Discussion:

  • -Y EXTERNAL = Use the SASL mechanism "EXTERNAL" to authenticate. See below for more information about this.
  • -H ldapi:/// = URI to use for the connection, in this case we connect via Unix domain socket. Only this type of connection is allowed for the SASL EXTERNAL mechanism. See below for more information about this.
  • -b "cn=config" = The base DN to use for the search


When the SASL EXTERNAL mechanism is used, the following happens:

  • The server (not the client!) generates a bind DN that follows this pattern: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
  • Group and user ID are taken from the uid/gid of the client process that connects. In the example above, the client process was the ldapsearch utility, and it was run while logged in as root.
  • The uid/gid of the client process are available because the connection was made via Unix domain socket (ldapi:///), which is capable of reporting uid/gid of the connecting process
  • The entire "config" database is protected by an ACL that looks like this (see the Access rights section for details on ACLs):
access to *
    by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=externalcn=auth manage
    by * break
access to *
    by * none
  • As we can see, one and only one user/group ID combination has any access rights to the "config" database, everybody else is locked out. That user/group ID combination is 0/0, which stands for root/root.


This scheme is foolproof as long as

  1. The client is not allowed to specify its own bind DN (in which case it could simply forge the necessary DN)
    • The client is not allowed to specify its own bind DN, as we have seen SASL EXTERNAL over ldapi:/// causes the server to generate its own bind DN.
  2. The client process cannot spoof its uid/gid
    • We must assume that this is true, otherwise we have a fundamental problem with the system
  3. The client process is allowed to authenticate only with SASL EXTERNAL when accessing the "config" database
    • I have not found any documentation that specifies this
    • I have also not found indicators, e.g. by looking at the config database content, or by experimenting on the command line
    • So far I have been unable to fabricate a login via any of the methods known to me


Converting slapd.conf to slapd-config

The slaptest command line utility can be used to convert from the old file based system to the new slapd-config system. The command is this:

slaptest -f /path/to/slapd.conf -F /path/to/slapd.conf.d

Discussion:

  • The input is an old-style configuration file.
  • Important: The input file does not need to be a complete server configuration, it can also be a partial configuration, or even just a single option. This is useful to find out how the old configuration statements translate into the new LDIF schema, and - even more importantly - it can also be "exploited" to add a custom schema to the slapd-config system with relative ease. More on that later.
  • The output is placed in the specified folder. When you execute slaptest, the output folder must already exist, otherwise the command will fail.
  • slaptest does not overwrite files that already exist in the output folder - but neither does it indicate that it did not write some (or all) of its output. So, to make sure that the output matches the content of the input file, I highly recommend to always point slaptest at an empty folder!


Changing existing slapd-config data

Further up we saw how to connect to the "config" database. Once you know how to do this, changing the "config" database data is as "simple" as using ldapvi:

ldapvi --host ldapi:/// --sasl-mech EXTERNAL --base cn=config


When you can't use ldapvi you have to manually prepare an LDIF file with the desired changes and then run ldapmodify or ldapdelete with the file. As you can imagine, this is not as straightforward as editing a config file and then restarting the server - actually it's a barely manageable process and, in my very personal opinion, a huge clusterfuck! LDAP has always been a rather arcane subject, only to be tackled when equipped with the appropriate black magic tomes and with large amounts of time at hand. But with the introduction of the slapd-config system the OpenLDAP project has managed to turn even server administration into a nightmare.


Back to the subject. If in doubt how to write your LDIF file, read the man pages for the LDAP command line tools or consult a search engine. Further down there is also a section with some recipes.


Manually changing slapd-config data

It turns out that you can modify slapd-config data manually with an editor such as vi. The main deterrent to this is that the data files contain a CRC32 checksum, so if you modify the data files without fixing the checksum you will get a warning in the log file that looks like this:

Jan 22 15:44:10 pelargir slapd[47138]: ldif_read_file: checksum error on "/etc/ldap/slapd.d/cn=config.ldif"

Fortunately this is not a hard error, i.e. the OpenLDAP server will start up despite the checksum mismatch. This means that for a quick test you can just edit the data files and try something out.


How to fix the CRC32 checksum? It is calculated over the lines in a data file that are not comments. With this command you can prepare a temporary file used as input for the checksum calculation:

grep -v '^#' slapd.d/cn\=config.ldif >/tmp/foo

To recalculate the new checksum you need the crc32 utility that comes with the Debian package libarchive-zip-perl. Install that package if you don't have it yet, then run this command:

crc32 /tmp/foo

It will print out the correct checksum, which you can then simply use to edit the data file. The checksum is located at the top of the data file, on line 2 in this example::

root@pelargir:~# cat /etc/ldap/slapd.d/cn\=config.ldif 
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 b5fe9372
dn: cn=config
[...]

Finally, you need to restart the OpenLDAP service so that it picks up the change:

systemctl restart slapd.service


slapd.conf option names vs. slapd-config attribute names

Generally there is a one-to-one correspondence between the attributes and the old-style slapd.conf configuration keywords, using the keyword as the attribute name, with an "olc" prefix attached.

Examples:

  • loglevel becomes olcLogLevel
  • password-hash becomes olcPasswordHash

"olc" is short for "OpenLDAP Configuration".


Configuration Part 2: Concrete configuration

Configuration basics

The configuration in /etc/ldap (either single file slapd.conf or directory-based slapd-config) contains the options for the slapd daemon. These options can be divided into two types:

  • Database definitions: The configuration can contain 0-n database definitions, and their options
  • Global options, i.e. options that are not tied to any specific database.


Global option: Log level

To change the log level, the configuration option for slapd.conf is

loglevel stats none

Discussion:

  • This combines the two log levels "stats" and "none" by OR'ing their numerical values
  • stats = The default log level, which logs connections, LDAP operations, and results
  • none = Logs critical messages. Note that "none" is a complete misnomer.


The option's attribute name for slapd-config is olcLogLevel. This is an LDIF snippet that can be used to add the option:

dn: cn=config
changetype: modify
add: olcLogLevel
olcLogLevel: stats none


Global option: Password storage

  • Basically a client can store passwords in whatever format it likes. When it reads the password it is up to the client to correctly interpret the password attribute value.
  • Typically, if a client stores the password in a hashed format, the client prefixes the value with an indicator of the algorithm that was used to form the hash.
  • A client may also store a password in clear text, in which case the password has no prefix.
  • OpenLDAP supports automatic hashing of clear text passwords, with a range of hashing algorithms to choose from. The strongest algorithm known to OpenLDAP is SSHA.
  • If automatic hashing is enabled and a client wants to store a password without a prefix, then OpenLDAP assumes that this is a password in clear text and automatically hashes the password with the configured algorithm. It then stores the password with a prefix that indicates the algorithm that was used to form the hash.
  • If a client reads the password for its own consumption, it will, of course, receive the value that consists of prefix + hashed password. This is the only possible outcome since hashing algorithms are one-way algorithms.
  • If automatic hashing is enabled and a client attempts to bind with a clear-text password, OpenLDAP transparently and automatically hashes the password so that it is possible to compare the binding password with the stored password.
  • The configuration option for slapd.conf is
password-hash {SSHA}
  • The option's attribute name for slapd-config is olcPasswordHash. This is an LDIF snippet that can be used to add the option:
dn: cn=config
changetype: modify
add: olcPasswordHash
olcPasswordHash: {SSHA}


Global option: Allow binding through LDAPv2

Note: Currently this section only contains the options that go into the old slapd.conf config file, there's no information yet about the new slapd-config format.

  • The use of LDAPv2 should generally be avoided
  • But some old applications may require this. One known example is ldapnavigator.
  • The configuration option for slapd.conf is
allow bind_v2


Global option: Adding schemas

With slapd.conf it is very simple to add a new schema. Just add a line like this to slapd.conf:

include /etc/ldap/schema/naef.schema


With slapd-config, adding a new schema is much more complicated. I found a solution in post #2 in this linuxquestions.org forum thread. Here is my procedure to work the magick:

  • Check the order in which the current slapd-config lists schemas
$ ls -l /etc/ldap/slapd.d/cn\=config/cn\=schema
total 48
drwxr-x--- 2 openldap openldap  4096 Jun  8 15:36 .
drwxr-x--- 3 openldap openldap  4096 Jun  9 03:02 ..
-rw------- 1 openldap openldap 15596 Jun  8 14:38 cn={0}core.ldif
-rw------- 1 openldap openldap 11381 Jun  8 14:38 cn={1}cosine.ldif
-rw------- 1 openldap openldap  6513 Jun  8 14:38 cn={2}nis.ldif
-rw------- 1 openldap openldap  2875 Jun  8 14:38 cn={3}inetorgperson.ldif
  • Create a pseudo slapd.conf file that contains the schemas listed above in the listed order, plus appends the schema you want to add (naef.schema in this example)
$ cat pseudo-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/naef.schema
  • Convert the pseudo slapd.conf file to LDIF
$ mkdir pseudo-slapd.d
$ slaptest -f pseudo-slapd.conf -F pseudo-slapd.d
  • Edit the LDIF file that contains the data from the new schema
$ vi pseudo-slapd.d/cn\=config/cn\=schema/cn\=\{4\}naef.ldif
  • Change the following attributes
    • Old
dn: cn={4}naef
cn: {4}naef
    • New
dn: cn=naef,cn=schema,cn=config
cn: naef
  • Remove the following attributes at the end of the file. Note that your values will differ from the ones in this example; what is important are the attribute names.
structuralObjectClass: olcSchemaConfig
entryUUID: 33c82b0a-c51a-1035-994c-b1dff6578926
creatorsName: cn=config
createTimestamp: 20160612184956Z
entryCSN: 20160612184956.921350Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20160612184956Z
  • Add the LDIF to the slapd-config configuration
$ ldapadd -Y EXTERNAL -H ldapi:/// -f pseudo-slapd.d/cn\=config/cn\=schema/cn\=\{4\}naef.ldif
  • Backup the LDIF file
$ mv pseudo-slapd.d/cn\=config/cn\=schema/cn\=\{4\}naef.ldif /etc/ldap/schema/naef.ldif
  • Cleanup
rm pseudo-slapd.conf
rm -r pseudo-slapd.d


SSL/TLS

Overview

There are two ways how communication between LDAP server/client can be encrypted with SSL/TLS:

  • LDAP over SSL/TLS (LDAPS): Through TCP port 636
  • StartTLS: Through TCP port 389. Because normal communication over this port is not encrypted, the client must explicitly ask for TLS encryption by issuing the StartTLS command.


Server configuration

To enable TLS, the configuration options for slapd.conf are:

  • Where the server certificate and its RSA key can be found
TLSCertificateFile      /etc/letsencrypt/live/herzbube.ch/cert.pem
TLSCertificateKeyFile   /etc/letsencrypt/live/herzbube.ch/privkey.pem
  • Where the CA certificate can be found (only necessary if the server certificate is signed; note: specify the entire certificate chain up to the root certificate)
TLSCACertificateFile    /etc/letsencrypt/live/herzbube.ch/fullchain.pem


With the slapd-config configuration scheme, the options can be added relatively straightforward with ldapvi:

ldapvi --host ldapi:/// --sasl-mech EXTERNAL --base cn=config

This is the content I added:

olcTLSCertificateFile: /etc/letsencrypt/live/herzbube.ch/cert.pem
olcTLSCertificateKeyFile: /etc/letsencrypt/live/herzbube.ch/privkey.pem
olcTLSCACertificateFile: /etc/letsencrypt/live/herzbube.ch/fullchain.pem

The same can also be achieved with an LDIF file and using ldapmodify:

ldapmodify -H ldapi:/// -Y EXTERNAL -f foo.ldif

Assuming the attributes do not yet exist, the LDIF file looks like this:

dn: cn=config
changetype: modify
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/letsencrypt/live/herzbube.ch/cert.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/letsencrypt/live/herzbube.ch/privkey.pem
-
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/letsencrypt/live/herzbube.ch/fullchain.pem


Additional server configuration

Here are a few additional options which I'm no longer using:

  • TLSCipherSuite / olcTLSCipherSuite
    • This can be used to restrict the allowed TLS ciphers.
    • Important: The names you can use here depend on the TLS library used by OpenLDAP. Originally OpenLDAP on Debian used OpenSSL, but a few years back they switched to GnuTLS.
    • Example: OpenSSL has a number of convenient names for cipher collections. For instance, "HIGH" refers to ciphers that require keys with length >= 128 Bits. You can't use "HIGH" with Debian OpenLDAP anymore, though, because GnuTLS does not know this cipher name.
    • This article goes into some details about the OpenSSL/GnuTLS cipher naming issue.
    • Because GnuTLS does not support any convenient cipher collection names, I have decided to no longer specify the TLSCipherSuite option.
  • security tls=128 / olcSecurity: tls=128
    • This is not a global option, it's an option that has be made on a specific LDAP database.
    • If the value is "tls=<number>" this specifies the minimum TLS strength encryption that is required.
    • In that case the use of TLS is enforced, even if the connection is made via UNIX domain socket. Because TLS is not possible via UNIX domain socket, this effectively prevents accessing a backend via UNIX domain socket.
    • Because of this I am no longer using the security option.
  • TLSVerifyClient never
    • The server never asks the client for a certificate
    • It would be nice if we could say "try" for this option, in which case the server would ask the client for a client certificate: if the client provides none the session proceeds normally, but if the client provides a certificate, that certificate must be valid. Unfortunately, we can't use "try" because this doesn't always seem to work - for instance, ldapsearch seems to always send a certificate on the server's request, but it is a bad certificate, and I have no idea where ldapsearch gets that certificate from...


Permissions

Because the Let's Encrypt folder structure has special access permissions, the OpenLDAP server can't read the certificate and key files just like that. Because the daemon runs as system user openldap (see /etc/default/slapd) the solution is to add the system user openldap to the system group ssl-cert.


Client configuration

If the server certificate is signed by a CA (i.e. it is not self-signed), all LDAP clients need access to the CA certificate so that the server certificate's validity can be verified. If the server certificate was signed by an intermediate CA, the entire certificate chain up to the root CA must be known to the client.


If the certificate is signed by a globally known root CA such as Let's Encrypt, there is no problem. With CAcert or a homebrew CA, however, things get more complicated.

  • For clients that run on pelargir (e.g. ldapsearch, PHP applications), the CA certificate can easily be configured in /etc/ldap/ldap.conf:
TLS_CACERT /etc/ssl/certs/cacert.org.certchain
  • For clients that run on other machines, the CA certificate must be made known in other ways.


Authentication

Regular authentication

There are two configuration options from which you can choose to prevent anonymous access when simple binding occurs. Only add one of them to the configuration, not both!

The configuration options for slapd.conf are

require authc
disallow bind_anon


The second option's attribute name for slapd-config is olcDisallows. This is an LDIF snippet that can be used to add the option:

dn: cn=config
changetype: modify
add: olcDisallows
olcDisallows: bind_anon


SASL authentication

Note 1: This section contains information that is untested and probably does not work properly!

Note 2: Currently this section only contains the options that go into the old slapd.conf config file, there's no information yet about the new slapd-config format.

See SASL for an overview of SASL.

To enable SASL in OpenLDAP, the configuration options for slapd.conf are:

sasl-host ldap.herzbube.ch   [Note: use FQDN]
sasl-real ldap
sasl-secprops noplain,noanonymous

To enforce SASL authentication, the configuration option for slapd.conf is:

require SASL


Database definition

Overview

Each LDAP directory is stored in its own database. The most important characteristics of a database are:

  • The base DN of the LDAP directory that is stored in the database
  • The backend type, i.e. which format is used to store the data
  • Storage location in the filesystem
  • Indices
  • Access rules


When you install the slapd package, a new LDAP directory is created for you that looks like this:

root@pelargir:~# cat /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1\}mdb.ldif 
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 21ed2a25
dn: olcDatabase={1}mdb
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbDirectory: /var/lib/ldap
olcSuffix: dc=herzbube,dc=ch
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonym
 ous auth by * none
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by * read
olcLastMod: TRUE
olcRootDN: cn=admin,dc=herzbube,dc=ch
olcRootPW:: secret
olcDbCheckpoint: 512 30
olcDbIndex: objectClass eq
olcDbIndex: cn,uid eq
olcDbIndex: uidNumber,gidNumber eq
olcDbIndex: member,memberUid eq
olcDbMaxSize: 1073741824
[...]


Database definition in slapd.conf

In slapd.conf, a database definition must start with the keyword "database". At the same time this defines the backend type of the database. For instance:

# Begin a new database definition
database bdb

# The base DN of the directory
suffix "dc=herzbube,dc=ch"

# Storage location in the filesystem
directory "/var/lib/ldap/herzbube.ch"


Indices in slapd.conf

Indices are vital for accessing a directory's content by searching. No indices, or the wrong indices, will cause a directory search to take an unbearable amount of time.

Every index of a directory starts with the index keyword. Next comes the name of the attribute that should be indexed. The last part is a comma-separated list of index types. For instance:

index cn sub,approx

The four index types are:

  • approx (approximate): index the information for an approximate, or phonetic, match of an attribute's value
  • eq (equality): index the information necessary to perform an exact match of an attribute value; the matching rule in the attribute's syntax defines whether the match may be case-sensitive or whitespace-sensitive
  • pres (presence): index the information necessary to determine if an attribute has any value at all
  • sub (substring): index the information necessary to perform a simple substring match on attribute values

My indices are:

# Basic indices
index objectClass eq
index cn sub,approx
# Indices for addressbookEntry
index mail sub,approx
index description sub,approx
index l sub,approx
# Indices for PAM/NSS (taken from Samba3-HOWTO)
index uidNumber eq
index gidNumber eq
index memberUid eq
# Indices for Samba (taken from Samba3-HOWTO)
index sambaSID eq,sub
index sambaPrimaryGroupSID eq
index sambaDomainName eq
index default sub
## required to support pdb_getsampwnam
index uid pres,sub,eq
## required to support pdb_getsambapwrid()
index displayName pres,sub,eq

After changing the index configuration, the indices need to be updated/regenerated using the slapindex tool. Unfortunately, this requires that the slapd daemon must be stopped first. For instance, to re-index database 1:

/etc/init.d/slapd stop
/usr/sbin/slapindex -n 1
/etc/init.d/slapd start

After running slapindex as root, any index files now owned by root must be given back to the user that slapd is running as:

chown openldap:openldap /var/lib/ldap/herzbube.ch/*


Indices in slapd-config

Note: Please read the previous section to understand the purpose of the indices in this section.


These are the slapd.conf options that I want to "convert" to slapd-config. They are only a subset of the indices described in the previous section because my requirements have changed since I wrote the previous section (e.g. I no longer use Samba on the outsourced dedicated server).

# Basic indices
index objectClass eq
index cn sub,approx
# Indices for PAM/NSS (taken from Samba3-HOWTO)
index uidNumber eq
index gidNumber eq
index memberUid eq

The "index" option's attribute name for slapd-config is olcDbIndex. The slapd.conf options from above can therefore be loosely translated into these slapd-config lines:

olcDbIndex: objectClass eq
olcDbIndex: cn sub,approx
olcDbIndex: uidNumber eq
olcDbIndex: gidNumber eq
olcDbIndex: memberUid eq

Most of these lines are already present in the default database that is created when the slapd Debian package is installed. There is only one exception:

olcDbIndex: cn sub,approx

The default database already has an index for the "cn" attribute, but that default index only has the "eq" index type. On the other hand, we would like to index the "cn" attribute with "sub,approx". The problem is that the olcDbIndex attribute allows only one value per attribute, so we have to combine the default index type with our custom index types:

olcDbIndex: cn eq,sub,approx

Here is the final LDIF, generated by ldapvi:

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcDbIndex
olcDbIndex: cn eq,sub,approx
olcDbIndex: uid eq
olcDbIndex: member,memberUid eq
olcDbIndex: objectClass eq
olcDbIndex: uidNumber,gidNumber eq


After changing the index configuration, slapd automatically updates its indices to match the new configuration. In other words, no need to run slapindex after the configuration change, so this is the first time that slapd-config is better than slapd.conf. Also see http://www.zytrax.com/books/ldap/apa/indeces.html.


Caching

Another means to increase performance when accessing LDAP directories is caching.

TBD

The O'Reilly book talks of caching a number of entries. The Debian sample configuration file slapd.conf contains an option that reserves memory for DB caching.


Access rights in slapd.conf

It is possible to specify credentials for a super user account in the slapd configuration. Personally, I do not like this as it means that yet another file needs to be protected because it contains a password. Instead, I prefer to use ACLs for managing the access rights to my directory.

An ACL (access control list) is a rule that defines who has what kind of access rights to what.

Possible forms of "who" are

  • * matches any connect user (including anonymous connections)
  • self matches the DN of the currently connected (and authenticated) user
  • anonymous matches non-authenticated connections
  • users matches authenticated connections
  • regular expression matches a DN

Possible types of access rights are

  • write
  • read
  • search
  • compare
  • auth (access only for the purpose of binding/authenticating)
  • none


The order in which ACLs appear in the slapd configuration is important! The first ACL that matches an operation is used for that operation. This means that more specific ACLs must appear before less specific ones.

Example for an ACL:

access to attrs=userPassword,shadowLastChange
        by dn="cn=admin,ou=users,dc=herzbube,dc=ch" write
        by anonymous auth
        by self write
        by * none

What does this ACL mean? It refers to the attributes userPassword and shadowLastChange. It restricts access to the attributes as follows:

  • the attributes can be changed (write) by the entry owning them if they are authenticated (self)
  • anonymous users have access only for the purpose of binding
  • others have no access at all
  • except the admin entry, which can change the attributes everywhere


Note: An ACL that does not explicitly end with a statement that regulates access for "*" has a default statement "by * none"


At the moment I have defined the following ACLs for my directory. Keep in mind that I have disabled anonymous access by specifying "disallow bind_anon" in the slapd configuration).

# Default access
# - the admin dn has full write access (specify this access right
#   at the beginning so we don't have to repeat it again on each rule)
# - all others have no access; the "break" statement tells slapd to
#   continue evaluating rules: the result is that certain rules
#   further down may give the user more rights on specific DNs
# - general READ (!) access is denied only at the very end, by the
#   implicit ACL "access to * by * none"
access to *
  by dn.exact="cn=admin,ou=users,dc=herzbube,dc=ch" write
  by * none break

# The userPassword, of course, needs to be accessible by anonymous
# for authentication (binding). Once authenticated, it can be changed
# by the entry owning it (this is used e.g. by PAM). Others should
# not be able to see it, except the libnss-ldap-root entry (which is
# used by libnss-ldap if it runs as root)
access to attrs=userPassword,shadowLastChange
  by anonymous auth
  by self write
  by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read
  by * none

# The Samba password-related attributes need to be accessible only
# by the samba-service dn.
access to attrs=sambaNTPassword,sambaLMPassword,sambaPwdLastSet
  by dn.exact="cn=samba-service,ou=users,dc=herzbube,dc=ch" write
  by self read
  by * none

# Everyone should have read-access to the directory base DN
# -> this allows users to navigate the directory tree to
#    ou=users and from there further on to their "self" entry
access to dn.exact="dc=herzbube,dc=ch"
  by * read

# Everyone should have read-access to the ou=users entry
# -> this allows users to navigate the directory tree to
#    their "self" entry
access to dn.exact="ou=users,dc=herzbube,dc=ch"
  by * read
# Access to user entries
# - read-only access by DNs that require access to user accounts
# - read+write access by the entry itself
# - no access to everybode else
access to dn.children="ou=users,dc=herzbube,dc=ch"
  by dn.exact="cn=readonly-users,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=samba-service,ou=users,dc=herzbube,dc=ch" read
  by self write
  by * none

# Access to group entries
# - read-only access by DNs that require access to group entries
# - no access to everybode else
access to dn.exact="ou=groups,dc=herzbube,dc=ch"
  by dn.exact="cn=readonly-users,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=samba-service,ou=users,dc=herzbube,dc=ch" read
  by * none
access to dn.children="ou=groups,dc=herzbube,dc=ch"
  by dn.exact="cn=readonly-users,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=samba-service,ou=users,dc=herzbube,dc=ch" read
  by * none

# Access to host entries
# - read-only access by the two libnss-ldap users because this is the
#   purpose of these entries
# - no access to everybode else
access to dn.exact="ou=hosts,dc=herzbube,dc=ch"
  by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read
  by * none
access to dn.children="ou=hosts,dc=herzbube,dc=ch"
  by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read
  by * none

# Access to samba entries
# - read-only access by samba-service because this is the purpose of
#   this entry
# - no access to everybode else
access to dn.exact="ou=samba,dc=herzbube,dc=ch"
  by dn.exact="cn=samba-service,ou=users,dc=herzbube,dc=ch" read
  by * none
access to dn.children="ou=samba,dc=herzbube,dc=ch"
  by dn.exact="cn=samba-service,ou=users,dc=herzbube,dc=ch" read
  by * none

# Access to DHCP entries
# - read-only access by dhcp-service because this is the purpose of
#   this entry
# - no access to everybode else
access to dn.exact="cn=dhcp,dc=herzbube,dc=ch"
  by dn.exact="cn=dhcp-service,ou=users,dc=herzbube,dc=ch" read
  by * none
access to dn.children="cn=dhcp,dc=herzbube,dc=ch"
  by dn.exact="cn=dhcp-service,ou=users,dc=herzbube,dc=ch" read
  by * none

# Access to addressbook entries
# - read-only access by readonly-addressbook because this is the purpose
#   of this entry
# - read+write access by selected users
# - no access to everybode else
access to dn.exact="ou=addressbook,dc=herzbube,dc=ch"
  by dn.exact="cn=readonly-addressbook,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=patrick,ou=users,dc=herzbube,dc=ch" write
  by dn.exact="cn=francesca,ou=users,dc=herzbube,dc=ch" write
  by * none
access to dn.children="ou=addressbook,dc=herzbube,dc=ch"
  by dn.exact="cn=readonly-addressbook,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=patrick,ou=users,dc=herzbube,dc=ch" write
  by dn.exact="cn=francesca,ou=users,dc=herzbube,dc=ch" write
  by * none

# Access to bookmark entries
# - read-only access by readonly-bookmarks because this is the purpose
#   of this entry
# - read+write access by selected users
# - no access to everybode else
access to dn.exact="ou=bookmarks,dc=herzbube,dc=ch"
  by dn.exact="cn=readonly-bookmarks,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=patrick,ou=users,dc=herzbube,dc=ch" write
  by dn.exact="cn=francesca,ou=users,dc=herzbube,dc=ch" write
  by * none
access to dn.children="ou=bookmarks,dc=herzbube,dc=ch"
  by dn.exact="cn=readonly-bookmarks,ou=users,dc=herzbube,dc=ch" read
  by dn.exact="cn=patrick,ou=users,dc=herzbube,dc=ch" write
  by dn.exact="cn=francesca,ou=users,dc=herzbube,dc=ch" write
  by * none

# Access to entries representing DHCP servers
# - read-only access by the dhcp-service dn
# - no access to everybode else
access to dn.onelevel="dc=herzbube,dc=ch"
          filter="(objectClass=dhcpServer)"
  by dn.exact="cn=dhcp-service,ou=users,dc=herzbube,dc=ch" read
  by * none

# Here OpenLDAP implicitly inserts the following ACL,
# so we don't need to explicitly specify it. This
# ACL denies general READ (!) access to everybody.
#   access to * by * none


Access rights in slapd-config

Note: Please read the previous section to understand the purpose of the ACLS in this section.

Compared to the previous section, this section omits the following ACLs because my requirements have changed:

  • Samba-specific rules removed
  • Host rules removed
  • DHCP rules removed
  • Addressbook rules removed
  • Bookmark rules removed


The "access" option's attribute name for slapd-config is olcAccess. Here are the original ACLs that exist in the default database that is created when the slapd Debian package is installed.

olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymous auth by * none
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by * read

Here is the LDIF to change the ACLs, generated by ldapvi:

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to * by dn.exact="cn=admin,ou=users,dc=herzbube,dc=ch" write by * none break
olcAccess: {1}to attrs=userPassword,shadowLastChange by self write by anonymous auth by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read by * none
olcAccess: {2}to dn.exact="dc=herzbube,dc=ch" by * read
olcAccess: {3}to dn.exact="ou=users,dc=herzbube,dc=ch" by * read
olcAccess: {4}to dn.children="ou=users,dc=herzbube,dc=ch" by dn.exact="cn=readonly-users,ou=users,dc=herzbube,dc=ch" read by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read by self write by * none
olcAccess: {5}to dn.exact="ou=groups,dc=herzbube,dc=ch" by dn.exact="cn=readonly-users,ou=users,dc=herzbube,dc=ch" read by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read by * none
olcAccess: {6}to dn.children="ou=groups,dc=herzbube,dc=ch" by dn.exact="cn=readonly-users,ou=users,dc=herzbube,dc=ch" read by dn.exact="cn=libnss-ldap,ou=users,dc=herzbube,dc=ch" read by dn.exact="cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch" read by * none


Configuration Part 3: Recipes for changing configuration options

Documentation

The main reference for slapd-config is this page:

http://www.openldap.org/doc/admin/slapdconf2.html

It contains the layout of the configuration LDAP scheme, as well as the name of each option.


Print current configuration to the console

Print all global options:

ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectclass=olcGlobal)"

Print a dump of the entire configuration (including all schemas):

ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=config"


For more information on how and why this particular ldapsearch command works, see the section Access to slapd-config data further up.


Change the value of a global configuration option

For a concrete example see the next recipe about changing the log level.

Global configuration options are attributes of the "cn=config" entry (which has the object class "olcGlobal", but that is not important here). Due to the way how LDIF works there are several scenarios to consider:

  • If the option is not yet present, the corresponding attribute needs to be added
  • If the option is already present, the corresponding attribute needs to be deleted first (specifying the current value), then re-added (specifying the new value). There is no way in LDIF to say "set an attribute to this value, regardless of what the old value was". The reason is that attributes allow multiple values.
  • If the attribute corresponding to the option allows multiple values (e.g. olcLogLevel), then you may need multiple delete/add operations

The consequence is: In order to write a correct LDIF script, you must first have a look at the current global options!


Change the log level

This recipe changes the log level to "none", i.e. log only high-priority messages. The LDIF only adds the "olcLogLevel" attribute because the attribute did not exist before.

root@pelargir:~# cat /tmp/change-loglevel.ldif
dn: cn=config
changetype: modify
add: olcLogLevel
olcLogLevel: none

root@pelargir:/tmp# ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/change-loglevel.ldif


This recipe changes the log level to "filter", i.e. show search filter processing. The LDIF first removes the existing "olcLogLevel" attribute, then re-adds it with the new value.

root@pelargir:~# cat /tmp/change-loglevel.ldif
dn: cn=config
changetype: modify
delete: olcLogLevel
olcLogLevel: none
-
add: olcLogLevel
olcLogLevel: filter

root@pelargir:/tmp# ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/change-loglevel.ldif


Create a completely new object class and attribute type

This recipe creates a completely new object class "drupalAccount" that uses an also completely new attribute type "drupalUid". The two things are added to my personal schema naef.schema.

Important: If you use this recipe in the future, adjust the position of the new attribute type ({31}) and the new object class ({3}).

root@pelargir:~# cat /tmp/add-drupal-account.ldif

dn: cn={4}naef,cn=schema,cn=config
changetype: modify
add: olcAttributeTypes
olcAttributeTypes: {31}( 1.3.6.1.4.1.18427.5.2 NAME 'drupalUid' SUP uid )
-
add: olcObjectClasses
olcObjectClasses: {3}( 1.3.6.1.4.1.18427.5.1 NAME 'drupalAccount' DESC 'Attrib
 utes that describe a Drupal account' SUP top AUXILIARY MUST drupalUid MAY dis
 playName )

root@pelargir:/tmp# ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/add-drupal-account.ldif


Create a new attribute type and add it to an existing object class

As in the previous recipe, this one creates a new attribute type "drupalMail", but here the object class "drupalAccount" that should receive the attribute type already exists. The object class is changed by 1) first deleting it; 2) then re-adding it using the new attribute type.

In order to find out what must be specified to the "delete" modification you can simply copy & paste the data that comes from an ldapsearch dump.

root@pelargir:~# cat /tmp/change-drupal-account.ldif

dn: cn={4}naef,cn=schema,cn=config
changetype: modify
add: olcAttributeTypes
olcAttributeTypes: {32}( 1.3.6.1.4.1.18427.5.3 NAME 'drupalMail' SUP mail )
-
delete: olcObjectClasses
olcObjectClasses: {3}( 1.3.6.1.4.1.18427.5.1 NAME 'drupalAccount' DESC 'Attrib
 utes that describe a Drupal account' SUP top AUXILIARY MUST drupalUid MAY dis
 playName )
-
add: olcObjectClasses
olcObjectClasses: {3}( 1.3.6.1.4.1.18427.5.1 NAME 'drupalAccount' DESC 'Attrib
 utes that describe a Drupal account' SUP top AUXILIARY MUST ( drupalUid $ dru
 palMail ) MAY displayName )

root@pelargir:/tmp# ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/change-drupal-account.ldif


Administration

Daemon

Starting/stopping the daemon

/etc/init.d/slapd start|stop


Testing whether the configuration looks OK:

slaptest -v


Changing basic things about how the daemon runs can be changed in

/etc/default/slapd

The most important settings on my machine are:

  • uid/gid that the daemon runs as = openldap/openldap (the default for Debian)
  • Services that the daemon provides = ldap://127.0.0.1:389/ ldaps:/// ldapi:///
    • ldap://127.0.0.1:389/ is TCP port 389, but only on the localhost interface. The only reason why this is enabled at the moment is because Apache's mod_authnz_ldap does not support connecting via Unix domain socket.
    • To listen on TCP port 389 on all interfaces would be ldap:///. I have disabled this because even with StartTLS there is the possibility that, through some mis-configuration of a client, I might attempt to bind before issuing the StartTLS command and so accidentally transmit the password in clear text.
    • ldaps:/// is LDAP-over-TLS, TCP port 636. I have enabled this so that I can administrate the OpenLDAP server remotely with Apache Directory Studio.
      • While it was still packaged in Debian I have been using phpLDAPadmin for administration. phpLDAPadmin was able to connect via ldapi:///, so I didn't need ldaps:/// then. Things changed when phpLDAPadmin was removed from the Debian package repository.
    • ldapi:/// is a Unix domain socket. This is required for administering the slapd-config database.


Note 1: With Unix domain sockets it is not possible to connect to the server via TLS, simply because the communication channel works differently.

Note 2: With TCP connections on 127.0.0.1 (or localhost), in theory TLS should also fail because the server FQDN specified for the connection does not match match the common name in the X.509 certificate (a wildcard *.herzbube.ch). In practice, however, it works. The reason is currently unknown.


Generate passwords

To generate an userPassword value suitable for use with ldapmodify or the rootpw configuration directive in the slapd configuration:

slappasswd [-h {SHA|SSHA|MD5|SMD5|CRYPT|CLEARTEXT}]

Note 1: The braces are necessary, and they need to be protected from interpretation from the shell.

Note 2: The generated password hash cannot be used for /etc/shadow


Command line usage

Search entries

ldapsearch -H ldapi:/// -D cn=patrick,ou=users,dc=herzbube,dc=ch -W -x -b "dc=herzbube,dc=ch" "(objectclass=*)"
ldapsearch -H ldap://localhost/ -D cn=patrick,ou=users,dc=herzbube,dc=ch -W -x -b "dc=herzbube,dc=ch" "(objectclass=*)"
ldapsearch -ZZ -H ldap://localhost/ -D cn=patrick,ou=users,dc=herzbube,dc=ch -W -x -b "dc=herzbube,dc=ch" "(objectclass=*)"
ldapsearch -H ldaps://ldap.herzbube.ch/ -D cn=patrick,ou=users,dc=herzbube,dc=ch -W -x -b "dc=herzbube,dc=ch" "(objectclass=*)"

Discussion:

  • -H = URI that specifies the LDAP server. Only protocol/host/port are allowed.
    • In the first example the ldapi protocol indicates that the connection should be made via Unix domain socket, which is why neither host name nor port are needed.
    • In the second example the connection is made via TCP/IP. The protocol ldap implies the default port 389. The communication is unencrypted, but this should not be a problem because the network traffic is not visible outside of the host.
    • The third example differs from the second example in that the initially unencrypted communication is upgraded to an encrypted communication using the StartTLS command. See the -ZZ option.
    • In the fourth example the connection is also made via TCP/IP. The protocol ldaps implies the default port 636 for TLS. The communication is encrypted from the start.
  • -D = DN of the user that should be used for authentication
  • -x = simple authentication instead of SASL
  • -W = ask for password (instead of specifying it on the command line), only for simple authentication
  • -b = base DN for the search
  • cn = space separated list of attributes that should be read
  • -ZZ = This option can be used only with the standard protocol ldap. This option causes the client to issue the StartTLS command, which upgrades the initially unencrypted communication to an encrypted communication.


Delete entries

no TLS:
ldapdelete -H ldapi:///                 -r -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f list-of-dns.ldif
with TLS:
ldapdelete -H ldaps://ldap.herzbube.ch/ -r -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f list-of-dns.ldif

Discussion:

  • -r = delete recursive
  • entries in the file must be DNs, but without the the "dn:" prefix usually required by other tools (e.g. ldapadd, ldapmodify); example
cn=last first,ou=addressbook,dc=herzbube,dc=ch


Add entries

no TLS:
ldapmodify ldapi:///                  -a -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f changes.ldif -H ldap://localhost/
ldapadd    ldapi:///                     -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f changes.ldif -H ldap://localhost/

with TLS:
ldapmodify ldaps://ldap.herzbube.ch/  -a -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f changes.ldif
ldapadd    ldaps://ldap.herzbube.ch/     -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f changes.ldif

Discussion:

  • -a = add new entries; this flag is turned on automatically if ldapadd is used (which in reality is a hardlink to ldapmodify)
  • -n = show what would be done, but don't actually modify entries; useful for debugging in conjunction with -v


It is possible to specify "changetype: add" within the LDIF file instead of using the command line (i.e. -a option) to say that we want to add entries. Example LDIF file:

pelargir:~# cat hosts.ldif 
dn: cn=pelargir,ou=hosts,dc=herzbube,dc=ch
changetype: add
cn: pelargir
cn: localhost
ipHostNumber: 127.0.0.1
objectClass: device
objectClass: ipHost

Corresponding usage of ldapmodify:

pelargir:~# ldapmodify ldapi:/// -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f hosts.ldif


Change entries

no TLS:
ldapmodify ldapi:///                 -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f changes.ldif
with TLS:
ldapmodify ldaps://ldap.herzbube.ch/ -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f changes.ldif

Discussion:

  • none


The syntax of the LDIF file is too complicated to discuss here; my reference is in the O'Reilly book "LDAP System Administration". An example copied verbatim from the book:

## Add entry for Peabody Soup
dn: cn=Peadbody Soup,ou=people,dc=plainjoe,dc=org
changetype: add
cn: Peabody Soup
sn: Soup
objectClass: inetOrgPerson

## Add new telephoneNumber for Jerry Carter
dn: cn=Jerry Carter,ou=people,dc=plainjoe,dc=org
changetype: modify
delete: telephoneNumber
telephoneNumber: 555-123-1234
-
add: telephoneNumber
telephoneNumber: 234-555-6789

## Remove entry Peabody Soup
dn: cn=Peadbody Soup,ou=people,dc=plainjoe,dc=org
changetype: delete

## Rename entry
dn: cn=Jerry Carter,ou=people,dc=plainjoe,dc=org
changetype: modrdn
newrdn: cn=Gerry Carter
deleteoldrdn: 1


Note 1: Only leaf nodes should be renamed with changetype modrdn. If a node has child nodes, using modrdn just like that will orphan the child nodes because the DN of their parent has changed.

Note 2: There is another changetype named moddn. There is currently no example for this.


A small additional example how to replace the value of an attribute that already exists. This certainly works for attributes with single values, but I haven't tested what happens if the attribute has multiple values.

dn: cn=Jerry Carter,ou=people,dc=plainjoe,dc=org
changetype: modify
replace: telephoneNumber
telephoneNumber: 234-555-6789


Test access to entries

slapacl -D "cn=dhcp-service,ou=users,dc=herzbube,dc=ch" -b "cn=pelargir.localnet.herzbube.ch,dc=herzbube,dc=ch" "dhcpServiceDN"
slapacl -D "cn=dhcp-service,ou=users,dc=herzbube,dc=ch" -b "cn=pelargir.localnet.herzbube.ch,dc=herzbube,dc=ch" "dhcpServiceDN/write"

Discussion:

-D 
Specifies the DN that should be assumed for authenticating
-b 
Specifies the DN of the entry that should be tested
dhcpServiceDN 
Specifies the attribute that should be tested
dhcpServiceDN/write 
Specifies whether write access is possible


Importing data

Address Book.app -> vcard -> LDIF -> LDAP directory

Export from the Mac OS X "Address Book" application:

  • select all entries (or select the group "all")
  • select menu entry File->Export vCards

The resulting .vcf file

  • possibly contains data where every (!) character is delimited by a null byte
    • to fix this, open the .vcf file in vi, search for the null byte

(Ctrl+V 0 <space>) and replace it by an empty string

  • has the CR+LF line ending (instead of just LF)
    • to fix this, open the .vcf file in vi, search for the CR byte

(Ctrl+V <enter key>) and replace it by an empty string

  • in theory has the vCard encoding that is configured in Preferences.app
    • in practice, the encoding is always LATIN1
    • the encoding can be changed by saying set fileencoding=utf-8;
  when the file is saved the next time, it will be written to disk
  using the encoding 

Now that the .vcf file has been prepared, you can process it, for instance, with an awk script. The output of this step should be an LDIF file.

The import of the LDIF file into the directory is done by the following command:

no TLS: ldapadd -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -f alle.ldif -H ldap://localhost/
with TLS: ldapadd -D cn=admin,ou=users,dc=herzbube,dc=ch -W -x -ZZ -f alle.ldif -h ldap.herzbube.ch


Exporting data

LDAP directory -> LDIF -> CSV

Export from the LDAP directory:

ldapsearch -H ldapi:/// -D cn=patrick,ou=users,dc=herzbube,dc=ch -W -x -b "ou=addressbook,dc=herzbube,dc=ch" "(objectclass=*)" >abook.ldif

An alternative is to use slapcat; its main advantage is that it is faster than ldapsearch, but on the other hands the drawbacks are that for some database backend types the slapd daemon needs to be stopped (see man slapcat for details), and that the output contains many fields that are not actually user data.

The resulting file abook.ldif can now be processed by any means (e.g. an awk or a perl script) and converted into CSV format.

There is one important thing to note: the LDIF format encodes non-ASCII values as base64. This affects, for instance, the common German umlaut characters 'ä', 'ö' and 'ü'. Fields that use base64 to encode their values can be identified by a double-colon. For instance:

cn:: TsOkZi1CcmF1biBMeWRpYQ==

To conveniently decode base64 values in perl, use the MIME::Base64 module:

use MIME::Base64;
$encoded = encode_base64('Aladdin:open sesame');
$decoded = decode_base64($encoded);


Clients

The clients in the following table use the directory as a data source:

Client Data Purpose Access type DN used for binding Links Remarks
PAM ou=users,dc=herzbube,dc=ch find user DN read-only cn=readonly-users,ou=users,dc=herzbube,dc=ch PAM
authenticate and read attributes user DN
password change read+write user DN
NSS ou=users,dc=herzbube,dc=ch
ou=groups,dc=herzbube,dc=ch
ou=hosts,dc=herzbube,dc=ch
read attributes read-only cn=libnss-ldap,ou=users,dc=herzbube,dc=ch NSS
cn=libnss-ldap-root,ou=users,dc=herzbube,dc=ch
Bugzilla ou=users,dc=herzbube,dc=ch  find user DN read-only cn=readonly-users,ou=users,dc=herzbube,dc=ch Bugzilla
authenticate and read attributes user DN
SquirrelMail ou=addressbook,dc=herzbube,dc=ch  read attributes read-only cn=readonly-addressbook,ou=users,dc=herzbube,dc=ch SquirrelMail
Samba ou=users,dc=herzbube,dc=ch read attributes read-only cn=samba-service,ou=users,dc=herzbube,dc=ch Samba
ou=users,dc=herzbube,dc=ch password change read+write
ou=groups,dc=herzbube,dc=ch read attributes read-only
ou=samba,dc=herzbube,dc=ch
Apache ou=users,dc=herzbube,dc=ch find user DN read-only cn=readonly-users,ou=users,dc=herzbube,dc=ch Apache
authenticate user DN
ou=groups,dc=herzbube,dc=ch check group membership cn=readonly-users,ou=users,dc=herzbube,dc=ch
DHCP Server entries directly below dc=herzbube,dc=ch
with (objectClass=dhcpServer)
find entry that matches the server's hostname,
then follow the entry's pointer to the subtree
that contains the server's DHCP configuration
read-only cn=dhcp-service,ou=users,dc=herzbube,dc=ch ISCDHCP
cn=dhcp,dc=herzbube,dc=ch read server's DHCP configuration and look up hosts
DAViCal ou=users,dc=herzbube,dc=ch find user DN read-only cn=readonly-users,ou=users,dc=herzbube,dc=ch DAViCal
authenticate and read attributes user DN
Mediawiki ou=users,dc=herzbube,dc=ch find user DN read-only cn=readonly-users,ou=users,dc=herzbube,dc=ch Mediawiki
authenticate user DN
ou=groups,dc=herzbube,dc=ch check group membership cn=readonly-users,ou=users,dc=herzbube,dc=ch
phpLDAPadmin ou=users,dc=herzbube,dc=ch find user DN read-only cn=readonly-users,ou=users,dc=herzbube,dc=ch phpLDAPadmin
authenticate user DN
Everything, everywhere Browse and modify the entire directory read+write user DN
Drupal ou=users,dc=herzbube,dc=ch find user DN read-only cn=readonly-users,ou=users,dc=herzbube,dc=ch Drupal
authenticate and read attributes user DN


Applications

ldapvi

The ldapvi utility is a fantastically nifty little tool that lets you browse and modify an LDAP directory within your favourite text editor (vi in my case, but can be something else, see "man ldapvi").


The basic workflow is this:

  • You execute an ldapvi command
  • ldapvi performs an LDAP search and presents the result in your favourite text editor
  • You browse and/or make modifications
    • Modifications are not in standard LDIF format, i.e. you don't need to specify a "changetype" or say on special lines which attributes you want to add, delete or replace - you simply edit the content into shape
  • When you save and exit the text editor, ldapvi compares the new data with the original search results and generates the appropriate LDIF file for you
  • You review the LDIF and either accept, modify or discard the proposed changes
  • If you accept, ldapvi submits the LDIF to the server


Edit the slapd-config configuration (only usable as root, search this page for the discussion about the SASL method "EXTERNAL"):

ldapvi --host ldapi:/// --sasl-mech EXTERNAL --base "cn=config"

Edit the herzbube.ch LDAP directory (specifying a bind DN that has write access; you could also use -Y EXTERNAL):

ldapvi --host ldapi:/// -D cn=admin,dc=herzbube,dc=ch --base "dc=herzbube,dc=ch"


phpLDAPadmin

There is quite a bit of software out there that allows you to nicely administrate your address book contacts in an LDAP directory. Unfortunately, 99% of all those tools are geared towards working with the InetOrgPerson object class, which is of no use to me since I have defined my own schema.

phpLDAPadmin belongs to the remaining 1%. This application is the best and most flexible tool I encountered that is also able to administrate my address book entries without too much trouble. It also has support for templates - if one day I manage to create such a template for my addressbookEntry schema, nothing will stand in the way of adding comfort to capability.

Update: I no longer use phpLDAPadmin because it is no longer packaged with Debian. Instead I have switched to Apache Directory Studio.


Installation

In the beginning I had my own installation in /var/www/phpldapadmin, but at some point, Debian started to have a regular phpldapadmin package, so this is what I use nowadays.

The main configuration file is stored in

/etc/phpldapadmin/config.php

Most of the following sections discuss modifications to that file.


Apache configuration

phpLDAPadmin brings its own Apache configuration file that defines everything necessary to run phpLDAPadmin either under an alias path /phpldapadmin, or under its own virtual host.

I have assigned an Apache vhost to phpLDAPadmin that is accessible under http://ldap.herzbube.ch/. These are the configuration details:

# --------------------------------------------------------------------------------
# ldap.herzbube.ch
# --------------------------------------------------------------------------------
<VirtualHost *:80>
  ServerName ldap.herzbube.ch
  Redirect permanent "/" "https://ldap.herzbube.ch/"
</VirtualHost>

# --------------------------------------------------------------------------------
# SSL Host
# --------------------------------------------------------------------------------
<VirtualHost *:443>
  ServerName ldap.herzbube.ch
  ServerAdmin webmaster@herzbube.ch
  ErrorLog ${APACHE_LOG_DIR}/ldap.herzbube.ch/error.log
  CustomLog ${APACHE_LOG_DIR}/ldap.herzbube.ch/access.log combined

  DocumentRoot /usr/share/phpldapadmin/htdocs
  Alias /robots.txt /var/www/ldap.herzbube.ch/robots.txt

  <Directory /usr/share/phpldapadmin/>
    php_admin_flag engine on
  </Directory>

  Include conf-available/pelargir-herzbube.ch-vhosts-ssl.conf
</VirtualHost>


Connecting to the directory

Documentation for server definitions can be found here. My configuration looks like this:

$servers->newServer('ldap_pla');
$servers->setValue('server','name','Local, pelargir');
$servers->setValue('server','host','ldapi:///');
$servers->setValue('server','base',array('dc=herzbube,dc=ch'));
$servers->setValue('login','auth_type','session');
$servers->setValue('login','bind_id','cn=readonly-users,ou=users,dc=herzbube,dc=ch');
$servers->setValue('login','bind_pass','secret');
$servers->setValue('server','tls',false);
$servers->setValue('login','attr','cn');
$servers->setValue('login','base',array('ou=users,dc=herzbube,dc=ch'));

Notes:

  • The host specification "ldapi:///" makes sure that the connection is made via UNIX domain socket. Because of this there is no need to specify a port, and TLS can also be safely disabled. An alternative would be to specify "127.0.0.1" as host, but then port 389 would also be required. Even in that scenario it wouldn't make much sense to enable TLS.
  • We want to enter a simple user name for login, not a full DN
  • This means that we must tell phpLDAPadmin the rules how it must translate the simple user name into a full DN
  • The first step is to set "login, auth_type" to "session": This tells phpLDAPadmin that authentication information must be entered by the user (instead of taking a hardcoded login DN directly from config.php). At the same time, this tells phpLDAPadmin to store the authentication information in a PHP session variable on the server (not in a browser cookie in the client!)
    • phpLDAPadmin will encrypt the session variable, currently using the Blowfish algorithm
    • In earlier configurations I used to define a simple Blowfish passphrase in config.php, and I was careless enough to write the passphrase into this wiki
    • I no longer do this. When leaving the Blowfish passphrase blank, phpLDAPadmin will use the session ID as the passphrase, which is good enough for me.
  • The next step is to set "login, attr" to the attribute that stores the user name. In my case this is the attribute "cn".
  • Finally we have to set "login, bind_id" and "login, bind_pass" in order to tell phpLDAPadmin how it should bind to the directory to lookup the login DN
  • I also think it makes sense to set "login, base" to restrict the search for login DNs to a given subtree. In my case this is really important because I use the attribute "cn", which is used everywhere in the directory
    • An alternative for restricting the search is to set "login, class"
  • Important final note: The file contains a password, therefore it must be protected so that its permissions look like this
 root@pelargir:~# ls -l /etc/phpldapadmin/config.php 
 -rw-r----- 1 root www-data 24363 Jun 12 17:12 /etc/phpldapadmin/config.php


Template related settings

phpLDAPadmin provides a number of templates that are meant to be examples that can be used to create custom templates. The presence of these example templates has a couple of annoying consequences.


When the first entry is selected after a login, there are a number of warning messages ("Automatically removed objectClass from template", "Automatically removed attribute from template"). These warnings can be disabled with this setting:

$config->custom->appearance['hide_template_warning'] = true;


The other problem is that, whenever an entry is selected, phpLDAPadmin prompts the user to select the template that should be used for editing. The user's choice is remembered, but only during the current session - when the user logs out, all selections are discarded. The only way how to fix this is to disable these example templates, by using the following setting:

$config->custom->appearance['custom_templates_only'] = true;


Note: The custom_templates_only setting also fixes the "warning messages" issue because the example templates are completely disabled.


Other settings

  • General settings:
$config->custom->appearance['timezone'] = 'Europe/Zurich';
  • Command settings: Allow all commands, i.e. remove the code comments /* */ around the variables that define the available commands.


Predefined Searches

Predefined searches appear to have been removed in PLA 1.2. The PLA wiki config page currently hints at some search-related settings (see bottom of page), but at the moment these are undocumented and I have not spent any time fiddling around with them.

If predefined (or custom) searches become available again in the future, the following searches would be useful:

  • List of users
  • List of groups
  • List of hosts
  • R7b list
  • Contacts marked by me in the description attribute


Templates

phpLDAPadmin supports two kinds of templates for editing LDAP entries: one type is for creating new entries, the other is for modifying existing entries. On a Debian system, templates are stored in

root@pelargir:/etc/phpldapadmin/templates# l
total 20
drwxr-x--- 4 root www-data 4096 Jun  9 23:04 .
drwxr-xr-x 3 root root     4096 Jun 12 17:12 ..
drwxr-x--- 2 root www-data 4096 Jun  9 23:04 creation
drwxr-x--- 2 root www-data 4096 Jun  9 23:04 modification
-rw-r----- 1 root www-data 2089 Oct 19  2010 template.dtd

The following resources are useful for understanding phpLDAPadmin templates:


The following steps are necessary to define a template for my custom objectClass "addressBookEntry":

  • TODO


Apache Directory Studio

http://directory.apache.org/studio/


Other

  • gq: an X11 client
  • ldapnavigator: a PHP application hosted on SourceForge that is quite useful but whose development seems to have stopped in 2002


Upgrades

Change backend from ldbm to bdb

Useful information can be found in

/usr/share/doc/slapd/README.Debian


First the daemon needs to be stopped

/etc/init.d/slapd stop

Then a backup of the database is in order

slapcat -n 1 >backup.ldif

Now change the configuration in the slapd configuration

  • the backend module needs to be loaded
moduleload back_bdb
  • the database must be configured with the new backend
database bdb

Now either move the original database files in the file system to a safe place, or change the database location inside the slapd configuration; in both cases it is important that the database directory in the file system exists and is empty (the next step will create its contents)

Now add the backup data to the new database (first make a test by specifying -u, then do the real thing)

slapadd -n 1 -u <backup.ldif
slapadd -n 1 <backup.ldif

Modify permissions (somewhere in between 2.2.26-4 and 2.3.27-1 the system user/group openldap has been introduced)

chown -R openldap:openldap /var/lib/ldap/herzbube.ch

Now restart the daemon

/etc/init.d/slapd start

Et voilà.


From slapd-2.2.26-4 to slapd-2.3.27-1

Somewhere in between the two versions named in the chapter title, the Debian package manager introduced a new system user/group named openldap. After I upgraded the package, the new slapd daemon ran as openldap, but the database files in /var/lib/ldap/herzbube.ch were still owned by root.

The following permission issues need to be fixed to make the upgrade complete:

chown -R openldap:openldap /var/lib/ldap
chown -R openldap:openldap /var/lib/slapd
chown -R openldap:openldap /var/run/slapd
chgrp openldap /etc/ssl/private/ldap.herzbube.ch.key*
chmod 440 /etc/ssl/private/ldap.herzbube.ch.key*
adduser openldap ssl-cert

(the last command is required so that the openldap user has the permission to get into the directory /etc/ssl/private)


As an afterthought, I decided to use the opportunity to clean-up my OpenLDAP installation a bit:

  • make sure that I have a backup of my LDAP directory and of everything in /etc/ldap
  • purge the slapd package
  • remove (rm -r) the directories /etc/ldap and /var/lib/ldap/herzbube.ch
  • re-install the slapd package
  • stop the daemon
  • remove (rm -r) the directory /var/lib/ldap that was created during package installation
  • update the pristine slapd.conf with the modifications necessary to run my directory
  • create the directory /var/lib/ldap/herzbube.ch
  • use slapadd to restore the last backup of the herzbube.ch LDAP directory
  • re-create indices with slapindex -n 1
  • chown -R openldap:openldap /var/lib/ldap/herzbube.ch
  • restart server


From slapd-2.3.27-1 to slapd-2.4.10-3

The following messag box displayed by dpkg seems to indicate that the new OpenLDAP package is now built with GnuTLS instead of OpenSSL:

A "TLSCipherSuite" option was found in your slapd config when upgrading. The values allowed for this option are determined by the SSL implementation used, which has been
changed from OpenSSL to GnuTLS.  As a result, your existing TLSCipherSuite setting will not work with this package.

This setting has been automatically commented out for you.  If you have specific encryption needs that require this option to be re-enabled, see the output of
'gnutls-cli -l' in the gnutls-bin package for the list of ciphers supported by GnuTLS.

In order to get the gnutls-cli utility, I first install the package gnutls-bin. I then have a look at the OpenSSL ciphers that the original value for TLSCipherSuite (HIGH:MEDIUM) selected:

osgiliath:~# openssl ciphers -v HIGH:MEDIUM
ADH-AES256-SHA          SSLv3 Kx=DH       Au=None Enc=AES(256)  Mac=SHA1
DHE-RSA-AES256-SHA      SSLv3 Kx=DH       Au=RSA  Enc=AES(256)  Mac=SHA1
DHE-DSS-AES256-SHA      SSLv3 Kx=DH       Au=DSS  Enc=AES(256)  Mac=SHA1
AES256-SHA              SSLv3 Kx=RSA      Au=RSA  Enc=AES(256)  Mac=SHA1
ADH-AES128-SHA          SSLv3 Kx=DH       Au=None Enc=AES(128)  Mac=SHA1
DHE-RSA-AES128-SHA      SSLv3 Kx=DH       Au=RSA  Enc=AES(128)  Mac=SHA1
DHE-DSS-AES128-SHA      SSLv3 Kx=DH       Au=DSS  Enc=AES(128)  Mac=SHA1
AES128-SHA              SSLv3 Kx=RSA      Au=RSA  Enc=AES(128)  Mac=SHA1
ADH-DES-CBC3-SHA        SSLv3 Kx=DH       Au=None Enc=3DES(168) Mac=SHA1
EDH-RSA-DES-CBC3-SHA    SSLv3 Kx=DH       Au=RSA  Enc=3DES(168) Mac=SHA1
EDH-DSS-DES-CBC3-SHA    SSLv3 Kx=DH       Au=DSS  Enc=3DES(168) Mac=SHA1
DES-CBC3-SHA            SSLv3 Kx=RSA      Au=RSA  Enc=3DES(168) Mac=SHA1
DES-CBC3-MD5            SSLv2 Kx=RSA      Au=RSA  Enc=3DES(168) Mac=MD5 
ADH-RC4-MD5             SSLv3 Kx=DH       Au=None Enc=RC4(128)  Mac=MD5 
RC4-SHA                 SSLv3 Kx=RSA      Au=RSA  Enc=RC4(128)  Mac=SHA1
RC4-MD5                 SSLv3 Kx=RSA      Au=RSA  Enc=RC4(128)  Mac=MD5 
RC2-CBC-MD5             SSLv2 Kx=RSA      Au=RSA  Enc=RC2(128)  Mac=MD5 
RC4-MD5                 SSLv2 Kx=RSA      Au=RSA  Enc=RC4(128)  Mac=MD5 

Checking against the ciphers listed by gnutls-cli -l, which do not match the OpenSSL ciphers at all, I am somewhat overwhelmed. The following Debian bug documents how the current upgrade behaviour (TLSCipherSuite getting commented out) came into being, what the original problems were, and generally provides nice information about TLS (in the context of OpenLDAP):

http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=462588

The bug report convinces me that I don't need a specific TLSCipherSuite setting because 1) I have no real clue about ciphers so I don't know what to choose, and 2) the bug report says (and I believe the statement) that "The most common use of this directive is to restrict use of weak ciphers, which GnuTLS doesn't support in the first place."

I make a final check whether I can make an LDAP request using TLS, and it works, so the issue is closed for me. Details how I made the check:

  • using ldapsearch -ZZ (see further up in this document for a detailed example)
  • the connection must be made via ldap.herzbube.ch because that is the DN of the server certificate
  • on osgiliath it is currently not possible to connect to ldap.herzbube.ch because the current DNAT rule seems to be insufficient to properly re-reoute packets to herzbube.ch if they come from osgiliath
  • I make the attempt from tharbad; here I first get the following error
ldap_start_tls: Connect error (-11)
	additional info: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
  • I then transmit the CA certificate chain file cacert.org.certchain to tharbad and edit the LDAP client configuration file /etc/openldap/ldap.conf so that it contains the option TLS_CACERT pointing to the CA certificate chain file
  • it works!


From slapd-2.4.10-3 to slapd-2.4.17-1

The upgrade procedure seems to work like this:

  1. Back up directory
  2. Install new stuff
  3. Create new directory
  4. Import from backup

This time, step 4 didn't work. The output looked like this:

  Loading from /var/backups/slapd-2.4.10-3: 
  - directory dc=herzbube,dc=ch... failed.

Loading the database from the LDIF dump failed with the following
error while running slapadd:
    slap_sasl_init: SASL library version mismatch: expected 2.1.23, got 2.1.22
    slapadd: slap_init failed!

Luckily, the fix this time was easy: Simply upgrade libsasl2-2 to 2.1.23. The broken slapd upgrade was retried automatically after libsasl2-2 had been upgraded, and this time everything worked. Phew!


From slapd-2.4.17-1 to 2.4.25-1+b1

This upgrade is not documented. It probably occurred while switching from osgiliath to pelargir.

Note: The upgrade to slapd-2.4.25-1+b1 automatically converted the single-file configuration into a directory-based configuration.


From slapd-2.4.25-1+b1 to slapd-2.4.25-3

The first attempt at upgrading completely broke my installation! Apparently a set of SASL packages with undocumented ABI changes had made it into testing, and I had unwittingly upgraded those. The effect was that it was no longer possible to run slapd or any of the LDAP command line tools (e.g. slapcat). The error printed when trying to run one of those programs:

slap_sasl_init: auxprop add plugin failed
slapadd: slap_init failed!

Downgrading SASL packages to a reportedly good version (2.1.23.dfsg1-7) did not help, nor was it possible to remove them completely because OpenLDAP depends on SASL being present on the system.

Debian bug 628237 had more information on the problem. The only solution I finally found was to manually download and install the latest OpenLDAP packages (version 2.4.25-4) from Debian unstable (!). Interestingly, slapd could not even be properly uninstalled by the regular package management system, so I had to manually uninstall it as follows before I was able to install the new version:

rm /var/lib/dpkg/info/slapd.*
dpkg --remove --force-remove-reinstreq slapd


From slapd-2.4.25-3 to slapd-2.4.40+dfsg-1+deb8u2

This is just a placeholder section to document the version change. No real upgrade has taken place:

  • slapd-2.4.25-3 was the version that was last active when pelargir was the MacMini, before that machine tragically expired
  • slapd-2.4.40+dfsg-1+deb8u2 is the version that was active when I put the new pelargir into service in its incarnation as Dedicated Server