LDAP Setup

Architecture Overview

OpenLDAP (slapd) runs inside the phpLDAPadmin container rather than as a separate service. A custom entrypoint script renders jinja2 templates from environment variables, starts slapd in the background, bootstraps initial data if needed, then hands off to the phpLDAPadmin (Caddy) process.

This approach: - Simplifies LDAP debugging (the cleanstart/openldap image lacks shell and system tools for interactive debugging) - Eliminates a separate LDAP container and inter-container networking for LDAP - Keeps slapd configuration and bootstrap data in a single Dockerfile - Uses 127.0.0.1:389 for phpLDAPadmin → slapd communication (no network hop) - Avoids hardcoding domain names and passwords into the image (jinja2 templates + env vars)

Image and Dockerfile

Base image: phpldapadmin/phpldapadmin:2.3 (Laravel-based, uses Caddy as web server)

The custom Dockerfile installs OpenLDAP server components and jinja2 on top:

FROM phpldapadmin/phpldapadmin:2.3

USER root

RUN apk add --no-cache openldap openldap-clients openldap-back-mdb openldap-overlay-memberof bash python3 py3-jinja2

RUN mkdir -p /var/lib/openldap/openldap-data /run/openldap \
    && chown -R ldap:ldap /var/lib/openldap /run/openldap

COPY files/ /

RUN chmod +x /opt/bin/*

ENTRYPOINT ["/opt/bin/entrypoint.sh"]

Key packages: - openldap — slapd server - openldap-clients — ldapsearch, ldapadd, etc. - openldap-back-mdb — MDB database backend - openldap-overlay-memberof — automatic memberOf attribute maintenance - python3, py3-jinja2 — template rendering at container startup

File Layout

phpldapadmin/
├── Dockerfile
├── variables.env                              # Laravel .env (legacy, being replaced by jinja2)
└── files/
    ├── opt/
    │   ├── bin/
    │   │   ├── entrypoint.sh                  # Custom entrypoint
    │   │   └── render_variables.py            # Jinja2 template renderer
    │   └── etc/
    │       └── openldap/
    │           ├── slapd.conf.jinja2          # OpenLDAP server config template
    │           ├── bootstrap.ldif.jinja2      # Initial data template
    │           └── variables.env.jinja2       # Laravel .env template
    └── app/
        └── templates/
            └── custom/
                └── cicd_user.json             # phpLDAPadmin user creation template

Jinja2 Templating

Configuration files use jinja2 templates ({{ env['VAR_NAME'] }}) rendered at container startup by render_variables.py. Environment variables come from env.txt via docker-compose env_file.

Key variables in env.txt: - LDAP_BASE_DN — e.g. dc=pathoris,dc=de - LDAP_ADMIN_USER — e.g. admin - LDAP_ADMIN_PASSWORD — admin password - LDAP_READ_USERNAME / LDAP_READ_INITIAL_PASSWORD — readonly bind account - CICD_ADMIN_USERNAME / CICD_ADMIN_INITIAL_PASSWORD — initial CI/CD admin user - LDAP_ORGANISATION — organization name for the base DN entry - CI_DOMAIN — domain component for the dc: attribute

Entrypoint Script

The entrypoint (/opt/bin/entrypoint.sh) performs these steps:

  1. Renders variables.env.jinja2/app/.env (phpLDAPadmin Laravel config)
  2. Renders slapd.conf.jinja2/etc/openldap/slapd.conf
  3. Removes /etc/openldap/slapd.d/ if present (cn=config takes precedence over slapd.conf)
  4. Ensures data and run directories exist with correct ownership
  5. Starts slapd in the background on port 389
  6. Waits up to 15 seconds for slapd to become ready
  7. If base DN doesn't exist: renders bootstrap.ldif.jinja2bootstrap.ldif, then runs ldapadd
  8. Hands off to phpLDAPadmin's Caddy server via exec

slapd.conf

Key configuration (after template rendering):

suffix    "dc=pathoris,dc=de"
rootdn    "cn=admin,dc=pathoris,dc=de"
rootpw    <from LDAP_ADMIN_PASSWORD>

Important: The overlay memberof directive must appear after database mdb but before suffix. This overlay automatically maintains memberOf attributes on user entries when they are added to groupOfNames groups.

Access Control

Bootstrap Data (bootstrap.ldif)

Created on first start when the base DN doesn't exist:

Service accounts (admin, readonly) sit directly under the base DN, not under ou=users. This is intentional — they are infrastructure accounts, not regular users.

Custom User Template (cicd_user.json)

phpLDAPadmin v2 supports custom JSON templates in /app/templates/custom/. The cicd_user.json template provides a form for creating CI/CD user accounts with:

Data Persistence

LDAP data is persisted via a volume mount in docker-compose:

volumes:
  - /var/lib/cicd/ldap/data:/var/lib/openldap/openldap-data:z

Use :z (lowercase, shared SELinux label), not :Z (private). The uppercase variant can cause permission issues.

Service Integration

Jenkins

Jenkins connects to LDAP via the phpldapadmin container hostname on port 389. The 01_enable_ldap.groovy init script configures this on first boot. It uses the readonly account for searches and searches under ou=users with filter uid={0}.

Forgejo

Forgejo's LDAP authentication source is configured via the web UI. The User Search Base must be the full DN: ou=users,dc=pathoris,dc=de (Forgejo doesn't have a separate "Base DN" field).

Troubleshooting

Check slapd is running inside the container

podman exec -it phpldapadmin ps aux | grep slapd

Check naming contexts

podman exec -it phpldapadmin ldapsearch -x -H ldap://localhost:389 -s base -b "" namingContexts

Test admin bind

podman exec -it phpldapadmin ldapsearch -x -H ldap://localhost:389 \
  -D "cn=admin,dc=pathoris,dc=de" -w '<LDAP_ADMIN_PASSWORD>' \
  -b "dc=pathoris,dc=de" "(objectClass=*)"

Reset LDAP data

podman compose stop phpldapadmin
rm -rf /var/lib/cicd/ldap/data/*
podman compose up -d --build phpldapadmin

Check memberOf overlay is working

podman exec -it phpldapadmin ldapsearch -x -H ldap://localhost:389 \
  -D "cn=admin,dc=pathoris,dc=de" -w '<LDAP_ADMIN_PASSWORD>' \
  -b "ou=users,dc=pathoris,dc=de" "(uid=cicdadmin)" memberOf

Common errors

Error Cause Fix
namingContexts: dc=my-domain,dc=com slapd.conf not rendered correctly Check entrypoint logs, verify env vars
Invalid credentials Wrong password or stale data Reset LDAP data directory
No such object on user search Search base not fully qualified Use full DN in search base
phpLDAPadmin keeps restarting slapd.conf syntax error or missing env var Check podman logs phpldapadmin
Mixed content errors Missing X-Forwarded-Proto https Check nginx proxy headers
CSRF 419 on AJAX Wrong Referrer-Policy Use same-origin in nginx