OpenLDAP & 389DS

Use this page when you need a general-purpose LDAP directory outside FreeIPA: what differs between OpenLDAP and 389 Directory Server, how to operate both, and what usually breaks.

If you only remember six things
  • 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.

AreaOpenLDAP389 Directory Server
Config modelcn=config LDIF, very LDAP-nativedsconf, web console, and LDAP config entries
Replicationsyncrepl, provider/consumer, mirror modeBuilt-in multi-supplier tooling and monitoring
Access contrololcAccess rulesACIs stored in the directory
Operational feelSmaller core, expects LDAP comfortMore batteries included, friendlier day-two tooling
Good fitYou want a lean standalone LDAP service and are happy editing LDIF directlyYou 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 )
Schema rule: if two apps disagree on attribute semantics, do not reuse the same custom attribute name for both. LDAP schema mistakes survive much longer than app code mistakes.

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
Related consumers: if Linux clients are querying this directory through NSS/PAM, pair this page with SSSD & Auth Flow. If Keycloak is the front door, pair it with Keycloak.

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.

  1. Export data and schema from the source first, before any target changes.
  2. Normalize DNs, custom attributes, and object classes. Remove dead application-specific entries while you still have context.
  3. Recreate indexes in the target before large imports if you know the dominant search filters.
  4. Translate olcAccess to ACIs when moving into 389DS; they are not copy-paste compatible.
  5. 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

SymptomLikely causeFix
ldap_bind: Invalid credentials (49)Wrong bind DN/password, or secure binds now required and the client still uses cleartext LDAPTest with ldapwhoami against the exact URL and DN the app uses; switch to LDAPS or StartTLS if needed.
Can't contact LDAP server over LDAPSWrong CA, hostname mismatch, or firewall on 636/TCPValidate 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 filtersMissing index for the searched attributeCapture the actual filter, add the matching index, and reindex during a maintenance window.
Replication agreement exists but never initializesReplication bind DN/password wrong, schema mismatch, or supplier cannot reach peerCheck repl-agmt status / monitor output, verify the replication bind manually, then compare schema on both sides.
Replication starts, then consumers reject changes with schema violationCustom schema not identical across nodesCopy 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 groupsACL/ACI too restrictive or wrong base DNReproduce with the app's bind DN using ldapsearch and inspect access controls on the exact subtree.