Files
felhom.eu/documentation/audits/SPIKE-baremetal-iso-2026-07-16.md
T
admin 914d51aa5f docs(spike): bare-metal Felhom ISO — R-21 spiked (all mechanisms GREEN on nested virt)
Empirical spike of the Proxmox automated-installation path for R-21 (bare-metal
customer-hardware ISO). All legs proven on nested VM 310 (felhom-pve):
- S1 assistant 9.2.7 + ISO provenance; validate-answer exits 0 on failure (parse output)
- S2/S2b zero-touch install SeaBIOS + UEFI incl. Secure Boot enforcing (no keypress/MOK)
- S3 first-boot hook: fully-up = root + working pvesh/pct, exactly-once
- S4 webhook payload enumerated (SMBIOS-UUID + MAC + host SSH keys)
- S5 disk-filter installs only target (canary byte-identical), match-nothing fails safe
- S6 from-url + cert-fingerprint fails CLOSED (red-proof)
- S7 http answer-fetch identity; S8 no-net + invalid-answer failure modes
Findings doc + ROADMAP R-21 status=spiked. No production code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
2026-07-16 11:29:03 +02:00

19 KiB

SPIKE — bare-metal Felhom ISO (Proxmox automated-installation mechanics), 2026-07-16

Class: spike (empirical mechanism validation before any production spec). Output: this findings doc. No production code shipped. The only repo write is this file + one ROADMAP line (R-21 → spiked).

Verdict in one line: every mechanism the bare-metal path depends on — the proxmox-auto-install-assistant toolchain, zero-touch install (BIOS and UEFI, incl. Secure Boot), the first-boot hook (privileges / ordering / exactly-once), the post-installation webhook, disk-filter safety, and from-url + cert-fingerprint pinning — works and fails-closed where it must. R-21 is technically green to spec on nested virt; the only residual unknowns need one real bare-metal run (vendor DMI serials, real firmware quirks). (The spike brief refers to this feature as "R-22"; it is entered in the ROADMAP as R-21 — the same item.)

0. Environment & baselines

