# 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/` 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/` page; **THE TRAP** auth+CSRF exemption via one `isPublicBindPath` predicate; two-factor unconditional compare; provenance event `customer_selfbind`; own rate limiter | | 4 | tests | Scenarios A–F + 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/` 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/` → `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/` 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).