6.5 KiB
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.
R-21 slice C — the universal secret-free ISO (hub v0.62.0 + scripts v1.19.0) — 2026-07-17
Baselines: hub v0.61.0 → v0.62.0; scripts v1.18.0 (3172df1) → v1.19.0. Commits 36c5cd5
(code), 9964ae6 (manifest). Hub v0.62.0 LIVE on k3s (Synced/Healthy). R-21 now shipped A+B+C.
What shipped
The last R-21 slice: a generic, secret-free ISO. Blank box → unattended install → the box
registers itself at the hub as an unclaimed appliance → the operator binds it to a customer → the hub
delivers customer-id + retrieval passphrase once → day-0 completes via the existing slice-A path.
The distributed ISO carries no secret (§4.4). Slice-A direct mode (env-baked) survives unchanged.
Registration payload (as implemented) + token model
POST /api/v1/appliance/register body: {uuid, macs[], ssh_host_pubkeys[], hw{product, cpu, mem_kb}}.
Keyed by (uuid, mac_set) — the N100 DMI verdict (serials = "Default string") + cheap-board duplicate
SMBIOS UUIDs make the MAC set the tiebreaker (same uuid + different mac-set = distinct appliance).
Token model: a random 256-bit appliance token per registration, returned once, stored only as
sha256 (token_hash); it is the box's sole pre-day-0 credential, never logged. Invalidated on
discard (token_hash blanked + status sticky) and after consume. GET /api/v1/appliance/poll (Bearer
token): unknown/discarded → 404 (no oracle) · unbound → 204 · bound → 200 + creds (consumed
once) · delivered → 410. The passphrase is read live from customer_configs (plaintext, as the
day-0 command already requires) and never logged. register is the ONE unauthenticated endpoint, per-IP
rate-limited (the ingress already geo-restricts to HU).
Red-proofs (run-fail-revert, all recorded)
- One-shot delivery (C): defeat
MarkApplianceDelivered's bound→delivered flip → the second poll re-delivers the passphrase (200) instead of 410 →TestAppliancePoll_OneShotAnd410FAILS → restored. - Register idempotency (A): drop the
(uuid, mac_set)upsert branch → the re-register hits the UNIQUE backstop (500) →TestApplianceRegister_IdempotentFAILS → restored. (The UNIQUE index is the load-bearing backstop; the upsert is the graceful update.) - Direct-mode regression (D):
test/bootstrap-modes.sh— a direct env drivesrun_directand makes ZERO/appliance/*calls (fake hub records none); the pairing code path is provably not entered. Plus 404-no-oracle + sticky-discard, bind staging/refusal + no-host-count-gate, and the render test.
Deploy verification
Hub 0.62.0 rolled out (Synced/Healthy, image confirmed). Live smoke through the public ingress:
POST /appliance/register → 200 with a 256-bit token; poll (valid, unbound) → 204; poll (bogus
token) → 404 (no oracle). (The smoke-test-uuid record is CC test litter — discard on the hub.)
Scenario E (the composition proof) + the secret-free artifact assertion
- Generic ISO built
felhom-pve-9.2-1-v1.19.0-generic-generic.iso(shaf37080c835300b6cc2831bd6e5a483cad307f4d009469ee4206442fe9bebd348). Secret-free asserted on the artifact: manifestmode: pairing/secret-bearing: no; the env decoded from the ISO's/proxmox-first-bootstub carries onlyFELHOM_HUB_URL+FELHOM_INSTALL_URL— noFELHOM_CUSTOMER_ID, noFELHOM_RETRIEVAL_PASSPHRASE. - Nested boot → register (CONFIRMED live): the generic ISO booted on scratch VM 311 (SeaBIOS, LAN,
uuid
7ed8cb9e-…, MACbc:24:11:c3:b3:d0) → zero-touch install (answer file consumed, disksdaauto-selected, country HU) → reboot → installed system → first-boot pairing → registered at the LIVE hub:[INFO] appliance registered: new unclaimed box (uuid=7ed8cb9e-…, macs=1, ssh_keys=3)— the box gathered its MAC + all 3 SSH host keys exactly as designed. It now polls 204 (unbound), staged for the operator bind. The record + VM are left in place as the handoff. (Boot-order lesson: the scratch VM must bootscsi0;ide2— disk-first, CD-fallback — or a CD-first order reinstalls in a loop and never reaches first-boot.) - The bind is the coordinated Viktor moment (operator-password-gated; CC cannot log in). CC has
staged everything: the box is registered and visible; a Viktor-created drill customer + the 2-minute
bind click complete boot→register→bind→delivery→day-0, then the drill customer is torn down via
RESET then Delete (re-exercising v0.61.0/v0.60.1). The bind→delivery→day-0 legs are proven by the
hub unit tests + the
bootstrap-modes.shdelivery handoff; the live composition folds into the supervised rehearsal, which now runs the complete final product flow in one pass.
SSH-host-key pinning disposition
The registration payload carries the box's SSH host public keys; they are stored with the appliance
record and their fingerprints displayed in the unclaimed-appliances UI. They are not auto-attached
to the host on bind — the hosts row is minted later by the box's own /host-enroll (mint-once-reuse),
and no clean "attach host keys to host" hand-off surface exists today. Per the spec's fallback, this is
the store+display+Observation path, not new plumbing (a future item can wire TOFU-free pinning).
Observations
- Bind never gates on host count (multi-host customers are real — Peti); the picker shows counts as display only. A post-RESET customer is naturally hostless and binds fine (tested).
- Pre-bind provenance is the
appliance_registrationsrow's own timestamps (register/discard have no customer to scope aneventsrow to —events.customer_idis NOT NULL);appliance_bound/appliance_credential_deliveredevents are recorded once a customer exists. - Universal disk selection is out of slice-C scope:
profiles/generic.profiledefaults to the first SATA disk (sda); NVMe/other targets need a profile variant (a future concern, noted in the profile). - Poll cadence = systemd: the bootstrap does one poll per invocation and exits non-zero when unbound,
reusing
Restart=on-failure/RestartSec=30as the 30s poll timer (no long-running-oneshot timeout).