DEPLOY-login-portal.md was the most wrong and is rewritten. It described taking a username/password portal live - bcrypt, and a first admin created with `create-admin --password`. Every command in it now fails. It keeps its filename and carries a note saying what it replaced, because an admin holding the old copy needs to know why the steps stopped working rather than concluding the deploy is broken. New content leads with the warning that there is no break-glass, and puts verification BEFORE announcing the deploy - the log line, the certificate check that binds nothing, then a real sign-in. DEPLOYMENT.md: AUTH_RESET_* replaced with the LDAP variables; the users table row no longer claims a password_hash column; "Self-service password reset" replaced by a section saying there isn't one and pointing at Okta. New "Domain authentication" section covering the three things that are not obvious - why prime.local and never a DC or an IP, why the CA bundle is not a certificate issued to this app (with the thumbprints and a Get-ChildItem line to rebuild it), and why the outbound network stopped being optional - plus the lockout arithmetic written out so the next person to raise AUTH_MAX_ATTEMPTS sees the constraint rather than a magic 2. server/README.md: endpoint table drops /api/auth/password and gains the role route; the login-portal section becomes domain authentication; create-admin becomes the two-step bootstrap (sign in, then promote). CLAUDE.md: a new "authentication rules" section beside the token rule, for the same reason that one exists - four things that look like tidying-up if you do not know why. The empty-password guard that must run before bind(), CERT_REQUIRED with an explicit CA file, AUTH_MAX_ATTEMPTS being arithmetic rather than taste, and connecting to the domain name rather than a DC. Plus: no break-glass, and roles are local - never read a role from AD. Closed three done-when boxes that were open rather than ticked: T10.8 all of them T10.9 promote/demote verified against a real bind (Aug 24), not a stub T10.3 the Postgres round trip, on postgres:16-alpine Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.7 KiB
Deploy: Work Package Suite — domain sign-in (D13)
Instructions for the Portainer admin to take domain authentication live. No prior context needed.
Repo: Project-SDE-WP-Suite (primegit) — changes are merged to main.
This document replaced an earlier one. Until Aug 24 2026 it described taking a username/password login portal live: bcrypt hashes, an
AUTH_SECRET_KEY, and a first admin created withmanage_users create-admin <user> --password …. All of that is gone. The app no longer stores a password of any kind,create-adminno longer exists, and following the old steps will fail at the first command. The superseded design is recorded indocs/waves/decisions-2026-08-21.md(D13).
What changed: signing in is now an LDAPS bind against prime.local. People
use their Windows password. The suite stores no credential, there is no password
reset, and accounts create themselves on first sign-in.
Going live needs:
- two environment variables,
- a rebuild of the stack (not just a restart),
- one network check, and
- promoting the first admin.
Why a rebuild (not a restart): both the nginx/webserver and api images bake the code in at build time (
COPY html/andCOPY server/). A plain restart will not pick up the new code — the images must be rebuilt from latestmain.
⚠ Read this before you start
There is no break-glass account. If the directory is unreachable, the CA bundle path is wrong, or the required group is misconfigured, nobody can sign in — including you. That was a deliberate decision, not an oversight. Recovery is to fix the configuration and restart; there is no local password to fall back on.
So: do step 3 before you tell anyone the deploy is done.
1. Add environment variables to the stack
In the stack's Environment variables section:
| Name | Value | Notes |
|---|---|---|
AUTH_SECRET_KEY |
a long random string | Required. Unchanged — still signs the session cookies. Keep the existing value; changing it signs everyone out. |
LDAP_REQUIRED_GROUP |
CN=Prime Employees,OU=Prime Distribution and Security Groups,DC=prime,DC=local |
AD group required to sign in. A full DN is best — it skips a directory lookup. Nested groups count. Leave empty to allow any domain account. |
AUTH_SESSION_HOURS |
12 |
Optional. Unchanged. |
You do not need to set LDAP_HOST, LDAP_DOMAIN or LDAP_CA_FILE. Their
defaults are correct for this estate, and the CA bundle ships inside the image.
Do not point LDAP_HOST at a domain controller's name or at an IP address. It is
set to prime.local on purpose: every DC's certificate carries that name in its SAN,
so the domain name both validates and load-balances across all six DCs. An IP fails
certificate validation outright, and the only way to force it through is to switch
validation off — which would let anyone on the network intercept domain passwords.
AUTH_RESET_MINUTES and AUTH_RESET_COOLDOWN_SECONDS can be deleted if present.
They configured the password-reset email, which no longer exists.
2. Pull latest main, rebuild, and redeploy
- Pull the latest commit on
mainand redeploy with image rebuild enabled. - The new Python dependency (
ldap3) is inrequirements.txtand installs during the rebuild. - A database migration drops the
users.password_hashcolumn. It runs automatically at container start. Every account, role and project membership is preserved — it removes one column, not any rows.
3. Verify BEFORE announcing it
a. Did the API start at all?
docker compose logs api | grep -i "LDAP auth"
You want:
LDAP auth enabled — ldaps://prime.local:636, domain prime.local, CA /app/server/certs/prime-ca-chain.pem, required group: CN=Prime Employees,…
If it says LDAP auth DISABLED, stop — nobody will be able to sign in. The message
names the reason.
b. Can the container actually reach a domain controller? This opens a TLS session and validates the certificate without binding, so it touches no account and cannot contribute to any lockout:
docker compose exec api openssl s_client -connect prime.local:636 -CAfile /app/server/certs/prime-ca-chain.pem </dev/null 2>&1 | grep "Verify return"
Want Verify return code: 0 (ok). If you get a connection error, the api container
is missing the outbound network — internal has no default gateway and blocks the
LAN as well as the internet. If you get 62 (hostname mismatch), something is
pointing at an IP instead of prime.local.
c. Sign in. Use your own Windows username and password.
4. Promote the first admin
Roles are stored locally and are not read from AD, so someone has to be made an admin once. Sign in first — that creates your account — then:
docker compose exec api python -m server.manage_users promote <your-sAMAccountName>
It asks for your domain username and password, binds to confirm who you are, and
prints <user>: project_user -> admin.
Other commands: list (needs no credential), demote, disable, enable.
create-admin, create and reset-password no longer exist.
After that, admins manage everyone else from the in-app Admin → User administration page. No further shell access needed.
What people will notice
- They sign in with their Windows password, not an app password.
- "Forgot password?" now goes to
https://primecontrols.okta.com/. The app cannot reset a password it does not hold. - The Change password item is gone from the top-right menu.
- Anyone in the required group can sign in without being added first — their account is created automatically. They will see no projects until an admin grants access, which is intentional. New accounts appear in the Admin console and each one is recorded in the audit log.
- Two wrong passwords and the app stops trying for a while. That is deliberate: every failed attempt is a real domain bind and counts against the AD lockout policy, so the app stops well short of locking anyone out of Windows.
Reference — what's in this release
server/ldap_auth.py— the LDAPS client: bind, nested-group check, certificate validation.server/app.py—login()binds instead of comparing a hash; password endpoints removed.server/auth.py— sessions and roles only; no hashing, no reset tokens.server/certs/prime-ca-chain.pem— the CA bundle that validates the DC certificate.server/manage_users.py—promote/demote, each requiring a domain bind.html/login.html,login.js— one view; "Forgot password?" points at Okta.- Migration
b7e4f1a20c93— dropsusers.password_hash.