88 lines
6.5 KiB
Markdown
88 lines
6.5 KiB
Markdown
# 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).
|
|
|
|
## 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_OneShotAnd410` FAILS → restored.
|
|
- **Register idempotency (A):** drop the `(uuid, mac_set)` upsert branch → the re-register hits the
|
|
UNIQUE backstop (500) → `TestApplianceRegister_Idempotent` FAILS → 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 drives `run_direct` and 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` (sha
|
|
`f37080c835300b6cc2831bd6e5a483cad307f4d009469ee4206442fe9bebd348`). **Secret-free asserted on the
|
|
artifact:** manifest `mode: pairing` / `secret-bearing: no`; the env decoded from the ISO's
|
|
`/proxmox-first-boot` stub carries **only** `FELHOM_HUB_URL` + `FELHOM_INSTALL_URL` — no
|
|
`FELHOM_CUSTOMER_ID`, no `FELHOM_RETRIEVAL_PASSPHRASE`.
|
|
- **Nested boot → register (CONFIRMED live):** the generic ISO booted on scratch VM 311 (SeaBIOS, LAN,
|
|
uuid `7ed8cb9e-…`, MAC `bc:24:11:c3:b3:d0`) → **zero-touch install** (answer file consumed, disk `sda`
|
|
auto-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 boot `scsi0;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.sh` delivery 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_registrations` row's own timestamps (register/discard have no
|
|
customer to scope an `events` row to — `events.customer_id` is NOT NULL); `appliance_bound` /
|
|
`appliance_credential_delivered` events are recorded once a customer exists.
|
|
- **Universal disk selection is out of slice-C scope:** `profiles/generic.profile` defaults 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=30` as the 30s poll timer (no long-running-oneshot timeout).
|