Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
6.9 KiB
REPORT — 2026-09-29: demo-hp's two open default logins closed; the setup gate SPIKED, PASSED and BUILT; every hard-coded default replaced
Architecture read first: 01-topology-and-trust.md §5 (trust boundaries — it had no statement of who may reach an app;
now it does), 04-control-plane-authorization.md (control plane only — nothing about app reachability), 09 §3
decisions 45–46, app-catalog-felhom.eu/FIRST-ADMIN.md. Controller v0.280.0 (one release). Floor 0.280.0; both demo
boxes run it. Evidence: documentation/audits/login-gate-2026-09-29/ (A, B, C, D).
The Parts
| Part | Step | State | Note |
|---|---|---|---|
| — | rulings recorded first | done | decision 46 + the demo-login ruling in 09 §3 before any work |
| A1 | read-only: defaults still work | done | bookstack and calibre-web: default signed in (302 → /), wrong refused (A1) |
| A2 | change through the app's own route | done | bookstack artisan bookstack:create-admin --initial; calibre-web cps.py -s with a special-character password. Default refused, new signs in, wrong refused, through each app's own login form |
| A3 | save in ~/.config/credentials |
done | four lines appended in the file's format (DEMO_HP_BOOKSTACK_USER/_PW, DEMO_HP_CALIBRE_USER/_PW), file 0600, read back equal; values in no evidence, commit or log (value scan before every evidence commit) |
| A4 | pages stop warning | done, changed | known_login.go could NOT learn of a manual change, and bookstack's page had ALREADY stopped warning while its default worked (R-710). Built "I changed it" in v0.280.0 (an honest household/operator record, not a faked after_install result) and pressed it on demo-hp: both sentences gone (A3) |
| B1 | dashboard session on the app's address | done | the cookie is host-only — it cannot be seen there; a redirect handshake instead, nothing widened |
| B2 | stranger / household in a browser | done | stranger: gate page or 401, never the app; household: 0.2 s, no extra step; one sign-in on a phone not signed in |
| B3 | "setup done" probes | done | n8n and immich measured flipping; 14 of 34 have a probe (then 2 measured, 12 upstream), 20 need the button |
| B4 | after the gate opens | done | immich's phone-app API (Bearer) and n8n's API unchanged; the gate stopped and removed |
| B5 | cost | done | ~2 ms; controller down → gated apps 500 (closed); phone app first → 401 until the web setup |
| B6 | exit test | PASSED | written before any build: B/B-VERDICT.md |
| C1 | controller v0.280.0: the gate | done | written before the first start (a failed write refuses the install); probe loop 20 s; button; restart keeps it; restore keeps the record; kept data never gates. hu + en copy, informal, no "please"; parity green |
| C2 | tests + red-proofs | done | TestSetupGate_* (stacks 9, web 4 + page 2), all green; RP1–RP12 each seen failing on an assertion |
| C3 | live on 3–5 class-4 apps | done (4) | immich (phone app), n8n, audiobookshelf (probe), uptime-kuma (button): ~530 stranger polls during the installs, 0 app answers before each gate opened; the probes opened 3 gates ≤ 27 s after setup; the press opened the 4th; a controller restart kept the 4th closed and the household's pass valid |
| C3 | restore does not re-gate | not live | proven by test only (TestSetupGate_ARestoreKeepsTheRecord): a per-app backup on 9202 needs a whole-box backup, which drills must not run (R-648) |
| C4 | design record + decision 46 outcome | done | 01 §5 "who may reach an app, and through what"; 09 §3 decision 46 outcome |
| C4 | floor | done | 0.280.0 after every live proof passed; both demo boxes delivered |
| D1 | mealie, wger | done | after_install, password as sys.argv[1]; fresh-install proof: default refused, generated signs in, wrong refused. Found and fixed R-712 (wger refused every browser sign-in: CSRF) |
| D2 | calibre-web | done | generate: password:24:special (server + install page; 2000 JS runs in node, 0 bad); cps.py -s as abc; fresh-install proof as D1 |
| D3 | romm, zipline notes | done | removed (hu + en); first steps say create the admin; romm's page on demo-hp no longer warns |
| D4 | R-708, R-709 | done | grafana ${…:?…} (compose refuses empty/unset); password fields off the page with a reveal eye (live: the value is not in the HTML) |
| D5 | tests + red-proofs + live | done | RP13–RP16; live on 9202 |
Claims in the brief that turned out wrong (or right), named
- "No class-4 app is reachable except through traefik" — right (read: only crafty-controller publishes ports, and it is class 1). Nuance: wanderer publishes a SECOND host (its database admin) — a gate must cover every host an app's labels publish; the built gate does.
- "Most class-4 apps expose a setup-done status" — wrong: 14 of 34 (now 3 measured, 11 upstream); 20 need the household's button.
- "The dashboard session can be checked on an app's subdomain without widening it" — wrong as stated: the cookie is host-only and never reaches an app host. A redirect handshake (a 60-second, one-use, host-bound token minted on the dashboard's own host) does the check instead — and nothing is widened.
- "immich's phone app works unchanged after the gate opens" — right, measured through its API (login → Bearer →
/users/me,/server/ping,/server/version, all 200); the real phone app was not run. - "
known_login.gocan learn of a manual password change" — wrong: it knew onlyafter_installrecords. And worse than the brief assumed: bookstack's page on demo-hp had already stopped warning while its default still worked (an absent record read as "not run yet" for ever — R-710). Fixed; proven live.
Also found
- The household's "Done" press trusts the household. The uptime-kuma proof pressed it without doing the setup, and the app then answered anyone. The page tells the household to press after the setup; nothing checks it (no probe exists for uptime-kuma over HTTP). Recorded in decision 46's outcome.
- A security review of the drill commit flagged mealie's and wger's commands (the password pasted into Python code). Fixed before the live catalog (argv). claper's Elixir command has the same shape → R-713.
Rows
Opened: R-710 (closed the same day), R-711, R-712 (closed), R-713. Closed: R-708, R-709, R-710, R-712. Narrowed: R-707 (30 of 37 left). Register 346 → 350 rows.
Teardown
Machines: 9202 — the spike's container, file and two apps removed; the seven Part C/D apps removed through the product
(immich, audiobookshelf and calibre-web kept their scratch-drive folders — R-442's refusal, as in earlier sessions);
no gate file left; the spike's python and node images removed; back on the live catalog; the drill catalog reset to
live main. demo-hp 9201 — the two admin passwords changed and the two "I changed it" records (the operator's
ruling); nothing else. demo-felhom — nothing. Host: nothing. Hub: floor 0.280.0. ep0: untouched.