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

6.7 KiB
Raw Blame History

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; the scripts history lives in 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 liveSelfBindTokenByHash queried it successfully (a missing table would 500).
  • Auth gate intact (THE TRAP is tight live): GET / and GET /hosts302 → /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).