server/ldap_auth.py: simple bind to ldaps://prime.local:636 as
sAMAccountName@prime.local, nested-group membership via the
LDAP_MATCHING_RULE_IN_CHAIN extensible match, and a selftest() that validates
the DC certificate without binding so it can never contribute to a lockout.
Verified against the live domain, not just reasoned about:
selftest() to prime.local -> ok, "certificate validates"
selftest() to 192.168.3.37 -> refused, untrusted (no IP SAN)
empty / whitespace password -> empty_input, with Connection nulled out
so any call to bind() would have raised
missing CA file -> unconfigured, is_config_problem=True
Tls.validate -> ssl.CERT_REQUIRED, explicit ca_certs_file
Three things here are load-bearing and commented as such at the call site:
- The empty-password guard runs BEFORE bind(). An LDAP simple bind with an
empty password is an anonymous bind and it SUCCEEDS, so without the guard a
blank password authenticates as whatever username was submitted.
- No `version=` pin on Tls. An earlier draft of this file pinned
PROTOCOL_TLSv1_2, which would have silently downgraded every connection from
the TLS 1.3 these DCs actually negotiate.
- Retries cover connect failures only. A rejected credential returns
immediately, because every failed bind counts against the domain lockout
policy and this endpoint must not become a way to lock people out of Windows.
The trust anchor is server/certs/prime-ca-chain.pem - PRIME CONTROLS ROOT CA
plus ISSUING CA 1, public certificates with no private key, checked in because
they are public and long-lived (2051 / 2036). The system trust store is
deliberately not used: it currently trusts five other self-signed CAs on this
estate. LDAP_CA_FILE overrides the path for a mounted bundle.
docker-compose.yml: the `outbound` network is no longer optional. Its comment
said to detach it if you were not using the Micron asset picker; doing that
now breaks every sign-in, since `internal` has no default gateway and
therefore no route to prime.local:636.
Not yet verified, and called out rather than assumed: the nested-group case
needs a real group with a nested member, and the in-container
`openssl s_client -CAfile` check needs the stack. Both are T10.1 done-when
boxes still open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>