Files
felhom.eu/REPORT.md
T
admin cfdcb50bc2 docs: record hub 0.66.0 self-bind live validation (R-27 slice 1)
Live through the real ingress: public /bind/ renders logged-out with the
no-oracle expired state (200-not-500 proves selfbind_tokens migrated);
gate intact (/ and /hosts -> /login); CSRF exemption is /bind/-only
(POST /bind/ no-CSRF 200 vs POST /customers/x/block no-CSRF 302).
Operator-minted full walk + new-ISO console banner remain operator/R-1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
2026-07-18 00:01:51 +02:00

94 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# felhom.eu — task reports
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md); the scripts history lives in [scripts/CHANGELOG.md](scripts/CHANGELOG.md).
## Hub v0.66.0 — Customer self-bind (R-27 slice 1) — 2026-07-17
A customer can now bind their **own** freshly-installed appliance without the operator. The operator
sends a **7-day tokenized capability link** by email; the customer opens a **public, Hungarian**
`/bind/<token>` page (no login — the URL token IS the capability), enters the **console pairing code**
shown on the box screen plus their **retrieval passphrase**, and the hub stages the bind through the
same `BindAppliance` the operator uses. The box's ~30 s appliance poll then delivers credentials and
day-0 install proceeds. The controller and agent are **untouched**.
Viktor's three rulings, honoured verbatim:
1. **"Only their own visible"** → possession is proven by the console pairing code; **no appliance
list is ever rendered** on any public surface.
2. **First-box entry** → an operator-sent tokenized capability link over Hungarian email (claim-engine
delivery pattern, a **sibling** sender in `web/`+`notify/` — deliberately NOT routed through the
claim engine).
3. **Lockout after 5 failed attempts** → the token locks; the page says *call support*.
### Components
| Part | Area | What |
|---|---|---|
| 1 | store + api + bootstrap | `pairing_code` on `appliance_registrations`; additive `pairing_code` in the register response; stable across idempotent re-register; console banner (`felhom-bootstrap.sh`, ISO **v1.20.0**) |
| 2 | store + notify + web | `selfbind_tokens` table (sha256-at-rest, single-active/customer, 5-attempt lock, one-shot); operator **"Send self-bind link"** button; Hungarian email (sibling sender); F1/F2 honesty |
| 3 | web | public `/bind/<token>` page; **THE TRAP** auth+CSRF exemption via one `isPublicBindPath` predicate; two-factor unconditional compare; provenance event `customer_selfbind`; own rate limiter |
| 4 | tests | Scenarios AF + F1/F2 (9 tests) + **4 red-proofs** |
| 5 | docs + deploy | this REPORT, CHANGELOGs, CONTEXT, capability-map, ROADMAP; build 0.66.0, manifest bump, sync; live validation |
### Security properties (spec §9)
- **THE TRAP (§9.2):** exactly one new public prefix `/bind/`, exempted from operator auth and CSRF at
the two gate sites `/login` occupies, through a **single** `isPublicBindPath` definition. Matched
tightly — trailing slash (no `/bindsecret` sibling), the `http.ServeMux` `..`-cleans before the
handler sees the path, and the handler rejects any token containing `/`. Scenario E asserts a public
link renders logged-out while `/`, `/hosts`, `/customers/…`, `/configuration`, `/offsite` still
redirect to `/login`; Scenario D is its companion.
- **No oracle:** an unknown token folds into "expired" (no *was-this-real* signal); wrong code and
wrong passphrase yield one byte-identical generic failure (both factors compared unconditionally
before deciding); the page renders no appliance data in any state.
- **Custody:** `sha256(token)` at rest, prefix-only in logs, raw token never in logs/events; the
passphrase is never logged/echoed/persisted; only attempt COUNTS are logged (`attempt N/5 … <8hex>…`).
- **No customer-login system** was built; the capability link is the whole auth model. A cross-site
POST without both secrets only burns attempts (accepted + documented).
### GC verdict (spec §3)
There is **no appliance-staleness garbage collection** in the hub — `applianceStaleAfter` (7 d) is a
DISPLAY badge only; `pruneAll` and `PurgeExpiredLogBundles` touch host reports and log bundles, not
appliances or self-bind tokens. The **7-day token TTL stands alone** and needs no reaper: single-active
per customer means at most one row per customer, a re-mint deletes the prior row, and an expired row
simply reads as expired (no security or storage pressure).
### Verification
- **Green gate:** `go build ./... && go vet ./... && go test ./...` — all hub packages pass (17 ok),
including 9 new self-bind tests. `bash -n` clean on both ISO scripts. Hub confirm gate OK.
- **Red-proofs (all confirmed red-then-green):**
- lockout removed (`locked = false`) → **C1** red.
- oracle introduced (distinct state on wrong code) → **B** red.
- `/bind/` prefix widened (drop the slash) → **E** red (`/bindsecret` leaks past auth).
- single-active DELETE dropped → **C4** red (the prior link still resolves).
- **D2 regression evidence:** with an operator password configured, `/`, `/hosts`, `/customers/acme`,
`/configuration`, `/offsite` all `302 → /login` through `RequireAuth`, while `/bind/<token>` renders
`200` — proving the exemption is tight (`TestSelfBind_D_AuthGateIntact`, `TestSelfBind_E_TheTrap`).
### Live validation (hub 0.66.0 live on k3s, through the real `hub.felhom.eu` ingress, 2026-07-18)
- **Public page renders logged-out, no oracle:** `GET /bind/<garbage>``200` rendering the Hungarian
*"Doboz összekötése … érvénytelen vagy lejárt"* page (an unknown token folds into the generic
expired state — no *was-this-real* signal). The `200` (not `500`) also **proves the `selfbind_tokens`
table migrated live** — `SelfBindTokenByHash` queried it successfully (a missing table would 500).
- **Auth gate intact (THE TRAP is tight live):** `GET /` and `GET /hosts``302 → /login`.
- **CSRF exemption is `/bind/`-only, live:** `POST /bind/<garbage>` with **no** CSRF/session → `200`
(exempt at gate site 1), renders the generic failure; the control `POST /customers/acme/block` with
no CSRF → `302 → /login` (auth fires first — the exemption did not widen).
- **Clean startup:** migrate() ran without error; `Listening on :8080`, all checkers seeded.
**Pending (NOT-live, as anticipated):** the operator-minted full customer walk (the "Send self-bind
link" button and the customer's real two-factor success are behind the operator password gate → CC
cannot log in) and the new-ISO console pairing-code banner on a physically re-imaged box — both fold
into the supervised rehearsal (R-1).
### Not in this slice (deferred)
- **R-27b** — the controller's second-box flow (a dismissable "bind another box" prompt + bind-later
under settings). Mechanism sketched; not built. The controller/agent were not touched.
- **Multi-box per link** — one link binds one box (single-active, one-shot). Binding several boxes for
one customer = repeated operator sends. Noted on the ROADMAP.
- Attaching the appliance's stored SSH host key to the host record on bind (no clean hand-off surface
today — same open item as slice C).