Files
Project-SDE-WP-Suite/DEPLOY-login-portal.md
Cody Schaefer dc0cee240e T10.8 D13/D14 - documentation matches what the code now does
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>
2026-08-24 08:26:31 -05:00

155 lines
6.7 KiB
Markdown

# 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 <user> --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 </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:
```bash
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` — drops `users.password_hash`.