Files
felhom.eu/REPORT.md
T
admin 592818492c hub v0.66.0 + ISO v1.20.0: customer self-bind (R-27 slice 1)
Let a customer bind their own freshly-installed appliance without the
operator: operator "Send self-bind link" mints a 7-day tokenized
capability link, emailed (Hungarian, sibling sender) to the customer, who
opens a public /bind/<token> page and proves two factors — the console
pairing code shown on the box screen + their retrieval passphrase — and
the hub stages the bind via the same BindAppliance (provenance
customer_selfbind). The box's ~30s appliance poll delivers.

Viktor's three rulings verbatim: console pairing code (no appliance list
ever rendered), operator-sent tokenized link, 5-attempt lockout ->
"call support". Wrong code == wrong passphrase (one generic failure, no
oracle, both factors compared unconditionally); expiry falls back to
operator-bind unchanged.

THE TRAP: one public prefix /bind/, exempt from auth+CSRF at both /login
gate sites via a single isPublicBindPath predicate (tight trailing-slash
match; ServeMux ..-cleans; handler rejects '/' in token). 9 tests
(Scenarios A-F + F1/F2); 4 red-proofs verified red-then-green (lockout,
oracle, widened-prefix, single-active). GC verdict: no appliance GC ->
the 7-day TTL stands alone. Controller/agent untouched; R-27b deferred.

Green: full hub build/vet/test (17 ok) + bash -n + hub confirm gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
2026-07-17 23:56:53 +02:00

81 lines
5.4 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
Filled in after deploy — see the CONTEXT.md live-state note.
### 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).