OpenLDAP & 389DS
- Pick the server based on operational model, not folklore benchmarks. The day-two experience matters more than raw LDAP ops/sec.
- Design the DIT and schema before onboarding apps. LDAP consumers are sticky, and bad DNs live forever.
- Require LDAPS or StartTLS for simple binds. A readable directory is fine; plaintext passwords are not.
- Index only the attributes your clients really filter on:
uid,mail,member,memberOf, and whatever your apps search most. - Replication only works cleanly when schema, indexes, and naming are consistent on every node.
- Give service accounts least privilege. If an app can bind as
cn=Directory Manager, the design is already wrong.
OpenLDAP vs 389DS
Both are capable LDAP directories. The better choice depends on how much native tooling you want and how close you want to stay to a pure LDAP-native configuration model.
| Area | OpenLDAP | 389 Directory Server |
|---|---|---|
| Config model | cn=config LDIF, very LDAP-native | dsconf, web console, and LDAP config entries |
| Replication | syncrepl, provider/consumer, mirror mode | Built-in multi-supplier tooling and monitoring |
| Access control | olcAccess rules | ACIs stored in the directory |
| Operational feel | Smaller core, expects LDAP comfort | More batteries included, friendlier day-two tooling |
| Good fit | You want a lean standalone LDAP service and are happy editing LDIF directly | You want enterprise-ish tooling, plugins, and something closer to what powers FreeIPA |
If you actually want Linux host enrollment, Kerberos, HBAC, sudo rules, and integrated certificates, do not hand-build all of that on raw LDAP. Start with FreeIPA instead; it already runs on top of 389DS and solves the higher-level identity problems.
Schema and DIT design
Keep the directory tree boring. Boring is good. A common pattern is one suffix, then clear organizational units for people, groups, and service accounts.
dc=example,dc=internal
+-- ou=People
+-- ou=Groups
+-- ou=Service Accounts
`-- ou=Hosts
dn: uid=svc-keycloak,ou=Service Accounts,dc=example,dc=internal
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: svc-keycloak
cn: Keycloak Service Account
sn: Service
mail: svc-keycloak@example.internal
Prefer standard object classes and attributes first. Add custom schema only when a real consumer needs new data. App teams love asking for custom attributes; your future replication and migration plan does not.
dn: cn=custom,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: custom
olcAttributeTypes: ( 1.3.6.1.4.1.4203.999.1.1 NAME 'employeeCostCenter'
DESC 'Cost center code'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.4203.999.2.1 NAME 'examplePerson'
SUP inetOrgPerson AUXILIARY
MAY employeeCostCenter )
TLS and secure binds
Whether you choose OpenLDAP or 389DS, the security story is the same: clients should use LDAPS or StartTLS, trust the issuing CA, and validate the server hostname. See Certificates if the PKI side is not already in place.
OpenLDAP TLS in cn=config
dn: cn=config
changetype: modify
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/openldap/certs/ca.crt
-
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/openldap/certs/ldap.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/openldap/certs/ldap.key
389DS TLS and secure binds
# Enable LDAPS on port 636
dsconf -D "cn=Directory Manager" ldap://dir1.example.internal \
config replace nsslapd-securePort=636 nsslapd-security=on
# Select the server cert stored in the NSS database
dsconf -D "cn=Directory Manager" ldap://dir1.example.internal \
security rsa set --tls-allow-rsa-certificates on \
--nss-token "internal (software)" \
--nss-cert-name Server-Cert
# Refuse simple binds over cleartext
dsconf -D "cn=Directory Manager" ldap://dir1.example.internal \
config replace nsslapd-require-secure-binds=on
Once secure binds are required, applications like Keycloak and SSSD must connect with LDAPS or StartTLS. That is the correct failure mode; plaintext bind success is the insecure state.
Indexes and search performance
LDAP search performance comes down to filter patterns and indexes. If apps search on an attribute that is not indexed, you get slow subtree scans and eventually operations staff blaming LDAP for a client-side design problem.
OpenLDAP index example
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcDbIndex
olcDbIndex: uid eq,pres,sub
olcDbIndex: mail eq,pres,sub
olcDbIndex: member eq
olcDbIndex: memberOf eq
# Rebuild indexes during a maintenance window
sudo systemctl stop slapd
sudo slapindex
sudo chown -R ldap:ldap /var/lib/ldap
sudo systemctl start slapd
389DS index example
# Add equality and substring indexes, then reindex automatically
dsconf -D "cn=Directory Manager" ldap://dir1.example.internal \
backend index add --attr roomNumber --index-type eq --index-type sub --reindex userRoot
# Add an extra index type later
dsconf -D "cn=Directory Manager" ldap://dir1.example.internal \
backend index set --attr roomNumber --add-type pres userRoot
Index what the clients really ask for: (uid=...), (mail=...), (member=...), (memberOf=...). Do not index everything "just in case"; every write has to maintain those indexes.
Replication
Replication problems are usually not network problems. They are schema mismatches, wrong bind DNs, missing secure-bind settings, or agreements created before both sides were actually ready.
OpenLDAP syncrepl pattern
dn: cn=config
changetype: modify
replace: olcServerID
olcServerID: 1 ldap://dir1.example.internal
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcSyncrepl
olcSyncrepl: rid=002 provider=ldaps://dir2.example.internal
bindmethod=simple
binddn="cn=replicator,dc=example,dc=internal"
credentials=ReplicateMe!
searchbase="dc=example,dc=internal"
type=refreshAndPersist
retry="5 5 300 +"
timeout=1
-
replace: olcMirrorMode
olcMirrorMode: TRUE
Apply the equivalent config on the peer with a different olcServerID and rid. Keep the schema identical on both nodes before you ever start replication.
389DS multi-supplier agreement
# Create an agreement on supplier1 and initialize supplier2
dsconf -D "cn=Directory Manager" ldap://supplier1.example.internal \
repl-agmt create \
--suffix "dc=example,dc=internal" \
--host "supplier2.example.internal" \
--port 636 \
--conn-protocol LDAPS \
--bind-dn "cn=Replication Manager,cn=config" \
--bind-passwd "ReplicateMe!" \
--bind-method SIMPLE \
--init supplier1-to-supplier2
# Check status
dsconf -D "cn=Directory Manager" ldap://supplier1.example.internal \
repl-agmt status --suffix "dc=example,dc=internal" supplier1-to-supplier2
389DS has much better replication ergonomics than OpenLDAP. If replication is central to your design and you want built-in monitoring closer to FreeIPA Replication, 389DS is usually the easier day-two choice.
Access controls
Directory ACLs should describe the minimum a service needs: read users, read groups, maybe compare a password hash at bind time. They should not quietly grant write access because it was convenient during testing.
OpenLDAP olcAccess
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {0}to attrs=userPassword
by self write
by anonymous auth
by dn.exact="uid=svc-keycloak,ou=Service Accounts,dc=example,dc=internal" none
by * none
olcAccess: {1}to dn.subtree="ou=People,dc=example,dc=internal"
by dn.exact="uid=svc-keycloak,ou=Service Accounts,dc=example,dc=internal" read
by users read
by * none
389DS ACI example
dn: dc=example,dc=internal
changetype: modify
add: aci
aci: (targetattr = "*")(version 3.0; acl "svc-keycloak-read";
allow (read,search,compare)
userdn = "ldap:///uid=svc-keycloak,ou=Service Accounts,dc=example,dc=internal";)
For apps like Keycloak or SSSD, create a dedicated read-only bind account. Never use the global admin DN from the app config.
Operational commands
The core day-two commands are the same everywhere: bind, search, modify, export, and inspect replication state. Keep a small tested set of commands in your runbook and use them before touching the UI.
# Verify a bind and show the authenticated identity
ldapwhoami -x -H ldaps://dir1.example.internal:636 \
-D "uid=svc-keycloak,ou=Service Accounts,dc=example,dc=internal" -W
# Search one user
ldapsearch -LLL -x -H ldaps://dir1.example.internal:636 \
-D "uid=svc-keycloak,ou=Service Accounts,dc=example,dc=internal" -W \
-b "dc=example,dc=internal" "(uid=alice)" dn cn mail memberOf
# Apply a change
ldapmodify -x -H ldaps://dir1.example.internal:636 \
-D "cn=Directory Manager" -W -f change.ldif
# OpenLDAP export
sudo slapcat -n 1 -l /var/backups/slapd-$(date +%F).ldif
# 389DS index and replication inspection
dsconf -D "cn=Directory Manager" ldap://dir1.example.internal backend index list userRoot
dsconf -D "cn=Directory Manager" ldap://dir1.example.internal replication monitor
Migration notes
Migrations between OpenLDAP and 389DS are mostly about normalizing assumptions, not just copying LDIF. Expect to re-express schema, indexes, and access controls in the target server's native model.
- Export data and schema from the source first, before any target changes.
- Normalize DNs, custom attributes, and object classes. Remove dead application-specific entries while you still have context.
- Recreate indexes in the target before large imports if you know the dominant search filters.
- Translate
olcAccessto ACIs when moving into 389DS; they are not copy-paste compatible. - Test replication only after basic bind/search semantics are working on a single node.
# OpenLDAP side: export the live database
sudo slapcat -n 1 -l /srv/export/openldap-main.ldif
# Record current schema names for translation work
ldapsearch -LLL -Y EXTERNAL -H ldapi:/// -b "cn=schema,cn=config" dn cn
When the destination is 389DS, pay close attention to features that exist as plugins or operational attributes, such as unique IDs, memberOf handling, and replication metadata. When the destination is OpenLDAP, plan to replace the convenience of dsconf tooling with direct LDAP config management.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ldap_bind: Invalid credentials (49) | Wrong bind DN/password, or secure binds now required and the client still uses cleartext LDAP | Test with ldapwhoami against the exact URL and DN the app uses; switch to LDAPS or StartTLS if needed. |
Can't contact LDAP server over LDAPS | Wrong CA, hostname mismatch, or firewall on 636/TCP | Validate the cert chain and SAN, confirm the CA is trusted, and test with openssl s_client or ldapsearch -ZZ. |
| Searches are slow only on specific filters | Missing index for the searched attribute | Capture the actual filter, add the matching index, and reindex during a maintenance window. |
| Replication agreement exists but never initializes | Replication bind DN/password wrong, schema mismatch, or supplier cannot reach peer | Check repl-agmt status / monitor output, verify the replication bind manually, then compare schema on both sides. |
| Replication starts, then consumers reject changes with schema violation | Custom schema not identical across nodes | Copy schema first, data second. Do not let replication be the first time a node sees a new object class. |
| App binds successfully but sees no users or groups | ACL/ACI too restrictive or wrong base DN | Reproduce with the app's bind DN using ldapsearch and inspect access controls on the exact subtree. |