Item Value
felhom.eu main @ d202b43 (clean, confirmed at start)
Host (nested-VM + live validation) felhom-pve 192.168.0.162, PVE 9.2.2, kernel 7.0.2-6-pve; 7.5 GiB RAM free at start (≥5 GiB gate ✓)
Assistant / ISO build host DooPlex 192.168.0.180 (Docker 29.2.1)
Spike VM 310 spike-baremetal-iso, 4 GB / 2 vCPU, scsi0 32 GB (target) + scsi1 8 GB (data/canary), virtio-scsi-single; SMBIOS uuid 14a82ff6-3ab5-440d-ac40-7f9968408618, NIC MAC bc:24:11:98:10:0e
Never touched qm 300 (drill), 9000, LXC 9201, Peti, hub — honored
Throwaway password $6$… crypt hash baked into answer files (spike-only; # THROWAWAY in the archived answer.toml)
Evidence root 180:~/spike-baremetal/ (ISOs, answers, evidence/<leg>/, listener/https logs) — kept
Spike VM final state stopped, kept (per cleanup rule) pending acceptance

S1 — toolchain + ISO provenance — PROVEN

  • Assistant obtainable + pinnable per release: proxmox-auto-install-assistant 9.2.7 (proxmox-installer-common 9.2.7) installed in a disposable debian:trixie Docker container on DooPlex from the PVE trixie no-subscription repo. This is the intended build-felhom-iso.sh host and it works. Pairing rule: install the assistant from the pve <codename> no-subscription repo whose Debian codename matches the ISO generation (PVE 9.x = Debian 13 trixie); the assistant point release (9.2.7) tracks the repo and pairs with the ISO by major.minor (9.2), not by the ISO's point release (9.2-1).
  • ISO provenance: proxmox-ve_9.2-1.iso sha256 4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c — matches the official download.proxmox.com/iso/SHA256SUMS and the copy already on felhom-pve.
  • Build cost (design input #6): container image build 37.9 s / 243 MB; prepare-iso ~11.5 s per prepared ISO (source local); prepared ISO 1,707,278,336 B (~1.1 MB over the 1,706,178,560 B source).
  • validate-answer catches a broken file — fed one with both root-password and root-password-hashed: `global.root-password` and `global.root-password-hashed` cannot be set at the same time / Error: Found issues in the answer file. TRAP (load-bearing for the pipeline): it prints that to stderr but returns exit code 0 — for both valid and invalid files. A build-felhom-iso.sh gate must parse the output text, never $?. It is also syntactic only: disk-list=["sdz"] (a non-existent disk) passed validation and only failed at install time (see S8b).
  • Evidence: evidence/S1-S8-key-results.txt, ~/spike-baremetal/build/build.log, iso/SHA256SUMS.

S2 — baseline zero-touch install (SeaBIOS) — PROVEN

  • Given/When/Then: prepared ISO (--fetch-from iso, keyless answer-base.toml) on VM 310 (SeaBIOS, single 32 GB disk) → boot → automated install with no keypress, reboot into working PVE.
  • Boot menu auto-selected "Install Proxmox VE (Automated)" with a 5 s countdown (evidence/S2/boot-8s.png); unattended package extraction (evidence/S2/mid-install.png); no interactive prompt at any point. Install wall-clock ≈ 3 min (start 09:45:33 → booting installed PVE by 09:48:48).
  • Installed system reachable over IPv6 link-local: 22 OPEN (SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4), 8006 OPEN. TLS cert CN=spike310.felhom.local (configured fqdn applied); SAN carries the DHCP-assigned IPv4 192.168.0.115. Boot-order scsi0;ide2 fall-through: the post-install reboot landed on the installed disk (no reinstall loop).
  • Evidence: evidence/S2/verdict.txt, boot-8s.png, mid-install.png.

S2b — UEFI + Secure Boot — PROVEN (no caveat needed)

  • UEFI (OVMF/q35, Secure Boot OFF, pre-enrolled-keys=0): automated install identical, zero keypress (evidence/S2b/boot-uefi-installer.png). Keyed SSH login worked; /sys/firmware/efi present → genuine UEFI boot; mokutil: SecureBoot disabled / Setup Mode. Password mechanism proven non-hollow: the installed /etc/shadow root hash equals the answer's root-password-hashed verbatim → the password would work as configured.
  • Secure Boot ON (pre-enrolled-keys=1, MS 2023 certs, enforcing): the PVE ISO's MS-signed shim chain booted the automated installer (evidence/S2b/sbon-18s.png), the install completed, and the installed PVE booted under Secure Boot enforcingmokutil: SecureBoot enabled, BOOTED_VIA=UEFI, lockdown=integrity, zero keypress, no MOK-enrollment prompt. → R-21 needs no Secure-Boot caveat: customer mini-PCs with SB on by default install and boot fine.
  • Bonus finding: the PVE installer writes a dual BIOS+UEFI-bootable disk — the S2 SeaBIOS install also booted under OVMF.
  • Re-imaging gotcha (finding): the first UEFI reinstall over a disk that still held the prior install's LVM PV signature (only first/last-10 MiB wiped) aborted: "unable to initialize physical volume /dev/sda3 / low level installer returned early". The auto-installer's "cleanup root-disks" step does not force-clear a pre-existing LVM PV. A full blkdiscard then installed cleanly. → a customer disk with a prior LVM/Proxmox/Linux install may need an explicit wipe before the automated install.

S3 — first-boot hook: privileges, ordering, exactly-once — PROVEN

  • --on-first-boot stub.sh, [first-boot] source="from-iso" ordering="fully-up". Unit is proxmox-first-boot-multi-user.service ("Fully Booted"), After=… pveproxy.service pmgproxy.service proxmox-backup-proxy.service.
  • Runs as root (uid 0). Under fully-up, pvesh get /nodes returned a complete node object (status online, ssl_fingerprint present, uptime) pvesh_rc=0; pct list pct_rc=0; outbound curl hit the DooPlex listener — GET /firstboot?host=spike310&uid=0 from 192.168.0.115 (network truly up). This is the ordering the real wrapper needs (it drives pveum/pct/pvesh).
  • Exactly-once mechanism: unit has ConditionPathExists=/var/lib/proxmox-first-boot/pending-first-boot-setup and ExecStartPost=/usr/bin/rm … pending-first-boot-setup. Empirically: after a reboot the hook log is byte-identical (16 lines, 1 marker, sha256 unchanged), the unit is inactive(dead) with "ConditionPathExists … was not met" (ConditionResult=no), and the listener shows exactly 1 firstboot hit.
  • Comparison leg — ordering="network-online" (proxmox-first-boot-network-online.service): pvesh returned degraded data — '/etc/pve/nodes/spike310/pve-ssl.pem' does not exist!, status:"unknown", ssl_fingerprint:null (pmxcfs/pveproxy not yet ready). Note pvesh_rc=0 in both orderings — the exit code lies; only fully-up yields a coherent PVE stack.
  • Evidence: evidence/S3/firstboot-network-online.log, listener log, evidence/S1-S8-key-results.txt.

S4 — post-installation-webhook payload — PROVEN (full payload archived)

  • [post-installation-webhook] url="http://…:8088/webhook" (plain HTTP; no cert-fingerprint required). Fired near end of install, before reboot (~2 m 42 s in). POST … application/json, ureq/3.0.11, 5052 B, schema v1.2. Full body: evidence/S4/webhook-payload.json.
  • Identity fields the hub's unclaimed-appliance record can key on:
    • dmi.system.uuid = 14a82ff6-… — SMBIOS/product UUID (stable hardware id).
    • network-interfaces[].mac = bc:24:11:98:10:0e (+ is-management:true, DHCP address).
    • machine-id (regenerates per install — not a hardware id), fqdn, boot-info.mode ("bios"), product.version 9.2.2, debian-version 13.5, cpu-info.model "Intel(R) N100".
    • disks[].udev-properties.ID_SERIAL / ID_SERIAL_SHORT / ID_PATH — disk serials.
    • ssh-public-host-keys (ecdsa/ed25519/rsa) — lets the hub pin the box's SSH host keys for later TOFU-free access.
  • CAVEATS (design input #2):
    • On nested virt dmi.baseboard is {}, dmi.chassis.serial and dmi.system.serial are empty. On real bare metal these carry vendor board/chassis/system serials — unprovable here (OPEN item).
    • Webhook disks[].size is unreliable: the 8 GB second disk was reported as 32 GB (it echoed the boot disk's size); the guest's own lsblk shows 8 GB. Key on disk serials, not disks[].size.

S5 — disk-filter safety (LOAD-BEARING) — PROVEN

Two disks: sda=32 GB target, sdb=8 GB ext4 canary (LABEL=FELHOM_CANARY, UUID=1111…5555, canary.bin sha256 0d22f95b…, canary.txt 42139528…; evidence/S5/canary-before.sha). Canary verified by mounting the LV on the host and hashing, not from installer output.

Sub-run Selection Result
(a) disk-list=["sda"] installed on sda; sdb canary sha256 -c: OK (byte-identical), UUID unchanged. PROVEN
(b) filter.ID_SERIAL_SHORT="drive-scsi0" (matches only sda) installed on sda (lsblk: sda=pve LVM, sdb=ext4 FELHOM_CANARY); canary OK. PROVEN
(c) filter.ID_SERIAL_SHORT="no-such-disk-zzz" (matches nothing) ERROR: Installation failed: filter did not match any deviceAuto-installation failed (exit-code 1) → aborts to installer root shell. No interactive fallback, no disk touched, canary intact. evidence/S5/matchnothing-console.png

S6 — first-boot from-url + cert-fingerprint pinning — PROVEN (fails closed)

  • Stub served over HTTPS with a self-signed cert from DooPlex; [first-boot] source="from-url" url="https://…:9443/stub.sh" cert-fingerprint=<SHA256> ordering="fully-up". The pin is checked at install time (in the installer environment), not first boot.
  • Correct fingerprint: installer fetched the stub — HTTPS log 192.168.0.115 "GET /stub.sh HTTP/1.1" 200 — and it ran at first boot (uid 0, log present, unit active).
  • RED-PROOF — wrong fingerprint (F6…A6…): "INFO: Fetching first-boot hook from https://…/stub.sh .. ERROR: Autoinstaller setup error: io: unexpected error: Fingerprint did not match! → Auto-installation failed (exit-code 1)" — the entire install aborts; the stub was never fetched (HTTPS log empty) and never ran. evidence/S6/redproof-console.png.
  • Verdict: pinning FAILS CLOSED — fit for the wrapper download story (from-url is safe; from-iso is not required for security).

S7 — HTTP answer-fetch identity (stretch) — PROVEN

  • --fetch-from http --url http://…:8088/answer. The installer POSTs for its answer: application/json, ureq/3.0.11, accept: application/json, application/toml;q=0.5, schema v1.0, 388 B. Body carries: dmi.system.uuid 14a82ff6-…, network_interfaces[].mac BC:24:11:98:10:0E, product/iso info, and (empty on virt) DMI serials — but no disk info yet (disks aren't enumerated at answer-fetch time). → option B (hub serves a per-customer answer) is viable, keyed on MAC + SMBIOS UUID. Full request in the listener log after the S7-HTTPFETCH marker.

S8 — failure modes — PROVEN

  • (a) No DHCP / no link at first boot (host-side qm reboot with NIC link_down=1; hook status read via the pending-flag over IPv6 link-local — no reboot needed):
    • fully-up: hook RUNS — flag consumed, new marker, pvesh works locally, outbound curl fails ("Could not connect"). Boot reaches login. → robust to "customer's router is off".
    • network-online: hook does NOT run — flag persists, unit inactive(dead) (gated on network-online.target, unsatisfied without a DHCP-configured link); boot still reaches login (no infinite hang). Hook stays armed for a later boot once network returns.
    • (Method note: a first attempt set link_down=1 before an ssh reboot, which then couldn't reach the guest — the reboot never fired. Redone host-side. The two "-nonet-console.png" captures are from that stale boot and are superseded by the flag-check result in evidence/S1-S8-key-results.txt.)*
  • (b) Invalid answer — disk-list=["sdz"] (non-existent disk): validate-answer passed it (syntactic only); at install: "ERROR: Installation failed: disk in 'disk-selection' not found → Auto-installation failed (exit-code 1)", aborts to shell, no disk touched, no interactive fallback. evidence/S8/invalid-disk-console.png.

13. Evidence

All under 180:~/spike-baremetal/evidence/: per-leg console PNGs (S2/S2b/S5/S6/S8), the full webhook JSON (S4/webhook-payload.json), the request-logging listener (listener.log) and HTTPS access log (https.log), canary hashes (S5/canary-before.sha), and the consolidated S1-S8-key-results.txt. Console captures were taken with QEMU-monitor screendump → PPM → PNG (felhom-pve has no image tools; converted on DooPlex). Prepared ISOs + source ISO retained under 180:~/spike-baremetal/iso/.

15. Design inputs for R-21 (bare-metal ISO) — (the spike brief calls this feature "R-22")

  1. First-boot ordering → fully-up. Proven: under fully-up, pvesh returns a complete node object and pct works (S3); under network-online pvesh is degraded (pve-ssl.pem missing) and the hook does not even run without a DHCP link (S8a). The wrapper (drives pveum/pct/pvesh) must use fully-up. The exactly-once guard is ConditionPathExists=…/pending-first-boot-setup + ExecStartPost=rm — re-arm by re-touching that flag if a redo is ever needed.

  2. Webhook keys for the unclaimed-appliance record: dmi.system.uuid (SMBIOS/product UUID) + management-NIC mac are the stable pair; also archive ssh-public-host-keys (host-key pinning), fqdn, machine-id (per-install, not hardware), cpu-info.model, disk ID_SERIAL. Do not key on disks[].size (unreliable — S4). Real board/chassis/system serials are empty on virt → confirm on bare metal before relying on them.

  3. Disk-selection policy for alpha: both disk-list=["sda"] and a udev filter.* glob install only the intended disk and leave other disks byte-identical (S5 a/b, proven by host-side hashing). A match-nothing filter and a non-existent disk name both fail-safe: abort with exit-code 1, touch no disk, no interactive fallback (S5c, S8b). Recommendation: single-disk mini-PC → a size/model filter.* glob (portable across identical hardware, self-documenting), with the match-nothing abort as the built-in wrong-hardware guard. Add a pre-install LVM/partition wipe step (or document it) — the installer does not force-clear a prior LVM PV (S2b re-imaging gotcha).

  4. UEFI / Secure Boot: no caveat required. Automated install and the installed PVE both work under UEFI with Secure Boot enforcing (pre-enrolled MS keys), zero keypress, no MOK prompt (S2b). A single install image is BIOS+UEFI-bootable.

  5. Pinning verdict: from-url + cert-fingerprint is fit — a wrong pin aborts the whole install (fails closed, S6). The wrapper/stub may be fetched from a URL with a pinned fingerprint; from-iso (baking) is not required for security. Note the cert-fingerprint format is the colon-separated uppercase SHA256 (openssl x509 -fingerprint -sha256).

  6. Assistant↔ISO pairing + build cost → build-felhom-iso.sh: install the assistant from the PVE no-subscription repo of the ISO's Debian codename (PVE 9.x = trixie); pair by major.minor. Per prepared ISO ≈ 11.5 s + ~1.7 GB artifact; the container build is ~40 s one-time. The pipeline runs fine on DooPlex (the build host). Gate on parsed validate-answer output, never $? (exit 0 on failure). Remember validate-answer is syntactic only — disk existence is a runtime check.

  7. Failure-mode table (customer-facing "what if it goes wrong"):

    Scenario Behavior Customer/operator signal
    Wrong hardware — filter matches nothing install aborts, exit 1, no disk touched box never finishes install; stays on ISO/shell
    Non-existent disk in answer install aborts, exit 1, no disk touched same
    Prior LVM/OS on disk install aborts ("cannot init PV") needs a wipe step (see #3)
    Wrong cert-fingerprint (from-url) whole install aborts, exit 1 box never finishes install
    No internet at first boot (fully-up) hook runs; net-dependent steps fail-and-retry box installed, wrapper fired, retries when net returns
    No internet at first boot (network-online) hook does not run; boot OK avoid this ordering
  8. OPEN — needs one real bare-metal run (not provable on nested virt):

    • Real vendor DMI serials (baseboard/chassis/system) in the webhook + answer-fetch payloads (empty on QEMU) — decide whether to key on them.
    • Real firmware quirks: vendor UEFI boot-entry handling, whether the disk-first→CD fall-through works on a real board (worked on SeaBIOS and OVMF here), and Secure Boot with a vendor's (non-MS-clean) key set.
    • Real-NIC naming (this VM enumerated ens18) and multi-NIC is-management selection.
    • Real DHCP behavior / captive networks; USB-stick boot media (we booted a virtual CD).
  9. Observations (recorded, not acted on):

    • validate-answer exit-0-on-failure is arguably a Proxmox bug; regardless, the pipeline must parse output.
    • The probe stub's curl_rc reflected the trailing pipe stage, not curl — rely on curl's error text.
    • Webhook disks[].size mis-report (see #2) is a Proxmox data-quality issue worth reporting upstream.
    • machine-id regenerates per install → not a durable hardware identity for re-imaged boxes.

Cleanup state

Spike VM 310 stopped, kept (config intact, s8b ISO still attached). ISOs + evidence retained under 180:~/spike-baremetal/. DooPlex helper listeners (8088 answer/webhook, 9443 HTTPS, 8099 file-server) were used only during the spike and may be stopped. Nothing else on either host was modified.