Why LDAP
Lightweight Directory Access Protocol (RFC 4511) is the standard for access to a hierarchical directory: users, groups, hosts, certificates, application configs. It appeared in the 1990s as a simplification of X.500. Today it is used for:
- Centralized auth: Active Directory (Microsoft), FreeIPA (Red Hat), OpenLDAP / 389-DS (linux-only)
- Address book for email servers (
ou=Peoplefor autocomplete in Outlook/Thunderbird) - Application configs and DNS data in a single tree
- Storage of user SSH keys (with patches/openssh-ldap)
Not a general-purpose database. Reads are fast, writes are rare. It is optimized for "many reads, few writes".
Hierarchical structure
A tree of objects (DIT, Directory Information Tree):
dc=example,dc=com ← root = domain
├─ ou=People
│ ├─ uid=alice (objectClass: posixAccount, inetOrgPerson)
│ ├─ uid=bob
│ └─ uid=carol
├─ ou=Groups
│ ├─ cn=admins
│ └─ cn=developers
└─ ou=Services
└─ cn=ldap-replica
- DN (Distinguished Name), the full "coordinate":
uid=alice,ou=People,dc=example,dc=com - RDN (Relative DN), the last component:
uid=alice - dc (domainComponent), ou (organizationalUnit), uid, cn (commonName), the attributes that form the DN
Read right to left: dc=com → dc=example → ou=People → uid=alice.
Bind, authentication
Before issuing queries the client performs a bind:
- Anonymous bind, no credentials, access only to public data
- Simple bind, DN + password in plaintext (requires TLS!)
- SASL bind, via Kerberos/EXTERNAL/DIGEST-MD5/PLAIN
# Anonymous (for tests)
ldapsearch -x -H ldap://ldap.example.com -b "dc=example,dc=com" "(uid=alice)"
# Simple bind
ldapsearch -x -D "uid=alice,ou=People,dc=example,dc=com" -W \
-H ldaps://ldap.example.com -b "dc=example,dc=com" "(objectClass=*)"
# SASL Kerberos (when a TGT is present)
ldapsearch -Y GSSAPI -H ldap://ldap.example.com -b "dc=example,dc=com" "(uid=alice)"
-x, simple bind-D, bind DN-W, prompt for the password interactively-H, URL (ldap://,ldaps://)-Y, SASL mechanism
Objects and attributes
Every object has one or more objectClass values (defined by the
schema), which set the required and optional attributes:
dn: uid=alice,ou=People,dc=example,dc=com
objectClass: top
objectClass: posixAccount # uidNumber, gidNumber, homeDirectory
objectClass: inetOrgPerson # mail, sn, givenName, telephoneNumber
objectClass: shadowAccount # shadowLastChange and the like
uid: alice
cn: Alice Wonderland
sn: Wonderland
givenName: Alice
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/alice
loginShell: /bin/bash
mail: alice@example.com
userPassword: {SSHA}xxxx # hashedThe main classes for a user on Linux:
posixAccount, uid/gid, so that NSS+PAM can use the accountinetOrgPerson, mail, nameshadowAccount, password expiry
LDAP filters
Search uses an LDAP Filter (RFC 4515):
(uid=alice) # exact match
(cn=Alice*) # wildcard
(mail=*@example.com) # suffix
(&(uid=alice)(memberOf=cn=admins,ou=Groups,dc=example,dc=com)) # AND
(|(uid=alice)(uid=bob)) # OR
(!(loginShell=/bin/false)) # NOT
(uidNumber>=1000) # >= (int only)
(objectClass=posixAccount)
Every special character in a value must be escaped (\28 for ().
ldapsearch, the core utility
# All posixAccount entries with uidNumber >= 1000
ldapsearch -x -H ldaps://ldap -b "dc=example,dc=com" \
"(&(objectClass=posixAccount)(uidNumber>=1000))" uid uidNumber cn
# Only specific attributes
ldapsearch ... "(uid=alice)" mail telephoneNumber
# EVERYTHING present on the object
ldapsearch ... "(uid=alice)" "+" # operational attributes
# Size of the tree
ldapsearch -x -H ldap://srv -b "" -s base "(objectclass=*)" # rootDSE
LDIF, the export/import format
LDIF (LDAP Data Interchange Format) is a text format:
dn: ou=Test,dc=example,dc=com
changetype: add
objectClass: organizationalUnit
ou: Test
dn: uid=alice,ou=Test,dc=example,dc=com
changetype: add
objectClass: posixAccount
objectClass: inetOrgPerson
uid: alice
cn: Alice
sn: Wonderland
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/alice
ldapadd -D "cn=admin,dc=example,dc=com" -W -f users.ldif
ldapmodify -f changes.ldif
ldapdelete "uid=alice,ou=People,dc=example,dc=com"
OpenLDAP vs 389-DS vs FreeIPA vs AD
| Trait | OpenLDAP (slapd) | 389-DS | FreeIPA | Active Directory |
|---|---|---|---|---|
| Default on | Debian/Ubuntu | RHEL | RHEL/Fedora | Windows Server |
| Config | cn=config (LDIF) | dse.ldif + console | web UI + CLI | Windows GUI |
| Replication | mirror/N-way multimaster | multi-supplier | suppliers + RO replicas | DC + RODC |
| Kerberos | separate (MIT) | possible | built in + DNS + Kerberos + CA | built in |
| Setup difficulty | medium | medium | low (all-in-one) | low (on Windows) |
| Linux clients | sssd / nss-ldap | sssd | sssd (native integration) | sssd via realmd |
For a new self-hosted deployment on Linux, use FreeIPA (if RHEL/Fedora) or 389-DS standalone. OpenLDAP is legacy but still widespread.
TLS, mandatory
Without TLS, simple-bind passwords travel in plaintext and tcpdump
will grab them. The options:
- ldaps:// on 636, implicit TLS
- STARTTLS on 389, opportunistic, though the client may not request it
A modern server should accept only ldaps:// or forbid
simple-bind without STARTTLS:
# OpenLDAP
olcSecurity: ssf=128
When something goes wrong
Invalid credentials (49), wrong DN or password. It also happens when the entry has no userPassword at all.Insufficient access rights (50), bind is fine, but an ACL rule forbids the read or write. CheckolcAccess.Server is unwilling to perform (53), the operation is forbidden by config (for example a write on a read-only replica).Constraint violation (19), the password failed the pwd-policy (length, history).Object class violation (65), you add an attribute that is not in the objectClass values, or you delete a required one.- Painfully slow search, no index on the attribute.
Run
slapindexafterolcDbIndex: cn,uid eq. - TLS handshake fails, old ciphers on one side or a self-signed CA missing from the client trust store.
Alternatives and related
- sssd, the Linux client, caches LDAP/AD/Kerberos for NSS+PAM
- nslcd / nss-ldap, the old client, deprecated in favor of sssd
- Apache Directory Studio, a GUI for browsing and editing the tree
- OpenID Connect / OAuth2, modern user-facing SSO (applications move away from LDAP authentication toward OIDC, but LDAP remains as the backing user store)