diff --git a/.gitignore b/.gitignore index 712a31c..2af37cc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ secret-key *.mo venv/ + +# dev LDAP CA certificate, regenerated on every start +dev/ldap/export/ diff --git a/dev/ldap/Dockerfile b/dev/ldap/Dockerfile index d7bd11e..d881f16 100644 --- a/dev/ldap/Dockerfile +++ b/dev/ldap/Dockerfile @@ -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 diff --git a/dev/ldap/config.ldif b/dev/ldap/config.ldif index 12d2c00..fb85a10 100644 --- a/dev/ldap/config.ldif +++ b/dev/ldap/config.ldif @@ -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 diff --git a/dev/ldap/entrypoint.sh b/dev/ldap/entrypoint.sh index 6d7e6e2..b73117b 100644 --- a/dev/ldap/entrypoint.sh +++ b/dev/ldap/entrypoint.sh @@ -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}" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8246581..d1ed4b5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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