OpenLDAP
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
- http://www.openldap.org/doc/admin/slapdconf2.html
- http://wiki.debian.org/LDAP/OpenLDAPSetup
man slapd-config
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
- See the "Using SASL" section in the OpenLDAP admin guide for this information (link for OpenLDAP 2.4)
- 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
- 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.
- The client is not allowed to specify its own bind DN, as we have seen SASL EXTERNAL over
- The client process cannot spoof its uid/gid
- We must assume that this is true, otherwise we have a fundamental problem with the system
- 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 pointslaptest
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
becomesolcLogLevel
password-hash
becomesolcPasswordHash
"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}
- This option is described adequately in
man slapd.conf
, but NOT in the OpenLDAP Administrator's Guide.
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 vialdapi:///
, so I didn't needldaps:///
then. Things changed whenphpLDAPadmin
was removed from the Debian package repository.
- While it was still packaged in Debian I have been using
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.
- In the first example the
- -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 theStartTLS
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
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:
- http://phpldapadmin.sourceforge.net/wiki/index.php/Templates
- http://phpldapadmin.sourceforge.net/wiki/index.php/FAQ#Template_FAQs
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:
- Back up directory
- Install new stuff
- Create new directory
- 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