# 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 with `manage_users create-admin --password …`. All of > that is gone. The app no longer stores a password of any kind, `create-admin` no > longer exists, and following the old steps will fail at the first command. The > superseded design is recorded in `docs/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: 1. two environment variables, 2. a **rebuild** of the stack (not just a restart), 3. one network check, and 4. 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/` and `COPY server/`). A plain restart > will **not** pick up the new code — the images must be **rebuilt** from latest `main`. --- ## ⚠ 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 `main` and redeploy **with image rebuild enabled**. - The new Python dependency (`ldap3`) is in `requirements.txt` and installs during the rebuild. - A database migration drops the `users.password_hash` column. 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?** ```bash 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: ```bash docker compose exec api openssl s_client -connect prime.local:636 -CAfile /app/server/certs/prime-ca-chain.pem &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: ```bash docker compose exec api python -m server.manage_users promote ``` It asks for **your** domain username and password, binds to confirm who you are, and prints `: 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` — drops `users.password_hash`.