hub v0.62.0 + scripts v1.19.0 — R-21 slice C: the universal secret-free ISO

A generic ISO carries NO customer secret. The box registers itself at the hub
as an unclaimed appliance; the operator binds it to a customer; the hub delivers
the customer-id + retrieval passphrase ONCE; day-0 completes via the slice-A path.

Hub (v0.62.0):
- store/appliance.go: appliance_registrations keyed by (uuid, mac_set) — MAC set
  is the tiebreaker (duplicate SMBIOS UUIDs); token stored as sha256 only.
  Idempotent register (sticky-discard), atomic one-shot delivery, bind/discard.
- api/appliance.go: POST /appliance/register (the one unauth endpoint, per-IP
  rate-limited, 256-bit token); GET /appliance/poll (404 no-oracle / 204 unbound
  / 200 deliver-once / 410 delivered). Passphrase read live, never logged.
- web/appliances.go: Hosts-page "Unclaimed appliances" section + BIND (customer
  picker, host count display-only) + DISCARD; SSH host-key fingerprints; events.
- Red-proofs: one-shot delivery + register idempotency (both proven red);
  404-no-oracle, sticky-discard, bind staging, render. Green + confirm gate.

Scripts (v1.19.0):
- felhom-bootstrap.sh: ONE unit, TWO modes. Direct (env has customer/passphrase)
  = slice-A path, byte-identical, only branched around. Pairing (generic) =
  register + poll (RestartSec=30 is the poll timer); on delivery write the env
  0600 and fall through to direct. Secrets + token shredded on success.
- build-felhom-iso.sh --pairing: generic secret-free ISO, -generic filename,
  manifest mode=pairing. profiles/generic.profile (new).
- test/bootstrap-modes.sh: Scenario D (direct = zero appliance calls) + pairing
  register/poll + delivery handoff — all green in a debian container.
This commit is contained in:
2026-07-17 15:07:31 +02:00
parent 3172df1927
commit 36c5cd5fdf
16 changed files with 1531 additions and 90 deletions
+28
View File
@@ -1,5 +1,33 @@
# Felhom Hub — Changelog
## v0.62.0 — R-21 slice C: the universal ISO — unclaimed-appliance registration + operator bind + one-shot delivery (2026-07-17)
The hub half of the universal, **secret-free** bare-metal ISO. A box booted from the generic ISO
registers itself as an UNCLAIMED APPLIANCE; the operator binds it to a customer on the Hosts page; the
hub delivers the customer-id + retrieval passphrase on the box's next poll, ONCE. The distributed ISO
carries no customer secret (§4.4).
- **Store (`internal/store/appliance.go`, new):** `appliance_registrations` keyed by **(uuid, mac_set)**
— serials are unusable (N100 DMI "Default string") and cheap boards duplicate SMBIOS UUIDs, so the
MAC set is the tiebreaker (same uuid + different mac-set = distinct appliance). `token_hash` = sha256
of the appliance token (the token itself is never stored). `RegisterAppliance` (idempotent upsert;
sticky-discard), `ApplianceByToken`, `BindAppliance`, `MarkApplianceDelivered` (atomic one-shot
bound→delivered), `DiscardAppliance` (invalidates the token), `ListUnclaimedAppliances`. The table's
own timestamps ARE the pre-bind provenance (no customer to scope an events row to yet).
- **API (`internal/api/appliance.go`, new):** `POST /api/v1/appliance/register` — the ONE
unauthenticated endpoint, per-IP rate-limited, returns a random 256-bit appliance token. `GET
/api/v1/appliance/poll` (Bearer token): unknown/discarded → **404** (no oracle), unbound → **204**,
bound → **200** + credentials (consumed once), delivered → **410**. The passphrase is read live from
`customer_configs` (plaintext, as the day-0 command already needs it) and never logged.
- **Web (`internal/web/appliances.go`, new):** the Hosts page grows an "Unclaimed appliances" section
(uuid, MACs, hw, **SSH host-key fingerprints**, first/last seen, stale >7d badge) with **BIND**
(customer picker showing host counts — display only, never a gate) and **DISCARD**. Bind stages the
delivery + emits `appliance_bound`; delivery emits `appliance_credential_delivered`.
- **Red-proofs (run-fail-revert):** the one-shot delivery (defeat the bound→delivered flip → second
poll re-delivers the passphrase → FAIL) and register idempotency (drop the upsert → duplicate/UNIQUE
violation → FAIL), both proven red then restored; plus 404-no-oracle + sticky-discard, bind
staging/refusal, and the render test. Green: `go build/vet/test`; hub confirm gate OK.
## v0.61.0 — Customer RESET: the middle lifecycle tier (2026-07-17)
One operator action returns a customer to **pre-first-install**: every OPERATIONAL trace dies (offsite