controller: customer-claim password gate v0.122.0 (closes DRILL-day0-vm F-4/F-5)

The customer sets + owns the dashboard password via a hub-emailed one-time
claim code. An unclaimed box (code hash present, no password) serves ONLY the
claim page — every other route → claim page (302) or 401, so a Day-0 box is
never open on the internet. A set password disables the gate (auth wins).
Reset rides the same code engine (login "Elfelejtett jelszó"). Legacy-open
(no password, no hash) shows a red transition banner until the hub delivers a
hash. Report ACK caches the code state idempotently by generation; report
carries claimed (set-only). --print-reset-code root escape hatch. Requires
hub v0.50.0. Gate-coverage signature test + 4 red-proofs proven.
This commit is contained in:
2026-07-12 18:42:39 +02:00
parent dec6fef20d
commit 3cf49c7fd5
18 changed files with 1117 additions and 1 deletions
+27
View File
@@ -1557,6 +1557,33 @@ self_update:
### 8. Authentication & Settings
#### Customer-claim gate (`internal/web/claim.go`, v0.122.0 — closes DRILL-day0-vm F-4/F-5)
The dashboard password is **customer-owned**, set through a one-time claim code the hub emails to
the registered address (no operator-set path, no open-until-set window). This closes the fresh-box
race where a new `felhom.<domain>` cert appears in CT logs minutes before any password exists.
- **States** (precedence): a SET password (settings→config) always wins — the gate never shows.
Else a delivered **claim-code hash + not-yet-claimed** → GATED: every route serves the claim
page (`302 → /claim`) or `401` JSON (API); only `/claim*`, `/static/*`, `/api/health` pass. Else
(no password, no hash) → **legacy-open** with a red transition banner until the hub delivers a
hash (transitional only, never the fresh-box state).
- **Claim/reset flow**: `GET /claim` (code + new password ×2, min 12) → `POST /claim` verifies the
code (bcrypt match AND generation not yet consumed AND ≤ 72 h old), sets the customer's password
via `settings.SetPasswordHash`, marks `Claimed` (set-only), consumes the generation (single-use),
invalidates sessions, issues a fresh one. `POST /claim/request-new-code` (the "Új kód kérése" /
login-page "Elfelejtett jelszó") forwards to the hub, which emails a fresh code to the
registered address only. Reset rides the same page (a claimed box reaches `/claim` pre-auth).
- **Anti-brute-force**: per-source + global counter, 5 failures → 15-minute lockout (both scopes),
raising the allowlisted `claim_lockout` event. Pre-auth CSRF is an HMAC over `web.session_secret`
(fixes the CTRL-007 bare-double-submit weakness).
- **Delivery**: the hub bakes `web.claim_code_{hash,generation,issued_at}` into the Day-0
controller.yaml (gate-from-first-boot) and serves the freshest state in the report ACK
(`report/claim_sync.go` caches it idempotently by generation — newer advances, same/older/nil
never rewrites, a hub outage never clears). The report carries `claimed` (hub ingests set-only).
- **Escape hatch**: `felhom-controller --print-reset-code` prints a one-time local code (generation
above cached/baked/consumed); the same gate consumes it. Root-gated by `docker exec` reachability.
#### Session Auth (`internal/web/auth.go`)
- bcrypt password verification with configurable source priority: `settings.json``controller.yaml` → no auth (open access)