docs: R-21 slice C — REPORT/CONTEXT + capability-map (universal-ISO chain, slice C live-verified) + ROADMAP (R-21 SHIPPED A+B+C, R-27 self-bind future)

This commit is contained in:
2026-07-17 15:50:33 +02:00
parent 9964ae6fd4
commit 4c9b0e8706
4 changed files with 85 additions and 78 deletions
+66 -76
View File
@@ -2,96 +2,86 @@
> **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).
## scripts v1.18.0 — R-21 slice B: firmware loader option `--loader shim|mkimage` (F1) — 2026-07-17
## R-21 slice C — the universal secret-free ISO (hub v0.62.0 + scripts v1.19.0) — 2026-07-17
**Baseline:** scripts v1.17.0 (host-install) / build-felhom-iso.sh was ISO_VERSION 1.16.0. Target
**v1.18.0**. Closes N100 **F1 (HIGH):** cheap AMI (`AN3PLUS 0.01`-class) UEFI firmware can't relocate
the ISO's stock signed GRUB from USB (`relocation 0x0`); the run's live `grub-mkimage` workaround is
now a first-class pipeline mode. Touched **only** `scripts/iso/`.
**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
`build-felhom-iso.sh` gains `--loader shim|mkimage` (default **shim**, profile-settable via
`FELHOM_LOADER`, `--loader` wins). **shim = the stock output, byte-for-byte unchanged** (surgery
skipped). **mkimage** replaces the ISO's UEFI boot path with a monolithic `grub-mkimage` loader.
New: `mkimage-surgery.sh`, `profiles/n100.profile`, Dockerfile grub/mtools tooling; loud banner +
manifest `loader:`/`grub-mkimage:` fields + `-mkimage` filename suffix.
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.
### Surgery approach actually used (Phase 2)
### Registration payload (as implemented) + token model
Recipe reproduced from the run evidence (the literal command was run on the now-wiped N100 old system
and saved nowhere — only the *procedure* is recorded; see Observations). In the assistant container,
**after** `prepare-iso`:
1. `xorriso -osirrox` extract the prepared tree (chmod u+w — osirrox files are read-only).
2. `grub-mkimage -O x86_64-efi` from the **container's** grub 2.12 build (`/usr/lib/grub/x86_64-efi`,
which has `kernel.img` — the ISO ships modules but not kernel.img; the box likewise used its
*installed* GRUB). Module set = the base search/configfile/USB chain **plus every `insmod` in the
ISO's own `grub.cfg`** (57 modules). Embedded config: `search --fs-uuid <ISO uuid>``configfile`
the real menu. The fs-uuid is derived from the ISO's PVD modification-date (GRUB's iso9660 uuid
source) and pinned on re-master so it stays valid.
3. Swap the loader into the ISO9660 tree (**real lowercase Rock-Ridge path** `/efi/boot/bootx64.efi`
an early bug created a spurious *uppercase* path instead; fixed) **and** inside the `efi.img` ESP
(FAT, case-insensitive — the loader UEFI firmware actually runs from USB).
4. Re-master with `xorriso -as mkisofs` from the ISO's own `as_mkisofs` report, dropping **only** the
Apple HFS+/APM map (`-hfsplus`/`-apm-block-size`) and the isohybrid GPT-basdat marking
(`-part_like_isohybrid`/`-isohybrid-gpt-basdat`) — re-emitting those trips xorriso 1.5.6's
"Overlapping MBR partition entries" on this layout, and Mac boot is irrelevant. **Kept:** protective
MBR + grub2-mbr (BIOS hybrid) + El Torito (BIOS + UEFI) + the GPT EFI System Partition (USB UEFI).
`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).
**Stall-rule outcome:** the surgery resisted for ~40 min of iteration (two overlap failures + the
kernel.img and path-case bugs) — **well within the 2 h budget**; no spike-note fallback needed.
### Red-proofs (run-fail-revert, all recorded)
### Per-leg results (RUNBOOK-B, nested VM 311 on felhom-pve, OVMF)
- **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.
- **Leg 1 — shim regression:** the default ISO **boots + zero-touch auto-installs under OVMF Secure
Boot ENFORCING** (`pre-enrolled-keys=1`; "EFI boot mode detected", DHCP, "Starting Proxmox
installation", package extraction unattended). **Plus one SeaBIOS boot** — the shim ISO also boots
under BIOS (hybrid grub2-mbr/eltorito path survived the v1.18.0 build). ✓
- **Leg 2 — mkimage happy path:** `--loader mkimage` ISO **boots + auto-installs under OVMF Secure
Boot OFF** (`pre-enrolled-keys=0`). The monolith's embedded `search --fs-uuid` found the ISO and
chained its real menu → the installer ran. ✓
- **Leg 3 — trade-off red-proof:** the **same** mkimage ISO under OVMF Secure Boot ENFORCING
**FAILS** — captured firmware behavior: `BdsDxe: failed to load Boot0002 "UEFI QEMU DVD-ROM …" :
Access Denied``No bootable option or device was found`. The unsigned loader is refused. This
makes "Secure Boot must be OFF for mkimage" a **fact**, not a guess. ✓
- **Leg 4 — structural / byte-identity:** diffing the shim(out) vs mkimage(final) trees, **only**
`efi/boot/bootx64.efi`, `efi/boot/grubx64.efi`, `efi.img`, and the regenerated El Torito artifacts
(`boot.cat`, `i386-pc/eltorito.img`) differ — **the entire install payload is byte-identical**. The
tree loader (1253376 B, vs shim's 1032032 B) carries the embedded `2026-05-21-…` fs-uuid and is
identical to the loader inside `efi.img`; **0 spurious uppercase paths**; El Torito keeps both boot
images. The `validate-answer` gate still runs pre-surgery. ✓
### Deploy verification
### Artifacts (shas + manifests)
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.)
- shim nested-test ISO `…-nested-vm-val.iso``28d862e2f7b014588717c15ef8c9bc46d11a9a5689a022f5d4de9e8f939d1377`
- mkimage nested-test ISO `…-nested-vm-val-mkimage.iso``71fce3333b3148e48d10711311f275da4b849bf383f711de0f2e4d5802618869`
- **safety ISO** `…-n100-safety-mkimage.iso``f9aa0428e5efdcb5e7c0ade05264ff681ac4f9aa47b7f3721c9eb712f025c1a8`
(mkimage + match-nothing filter; **built + sha-recorded, NOT booted**; at `180:~/n100-baremetal/safety/`).
Manifests record `loader: mkimage … Secure Boot OFF`, `grub-mkimage: 2.12-9+deb13u2`, secret-bearing.
Nested-test ISOs + VM 311 destroyed after the legs.
### Scenario E (the composition proof) + the secret-free artifact assertion
### Physical proof on the real AMI board
- **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**.
Shape **(a) DEFAULT** stands unless Viktor picks otherwise: the F1 closure folds into the supervised
N100 rehearsal (its install ISO built `--loader mkimage`). Shape **(b)** — the zero-risk `n100-safety`
pre-flight ISO — is **built and sha-recorded** (above), ready either way; **do not boot without Viktor**.
### SSH-host-key pinning disposition
### Docs diffs
`scripts/CHANGELOG.md` (v1.18.0); `scripts/iso/README.md` (loader-modes section + N100 SB-off prep +
`mkimage-surgery.sh` row); `scripts/iso/profiles/n100.profile` (new, mkimage); `VALIDATION-n100-baremetal`
F1 → PIPELINE-FIXED erratum; `00-capability-map.md` (new bare-metal ISO row + loader caveat);
`ROADMAP.md` (slice B collapsed to SHIPPED; PXE one-line deferred note); `CONTEXT.md`.
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
- **The literal `grub-mkimage` command was unrecoverable** — it ran on the N100's old system (wiped at
the run) and is not in `~/n100-baremetal/` nor 180's history. Only the *procedure* is recorded
(VALIDATION F1 / CONTEXT). Reproduced faithfully: mkimage from a working (non-ISO) 2.12 GRUB build,
module set = the ISO's grub.cfg needs, embedded `search --fs-uuid``configfile`. Not a strategy
re-derivation.
- The mkimage re-master **drops Apple HFS+/APM (Mac boot)** — irrelevant for N100/PC, and required to
avoid xorriso's overlap check. BIOS + UEFI + GPT-ESP boot all preserved (asserted in-build + Leg 4).
- Leg 2's "first-boot stub fires" is assured transitively: Leg 4 proved the first-boot payload is
byte-identical to the shim variant's (slice-A-proven), and Leg 2 proved the answer is consumed. A
full ~10-min nested install-to-first-boot was not re-run for the mkimage ISO on that basis.
- **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).