Serve LDAPS, StartTLS and IPv6 from the dev LDAP

Production and the client's test server use ldaps://, so the dev server
now offers TLS as well: ldaps on 636 (localhost:3636) and StartTLS on
389, with both ports also published on [::1].

A throwaway CA and server certificate are generated on every start, with
the full CA extensions that Python 3.13+ strict verification requires.
The CA certificate is copied to dev/ldap/export/ca.crt (gitignored).
The server certificate covers localhost, the service names and ::1, but
not 127.0.0.1, so tests can show that hostname mismatches are
rejected.
This commit is contained in:
sajeyks 2026-11-10 14:54:20 +03:00
commit b9c043dcd4
5 changed files with 42 additions and 5 deletions

3
.gitignore vendored
View file

@ -39,3 +39,6 @@ secret-key
*.mo
venv/
# dev LDAP CA certificate, regenerated on every start
dev/ldap/export/

View file

@ -9,6 +9,7 @@ RUN apk add --no-cache \
openldap \
openldap-back-mdb \
openldap-clients \
openssl \
&& mkdir -p /run/openldap /var/lib/openldap/openldap-data /etc/openldap/slapd.d \
&& rm -rf /etc/openldap/slapd.conf /etc/openldap/slapd.ldif
@ -16,7 +17,7 @@ COPY config.ldif openssh-lpk.ldif seed.ldif /bootstrap/
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 389
EXPOSE 389 636
HEALTHCHECK --interval=5s --timeout=3s --retries=10 \
CMD ldapsearch -x -H ldap://localhost -b "" -s base namingContexts | grep -q "dc=ungleich,dc=ch" || exit 1

View file

@ -3,6 +3,10 @@ objectClass: olcGlobal
cn: config
olcArgsFile: /run/openldap/slapd.args
olcPidFile: /run/openldap/slapd.pid
olcTLSCACertificateFile: /etc/openldap/tls/ca.crt
olcTLSCertificateFile: /etc/openldap/tls/server.crt
olcTLSCertificateKeyFile: /etc/openldap/tls/server.key
olcTLSProtocolMin: 3.3
dn: cn=module,cn=config
objectClass: olcModuleList

View file

@ -8,6 +8,28 @@ DATA=/var/lib/openldap/openldap-data
rm -rf "${CONF:?}"/* "${DATA:?}"/*
# Throwaway CA and server certificate, new on every start. The CA cert is
# copied to /export (mounted from the host) so clients can verify the
# server. 127.0.0.1 is left out on purpose: tests use it to prove that
# hostname verification rejects a name the certificate does not cover.
TLS=/etc/openldap/tls
mkdir -p "$TLS"
# Full CA extensions: Python 3.13+ verifies strictly by default and
# rejects a CA without keyUsage.
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 3650 \
-subj "/CN=ungleich-user dev CA" -keyout "$TLS/ca.key" -out "$TLS/ca.crt" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "subjectKeyIdentifier=hash" 2>/dev/null
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-subj "/CN=localhost" -keyout "$TLS/server.key" -out "$TLS/server.csr" 2>/dev/null
printf 'subjectAltName=%s\nextendedKeyUsage=serverAuth\nkeyUsage=critical,digitalSignature\nbasicConstraints=CA:FALSE\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid\n' \
"DNS:localhost,DNS:ldap,DNS:ldap1,DNS:ldap2,DNS:v6-ldap,IP:::1" > "$TLS/ext.cnf"
openssl x509 -req -in "$TLS/server.csr" -CA "$TLS/ca.crt" -CAkey "$TLS/ca.key" \
-CAcreateserial -days 3650 -extfile "$TLS/ext.cnf" -out "$TLS/server.crt" 2>/dev/null
rm -f "$TLS/ca.key"
if [ -d /export ]; then cp "$TLS/ca.crt" /export/ca.crt; fi
ADMIN_PW_HASH=$(slappasswd -s "${LDAP_ADMIN_PASSWORD:-admin}")
# cn=config: modules, schemas, database, ACLs
@ -18,11 +40,11 @@ slapadd -n 0 -F "$CONF" -l /bootstrap/openssh-lpk.ldif
# Directory tree and seed users
slapadd -n 1 -F "$CONF" -l /bootstrap/seed.ldif
chown -R ldap:ldap "$CONF" "$DATA" /run/openldap
chown -R ldap:ldap "$CONF" "$DATA" /run/openldap "$TLS"
# slapd sizes its descriptor table by the open-file limit; under some
# runtimes (kind/containerd) that is ~10^9, and slapd is OOM-killed.
ulimit -n 4096
# -d keeps slapd in the foreground; "stats" logs every operation to stdout.
exec slapd -u ldap -g ldap -F "$CONF" -h "ldap:///" -d "${SLAPD_LOGLEVEL:-stats}"
exec slapd -u ldap -g ldap -F "$CONF" -h "ldap:/// ldaps:///" -d "${SLAPD_LOGLEVEL:-stats}"

View file

@ -1,6 +1,8 @@
# Dev dependencies only. The Django app runs natively (no container) and
# talks to LDAP on ldap://localhost:3389 and, if DATABASE_HOST=localhost is
# set, to PostgreSQL on localhost:5433 (SQLite is used otherwise).
# talks to LDAP on ldap://localhost:3389 (StartTLS) and ldaps://localhost:3636,
# and, if DATABASE_HOST=localhost is set, to PostgreSQL on localhost:5433
# (SQLite is used otherwise). The LDAP CA certificate is written to
# dev/ldap/export/ca.crt on every start.
#
# docker compose -f docker-compose.dev.yml up -d --build
# docker compose -f docker-compose.dev.yml up -d --force-recreate ldap # reset data
@ -12,6 +14,11 @@ services:
LDAP_ADMIN_PASSWORD: admin
ports:
- "127.0.0.1:3389:389"
- "127.0.0.1:3636:636"
- "[::1]:3389:389"
- "[::1]:3636:636"
volumes:
- ./dev/ldap/export:/export
postgres:
image: postgres:18-alpine