It opens with the five required variables, then what production
usually sets (proxy header, PostgreSQL, mail relay, OTP), then every
optional setting commented out at its default. It lists exactly the 51
variables the code reads.
Without OTPSERVER, new accounts get no OTP seed, the seed page says OTP
is not configured, and the seed API returns 503. Everything else works
as before. If OTPSERVER is set, ADMIN_SEED, ADMIN_NAME, ADMIN_REALM
and USER_REALM are required and startup fails without them.
Fewer variables are required to deploy. The directory layout now derives
from ENTIRE_SEARCH_BASE (default dc=ungleich,dc=ch): LDAP_CUSTOMER_DN
defaults to ou=customer under it and LDAPSEARCH to ou=customer plus
ou=users. A different tree, such as the test server, only sets
ENTIRE_SEARCH_BASE. LDAP_CUSTOMER_GROUP_ID (10004) and
LDAP_DEFAULT_START_UID (10000) default to production's values, and
ALLOWED_HOSTS to account.ungleich.ch.
CSRF_TRUSTED_ORIGINS defaults to https:// plus each allowed host, which
is what a site behind an HTTPS ingress needs. LDAPSERVER deliberately
has no default, so a test deployment cannot reach production by
accident.
New customer entries always got ldapPublicKey, which needs the
openssh-lpk schema. Production has it; ungleich's test server only
loads core, cosine, inetorgperson and nis, so registration failed
there. LDAP_USER_OBJECT_CLASSES (space separated) defaults to the
production set, inetOrgPerson posixAccount ldapPublicKey, and can drop
ldapPublicKey for servers without that schema.
ldap3 does not verify TLS certificates by default, and the app never
configured it to. ldaps:// and StartTLS were encrypted but accepted any
certificate, so anyone able to intercept LDAP traffic could pose as the
server and collect the admin password and every password used to log in.
All LDAP servers now use VerifiedTls, which leaves verification to
Python's ssl module: certificate chain and hostname, IP literals
included (ldap3's own matcher cannot handle IPv6 literals). It also
sends SNI and requires TLS 1.2 or newer. LDAP_CA_FILE adds a CA bundle
for a private CA. LDAP_TLS_VERIFY=False turns verification off, with a
logged warning. A failed StartTLS now always raises instead of
continuing in plaintext.
New tests against the dev LDAP cover ldaps, StartTLS and ldaps over an
IPv6 literal with the CA (and check the socket is encrypted), plain LDAP
over IPv6, rejection of an untrusted certificate (ldaps and StartTLS),
rejection of a hostname mismatch, and the opt-out.
The container runs only gunicorn, with no web server in front of it
inside the pod. WhiteNoise serves the collectstatic output
(STATIC_ROOT, configurable) with gzip compression. It uses the
non-hashed storage because the vendor CSS references source maps that
are not shipped. All 20 static files on the landing page are served.
Adds gunicorn and whitenoise to the requirements.
They are served by the first middleware, so probes, which use the pod
IP as Host, are not rejected by ALLOWED_HOSTS or redirected to HTTPS.
/healthz only shows the process is serving, so an LDAP outage never
restarts pods. /readyz returns 503 unless the database answers and an
LDAP server accepts the admin bind.
Sessions, password reset tokens and pending registrations live in the
database, so running more than one replica needs a shared one. With
DATABASE_HOST set the app uses PostgreSQL (psycopg 3 binary wheels, no
system libraries) with persistent, health-checked connections.
Otherwise SQLite is used, at a path configurable with SQLITE_PATH so a
single replica can keep it on a volume.
docker-compose.dev.yml gains a PostgreSQL 18 service on
localhost:5433. The full test suite passes on SQLite and PostgreSQL.
- Session and CSRF cookies are secure-only unless DEBUG is on.
- TRUST_X_FORWARDED_PROTO honours X-Forwarded-Proto from a TLS
terminating ingress. Without it, Django 4+ fails the CSRF origin
check on every form POST.
- CSRF_TRUSTED_ORIGINS, SECURE_SSL_REDIRECT and SECURE_HSTS_SECONDS are
configurable and off by default. HSTS for subdomains and preload are
left to ungleich, since they affect all of *.ungleich.ch.
None of the calls to the OTP server set a timeout, so a hanging OTP
server blocked a web worker indefinitely. Each call now times out after
OTP_TIMEOUT seconds (default 10).
The OTP server variables were read straight from the environment in
views.py. They are now OTP_* settings next to everything else, under
the same environment variable names, so existing deployments are
unaffected.
Remove LDAP_SERVER and LDAP_USERS_DN, which nothing used.
env.sample moves to the repository root (where .env is read from) and
lists every variable settings.py reads, including defaults and which
values are secrets.
Containers should log to stdout. The RotatingFileHandler wrote to
logs/debug.log, which is lost with the pod and crashed startup when
the directory was missing.
All logging now goes to a console handler. LOG_LEVEL sets the root
level (default INFO), DJANGO_LOG_LEVEL sets django.* (default INFO),
and LDAP3_LOG=1 turns on ldap3 protocol logging. ENABLE_DEBUG_LOG and
MODULES_TO_LOG are gone.
EMAIL_HOST, EMAIL_PORT and EMAIL_USE_TLS are deprecated in 6.1 in favour
of the MAILERS setting. The same environment variables now feed the
default SMTP mailer, so existing deployments keep working. Also adds
optional EMAIL_HOST_USER, EMAIL_HOST_PASSWORD and EMAIL_TIMEOUT
(default 10s; previously no timeout).
LDAPSERVER was documented as a space separated list, but the whole
string went into a single ldap3.Server. Logins worked (python-ldap
accepts lists), while registration and every write would have failed
with more than one URI.
settings.LDAP_SERVERS is now the split list, used through one ldap3
ServerPool per process: requests go to the first reachable server, a
server that is down is skipped for 60s, connections time out after 5s,
and with all servers down the pool gives up after two passes instead
of retrying forever.
python-ldap is a C extension that needs the OpenLDAP headers to build
(and needed a workaround in the Dockerfile). ldap3, which the app
already used for all writes, is pure Python.
dal.backends.LdapBackend keeps the django-auth-ldap behaviour: search
the uid in every LDAPSEARCH base as the admin DN, require exactly one
match, log in with the user's DN, and mirror givenName, sn and mail into
the local Django user. Empty passwords are rejected (LDAP treats them
as anonymous logins, which succeed).
LDAP_USE_TLS now applies StartTLS to every connection, including the
admin connection used for writes, not just logins.
Remove USE_L10N; localized formatting is always on from 5.0. Replace
User.objects.make_random_password() (removed in 5.1) with a
secrets-based generator for the create API's initial password, using
the same unambiguous alphabet and 16 characters instead of 10.
Step 1 of the upgrade (Python 3.9). Dependencies are bumped to their
newest versions compatible with Django 3.2.
DEFAULT_AUTO_FIELD is set to AutoField, which matches the existing
tables, so no migration is needed and the W042 warnings go away.
The highest uidNumber was cached in dal/ldap_max_uid_file. That breaks
with more than one replica and is lost on every container restart. The
uniqueness check behind it searched base '' (the root DSE), which
OpenLDAP never matches, so nothing caught a stale file. The error path
also referenced a setting that does not exist.
Now a paged search over ENTIRE_SEARCH_BASE finds the highest uidNumber
on each activation. Numbering is unchanged: the next number is above
both LDAP_DEFAULT_START_UID and every existing uidNumber.
LDAP_MAX_UID_FILE_PATH is no longer used.