- Add syncprov module to init-config.sh - Create init-replication.sh for configuring N-way multi-master - Update entrypoint to handle replication configuration - Support LDAP_REPLICATION_ENABLED, LDAP_SERVER_ID, LDAP_REPLICATION_HOSTS - Replica servers can sync DIT from existing masters
OpenLDAP Container Operations Guide
Introduction
This document provides operational guidance for deploying and managing the Enterprise OpenLDAP container. It explains not just the how, but also the what and why behind each configuration decision.
What This Container Provides
This is a purpose-built OpenLDAP container designed to serve as a central identity provider for an enterprise environment. It provides:
- Centralized user authentication for all services (Keycloak, Nextcloud, Gitea, mail, etc.)
- POSIX account support for Linux systems via SSSD integration
- Group-based access control with automatic membership tracking
- Service account isolation so each application has its own credentials with limited permissions
Why OpenLDAP?
LDAP (Lightweight Directory Access Protocol) remains the standard for centralized identity management because:
- Universal support - Nearly every enterprise application supports LDAP authentication
- Hierarchical organization - Natural fit for organizational structures (users, groups, departments)
- Fine-grained access control - Different applications can have different levels of access
- Proven reliability - Decades of production use in demanding environments
Key Design Decisions
RFC2307bis instead of NIS schema: Traditional LDAP setups use the NIS schema where posixGroup is a STRUCTURAL class. This prevents combining POSIX groups with other group types. We use RFC2307bis where posixGroup is AUXILIARY, allowing groups to have both POSIX attributes (gidNumber) and member tracking (member attribute). This is essential for the memberOf overlay to work correctly.
Pre-configured overlays: The container includes four overlays that are critical for enterprise use:
- memberOf - Automatically maintains reverse group membership on user objects
- refint - Maintains referential integrity when users are deleted
- unique - Prevents duplicate UIDs, email addresses, and ID numbers
- ppolicy - Enforces password complexity and expiration policies
Service accounts: Instead of sharing the admin password with applications, each service gets its own account with read-only access to the directory. This follows the principle of least privilege and provides audit trails.
Architecture Overview
Directory Structure
The container creates a directory tree optimized for enterprise use:
dc=example,dc=com ← Your organization's base
│
├── ou=People ← All user accounts live here
│ └── uid=jdoe ← Individual user
│
├── ou=Groups ← Authorization and access groups
│ ├── cn=admins ← LDAP administrators
│ └── cn=developers ← Example application group
│
├── ou=Services ← Application service accounts
│ ├── cn=keycloak ← For Keycloak LDAP federation
│ ├── cn=nextcloud ← For Nextcloud user backend
│ ├── cn=gitea ← For Gitea authentication
│ ├── cn=postfix ← For mail routing lookups
│ ├── cn=dovecot ← For mail authentication
│ └── cn=sssd ← For Linux PAM/NSS
│
├── ou=Domains ← Virtual mail domains (for mail server)
│
├── ou=Policies ← Password and security policies
│ └── cn=default ← Default password policy
│
└── ou=Kerberos ← Reserved for Kerberos integration
Why this structure?
- Separate OUs for each purpose makes ACLs (access control lists) simpler and more secure
- Service accounts in their own OU allows restricting what parts of the tree they can access
- Policies OU keeps password policies separate from user data
- Domains OU is specifically for mail server virtual domain configuration
How Authentication Works
When an application authenticates a user:
- Application connects to LDAP using its service account (e.g.,
cn=keycloak,ou=Services,...) - Application searches for the user in
ou=Peopleby uid or email - Application attempts to bind (authenticate) as that user with their password
- LDAP verifies the password and returns success/failure
- Application can then query the user's groups via the
memberOfattribute
The service account never sees user passwords - it only searches for users. The actual password verification happens when the user's DN is used for a bind operation.
Getting Started
Prerequisites
Before deploying, ensure you have:
- Docker installed and running
- Decided on your domain name (this determines your base DN)
- Generated a strong admin password
- (Optional) TLS certificates if you need encrypted connections
Step 1: Basic Deployment
The simplest deployment requires just three pieces of information:
docker run -d --name openldap \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-p 389:389 \
enterprise-openldap:latest
What happens on first run:
- The container detects this is a fresh start (no existing database)
- It generates the base DN from your domain (
example.com→dc=example,dc=com) - Creates the cn=config database with your settings
- Loads all required schemas in the correct order
- Configures the overlays (memberOf, refint, unique, ppolicy)
- Creates the directory structure (OUs)
- Sets up ACLs to protect sensitive data
- Optionally creates service accounts
- Starts the LDAP server
On subsequent restarts, steps 2-8 are skipped - the existing database is used.
Step 2: Verify the Deployment
Test that the server is responding:
# Anonymous query to verify the server is up
ldapsearch -x -H ldap://localhost:389 -b "" -s base "(objectClass=*)"
Test admin authentication:
ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "your-secure-password" \
-b "dc=example,dc=com" "(objectClass=organizationalUnit)"
You should see the OUs (People, Groups, Services, etc.) listed.
Step 3: Enable Service Accounts (Recommended)
Service accounts allow applications to connect with limited, auditable credentials:
docker run -d --name openldap \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-e LDAP_CREATE_SERVICE_ACCOUNTS=true \
-p 389:389 \
enterprise-openldap:latest
The generated passwords are saved to /var/lib/openldap/service-passwords.txt inside the container. Retrieve them:
docker exec openldap cat /var/lib/openldap/service-passwords.txt
Important: Save these passwords securely, then delete the file:
docker exec openldap rm /var/lib/openldap/service-passwords.txt
Step 4: Add Persistence
Without volumes, your data is lost when the container is removed. For production, always use volumes:
docker run -d --name openldap \
-v openldap-data:/var/lib/openldap/openldap-data \
-v openldap-config:/etc/openldap/slapd.d \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-e LDAP_CREATE_SERVICE_ACCOUNTS=true \
-p 389:389 \
enterprise-openldap:latest
Volume purposes:
| Volume | Contains | Why it matters |
|---|---|---|
/var/lib/openldap/openldap-data |
The actual LDAP database (MDB format) | All your users, groups, and data |
/etc/openldap/slapd.d |
The cn=config configuration | Schema, overlays, ACLs, indexes |
Step 5: Enable TLS (Production)
For production environments, you should enable TLS to encrypt connections:
docker run -d --name openldap \
-v openldap-data:/var/lib/openldap/openldap-data \
-v openldap-config:/etc/openldap/slapd.d \
-v /path/to/certs:/certs:ro \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-e LDAP_TLS_ENABLED=true \
-p 389:389 -p 636:636 \
enterprise-openldap:latest
The /certs directory must contain:
ldap.crt- Server certificateldap.key- Private key (readable only by root/ldap user)ca.crt- CA certificate (for client verification)
Port 389 vs 636:
- Port 389 (LDAP) - Unencrypted, but supports STARTTLS upgrade
- Port 636 (LDAPS) - TLS from the start, like HTTPS
Most modern applications support STARTTLS on port 389, which is generally preferred.
Configuration Reference
Required Environment Variables
These must be set for the container to start:
| Variable | Purpose | Example |
|---|---|---|
LDAP_DOMAIN |
Your organization's domain. Converted to base DN automatically. | example.com → dc=example,dc=com |
LDAP_ORGANISATION |
Human-readable organization name, stored in the base entry. | Example Corporation |
LDAP_ADMIN_PASSWORD |
Password for the directory administrator account. Use cleartext - the container hashes it automatically with slappasswd. |
Str0ng!Passw0rd |
Optional Environment Variables
General Settings
| Variable | Default | Purpose |
|---|---|---|
LDAP_BASE_DN |
Generated from domain | Override if you need a non-standard base DN |
LDAP_CONFIG_PASSWORD |
Random | Password for cn=config administrative access. Only needed for advanced configuration changes. |
LDAP_LOG_LEVEL |
256 |
Controls logging verbosity. 256 logs connections and operations. See Log Levels. |
TLS Settings
| Variable | Default | Purpose |
|---|---|---|
LDAP_TLS_ENABLED |
true |
Master switch for TLS. Set false to disable completely. |
LDAP_TLS_CERT_FILE |
/certs/ldap.crt |
Path to server certificate inside container |
LDAP_TLS_KEY_FILE |
/certs/ldap.key |
Path to private key inside container |
LDAP_TLS_CA_FILE |
/certs/ca.crt |
Path to CA certificate for client verification |
LDAP_TLS_VERIFY_CLIENT |
try |
Client cert policy: never (don't ask), try (optional), demand (required) |
Service Account Settings
| Variable | Default | Purpose |
|---|---|---|
LDAP_CREATE_SERVICE_ACCOUNTS |
false |
Set true to create pre-configured service accounts |
LDAP_SERVICE_KEYCLOAK_PASSWORD |
Random | Explicit password for Keycloak account |
LDAP_SERVICE_NEXTCLOUD_PASSWORD |
Random | Explicit password for Nextcloud account |
LDAP_SERVICE_GITEA_PASSWORD |
Random | Explicit password for Gitea account |
LDAP_SERVICE_POSTFIX_PASSWORD |
Random | Explicit password for Postfix account |
LDAP_SERVICE_DOVECOT_PASSWORD |
Random | Explicit password for Dovecot account |
LDAP_SERVICE_SSSD_PASSWORD |
Random | Explicit password for SSSD account |
When to set explicit passwords: If you're deploying with configuration management (Ansible, Terraform) and need predictable credentials, set them explicitly. For manual deployments, random passwords are more secure.
Day-to-Day Operations
Adding Users
Users belong in ou=People. Here's a complete user entry:
dn: uid=jdoe,ou=People,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: jdoe
cn: John Doe
sn: Doe
givenName: John
mail: john.doe@example.com
uidNumber: 10001
gidNumber: 10000
homeDirectory: /home/jdoe
loginShell: /bin/bash
userPassword: {SSHA}hashed-password
Object classes explained:
inetOrgPerson- Standard LDAP person with email, phone, etc.posixAccount- UNIX account attributes (uid/gid numbers, home directory, shell)shadowAccount- Password aging and expiration
Add the user:
ldapadd -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
-f user.ldif
Adding Groups
Groups use groupOfMembers (for member tracking) combined with posixGroup (for UNIX gidNumber):
dn: cn=developers,ou=Groups,dc=example,dc=com
objectClass: groupOfMembers
objectClass: posixGroup
cn: developers
gidNumber: 10001
description: Development team
member: uid=jdoe,ou=People,dc=example,dc=com
Why groupOfMembers instead of groupOfNames?
groupOfNames requires at least one member - you can't have an empty group. groupOfMembers allows empty groups, which is often needed when setting up access control before adding members.
Checking Group Membership
Thanks to the memberOf overlay, you can see a user's groups directly:
ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
-b "uid=jdoe,ou=People,dc=example,dc=com" \
memberOf
This returns all groups the user belongs to, without having to search each group.
Modifying Entries
To change an attribute:
dn: uid=jdoe,ou=People,dc=example,dc=com
changetype: modify
replace: mail
mail: j.doe@example.com
ldapmodify -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
-f modify.ldif
Deleting Entries
ldapdelete -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
"uid=jdoe,ou=People,dc=example,dc=com"
The refint overlay automatically removes the user from any groups they belonged to.
Connecting Applications
Keycloak
Keycloak uses LDAP for user federation. Configure it with:
| Setting | Value |
|---|---|
| Connection URL | ldap://openldap:389 |
| Bind DN | cn=keycloak,ou=Services,dc=example,dc=com |
| Bind Credential | (from service-passwords.txt) |
| Users DN | ou=People,dc=example,dc=com |
| Username Attribute | uid |
| RDN Attribute | uid |
| UUID Attribute | entryUUID |
Nextcloud
In Nextcloud's LDAP settings:
| Setting | Value |
|---|---|
| Host | openldap |
| Port | 389 |
| User DN | cn=nextcloud,ou=Services,dc=example,dc=com |
| Password | (from service-passwords.txt) |
| Base DN | dc=example,dc=com |
| User Filter | (&(objectClass=inetOrgPerson)(uid=%uid)) |
| Group Filter | (objectClass=groupOfMembers) |
SSSD (Linux PAM/NSS)
For Linux systems to authenticate against LDAP:
# /etc/sssd/sssd.conf
[sssd]
services = nss, pam
domains = example.com
[domain/example.com]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldap://openldap.example.com
ldap_search_base = dc=example,dc=com
ldap_default_bind_dn = cn=sssd,ou=Services,dc=example,dc=com
ldap_default_authtok = sssd-password-here
ldap_user_search_base = ou=People,dc=example,dc=com
ldap_group_search_base = ou=Groups,dc=example,dc=com
Backup and Recovery
Why Backups Matter
The LDAP directory contains:
- All user accounts and credentials
- Group memberships and access control
- Service account configurations
- Password policies
Losing this data means losing access to all integrated systems.
Creating Backups
Export the directory to LDIF format:
# Backup user data (database 1)
docker exec openldap slapcat -n 1 > ldap-data-$(date +%Y%m%d).ldif
# Backup configuration (database 0) - optional but recommended
docker exec openldap slapcat -n 0 > ldap-config-$(date +%Y%m%d).ldif
Backup frequency recommendation:
- Daily for the data backup
- After any configuration changes for the config backup
Restoring from Backup
Warning: This destroys all existing data!
# Stop the container
docker stop openldap
docker rm openldap
# Remove existing volumes
docker volume rm openldap-data openldap-config
# Create fresh container with backup
docker run --rm \
-v openldap-data:/var/lib/openldap/openldap-data \
-v $(pwd)/ldap-data-20240101.ldif:/backup.ldif:ro \
enterprise-openldap:latest \
sh -c "slapadd -n 1 -l /backup.ldif && chown -R ldap:ldap /var/lib/openldap"
# Start normally
docker run -d --name openldap \
-v openldap-data:/var/lib/openldap/openldap-data \
-v openldap-config:/etc/openldap/slapd.d \
... # your normal settings
Troubleshooting
Understanding Log Levels
The LDAP_LOG_LEVEL controls what gets logged. Common values:
| Level | What it shows | When to use |
|---|---|---|
0 |
Nothing | Never in production |
256 |
Connections, operations, results | Normal operation |
384 |
Above + ACL decisions | Debugging access issues |
512 |
Stats with entry counts | Performance analysis |
-1 |
Everything | Last resort debugging |
Increase temporarily for debugging:
docker run -e LDAP_LOG_LEVEL=384 ...
Common Issues
Container exits immediately
Symptom: Container starts and stops within seconds.
Cause: Missing required environment variables.
Solution: Check that LDAP_DOMAIN, LDAP_ORGANISATION, and LDAP_ADMIN_PASSWORD are all set.
docker logs openldap # Will show which variable is missing
Cannot authenticate as admin
Symptom: ldap_bind: Invalid credentials (49)
Causes:
- Wrong password
- Wrong bind DN (check the domain components match your LDAP_DOMAIN)
- Typing
dc=example.cominstead ofdc=example,dc=com
Solution: Verify the exact bind DN:
# Your bind DN is always: cn=admin,<your-base-dn>
# If LDAP_DOMAIN=example.com, then it's: cn=admin,dc=example,dc=com
TLS connection refused
Symptom: Cannot connect to port 636.
Cause: Certificates not found or not readable.
Solution: Check the logs for TLS errors:
docker logs openldap | grep -i tls
Verify certificates are mounted correctly:
docker exec openldap ls -la /certs/
Service account cannot search
Symptom: Service account binds successfully but searches return empty.
Cause: ACLs restrict what the service account can see.
Solution: Service accounts can only read specific OUs. Verify you're searching the correct base:
# Correct - searching People OU
ldapsearch -D "cn=keycloak,ou=Services,..." -b "ou=People,dc=example,dc=com" ...
# Wrong - searching the entire tree
ldapsearch -D "cn=keycloak,ou=Services,..." -b "dc=example,dc=com" ...
Users don't have memberOf attribute
Symptom: User exists in group but memberOf attribute is empty.
Cause: User was added before the memberOf overlay, or group uses wrong attribute.
Solution: The memberOf overlay only tracks member attribute on groupOfMembers objects. Verify:
- Group has
objectClass: groupOfMembers - Group uses
memberattribute (notmemberUid)
To fix existing users, remove and re-add them to groups.
Security Recommendations
Password Security
- Use strong admin password - At least 16 characters with mixed case, numbers, symbols
- Rotate service account passwords periodically
- Delete service-passwords.txt after retrieving the passwords
- Never commit passwords to version control
Network Security
- Use TLS for all production deployments
- Restrict port access - Only allow LDAP ports from trusted networks
- Use internal Docker networks when possible:
docker network create ldap-net
docker run --network ldap-net --name openldap ...
# Other containers on ldap-net can access via hostname "openldap"
Access Control
- Use service accounts instead of sharing admin credentials
- Principle of least privilege - Service accounts only get read access
- Audit regularly - Check who has access to what
Docker Compose Reference
Complete production-ready example:
version: '3.8'
services:
openldap:
image: enterprise-openldap:latest
container_name: openldap
hostname: ldap.example.com
environment:
LDAP_DOMAIN: example.com
LDAP_ORGANISATION: "Example Corporation"
LDAP_ADMIN_PASSWORD: ${LDAP_ADMIN_PASSWORD:?Set LDAP_ADMIN_PASSWORD}
LDAP_TLS_ENABLED: "true"
LDAP_CREATE_SERVICE_ACCOUNTS: "true"
LDAP_LOG_LEVEL: "256"
volumes:
- openldap-data:/var/lib/openldap/openldap-data
- openldap-config:/etc/openldap/slapd.d
- ./certs:/certs:ro
ports:
- "389:389"
- "636:636"
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "ldapsearch", "-x", "-H", "ldap://localhost", "-b", "", "-s", "base"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
backend:
driver: bridge
volumes:
openldap-data:
openldap-config:
Create a .env file:
LDAP_ADMIN_PASSWORD=your-secure-password-here
Start:
docker compose up -d