# 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: 1. **Universal support** - Nearly every enterprise application supports LDAP authentication 2. **Hierarchical organization** - Natural fit for organizational structures (users, groups, departments) 3. **Fine-grained access control** - Different applications can have different levels of access 4. **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: 1. Application connects to LDAP using its service account (e.g., `cn=keycloak,ou=Services,...`) 2. Application searches for the user in `ou=People` by uid or email 3. Application attempts to bind (authenticate) as that user with their password 4. LDAP verifies the password and returns success/failure 5. Application can then query the user's groups via the `memberOf` attribute 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: ```bash 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:** 1. The container detects this is a fresh start (no existing database) 2. It generates the base DN from your domain (`example.com` → `dc=example,dc=com`) 3. Creates the cn=config database with your settings 4. Loads all required schemas in the correct order 5. Configures the overlays (memberOf, refint, unique, ppolicy) 6. Creates the directory structure (OUs) 7. Sets up ACLs to protect sensitive data 8. Optionally creates service accounts 9. 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: ```bash # Anonymous query to verify the server is up ldapsearch -x -H ldap://localhost:389 -b "" -s base "(objectClass=*)" ``` Test admin authentication: ```bash 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: ```bash 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: ```bash docker exec openldap cat /var/lib/openldap/service-passwords.txt ``` **Important**: Save these passwords securely, then delete the file: ```bash 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: ```bash 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: ```bash 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 certificate - `ldap.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](#understanding-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: ```ldif 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: ```bash 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): ```ldif 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: ```bash 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: ```ldif dn: uid=jdoe,ou=People,dc=example,dc=com changetype: modify replace: mail mail: j.doe@example.com ``` ```bash ldapmodify -x -H ldap://localhost:389 \ -D "cn=admin,dc=example,dc=com" \ -w "admin-password" \ -f modify.ldif ``` ### Deleting Entries ```bash 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: ```ini # /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: ```bash # 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! ```bash # 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: ```bash 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. ```bash docker logs openldap # Will show which variable is missing ``` #### Cannot authenticate as admin **Symptom**: `ldap_bind: Invalid credentials (49)` **Causes**: 1. Wrong password 2. Wrong bind DN (check the domain components match your LDAP_DOMAIN) 3. Typing `dc=example.com` instead of `dc=example,dc=com` **Solution**: Verify the exact bind DN: ```bash # Your bind DN is always: cn=admin, # 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: ```bash docker logs openldap | grep -i tls ``` Verify certificates are mounted correctly: ```bash 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: ```bash # 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: 1. Group has `objectClass: groupOfMembers` 2. Group uses `member` attribute (not `memberUid`) To fix existing users, remove and re-add them to groups. --- ## Security Recommendations ### Password Security 1. **Use strong admin password** - At least 16 characters with mixed case, numbers, symbols 2. **Rotate service account passwords** periodically 3. **Delete service-passwords.txt** after retrieving the passwords 4. **Never commit passwords** to version control ### Network Security 1. **Use TLS** for all production deployments 2. **Restrict port access** - Only allow LDAP ports from trusted networks 3. **Use internal Docker networks** when possible: ```bash docker network create ldap-net docker run --network ldap-net --name openldap ... # Other containers on ldap-net can access via hostname "openldap" ``` ### Access Control 1. **Use service accounts** instead of sharing admin credentials 2. **Principle of least privilege** - Service accounts only get read access 3. **Audit regularly** - Check who has access to what --- ## Docker Compose Reference Complete production-ready example: ```yaml 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: ```bash LDAP_ADMIN_PASSWORD=your-secure-password-here ``` Start: ```bash docker compose up -d ```