SPIKE 3: [first-boot] does NOT fire on an interactive install

Findings only — no script, profile or build file changed; no release ISO built, nothing published.
documentation/audits/SPIKE-universal-iso-3-2026-07-31.md

MEASURED with a control from the SAME image (one ISO, 15 GRUB entries):
- Automated entry  -> hook fires: ttyS0 marker, marker file,
  /var/lib/proxmox-first-boot/proxmox-first-boot (0700), activation symlink, unit active.
- Terminal-UI entry, normal manual install -> ALL absent, and the proxmox-first-boot PACKAGE is
  not installed at all. A whole-filesystem grep for the marker returns nothing.

Mechanism cited: Config.pm:118 defaults first_boot.enabled=0 and set_first_boot_opt is never
called in the Perl tree; Install.pm:746 returns early without it; Install.pm:1360 skips the
package. proxinstall (graphical) has ZERO occurrences of first-boot. [first-boot] is an
automated-installer feature, unavailable on every interactive path by construction. R-154.

A delivery mechanism DOES exist and is UNTESTED: Install.pm:1343-1372 unpacks every .deb in the
ISO's /proxmox/packages/ into the target on every path (fixed skip-list), then dpkg --configure -a
runs postinsts (:1378) — how PVE ships first-boot itself. Read from source, not measured.

Q5: the public image should carry NO answer.toml at all — that removes the baked root hash, the
disk profile and the whole Spike 1-2 problem space, and makes it a one-line release gate. But
iso-repack.sh:100-106 refuses an ISO without auto-installer-mode.toml. R-155.

Incidental R-153: hub hostInstallVersion=1.19.0 vs SCRIPT_VERSION=1.22.0; hostinstall_gates.py
detects it and exits 1 — the gate works, nothing runs it.

Q3 (real stub at before-network) was NOT reached and is recorded as not reached.
This commit is contained in:
2026-07-31 14:47:18 +02:00
parent 19c932a693
commit bb29186d62
3 changed files with 387 additions and 65 deletions
+64 -65
View File
@@ -1,89 +1,88 @@
# REPORT — SPIKE 2: closing the last mechanisms, and the universal ISO's shape (2026-07-31)
# REPORT — SPIKE 3: on an interactive install, does the ISO deliver anything at all? (2026-07-31)
> Written as `REPORT-universal-iso-spike.md`, not `REPORT.md`: the shared file belongs to today's hub
> v0.85.0 session and the second session in a shared clone never touches it.
> v0.85.0 session and the second session in a shared clone never touches it. Supersedes this file's
> Spike 1 and Spike 2 contents.
**Class: Spike.** Findings only — no production file changed, no ISO built, nothing published.
Evidence: `documentation/audits/SPIKE-universal-iso-2-2026-07-31.md`.
**Class: Spike.** Findings only — no production file changed, no release ISO built, nothing published.
Evidence: `documentation/audits/SPIKE-universal-iso-3-2026-07-31.md`.
**All four questions are answered**, and every answer that matters is measured on a real PVE install.
The session lost both Tier 0 boxes mid-run (provider cable fault; four routes tried, no Tier 2
fallback used) and resumed when connectivity returned.
## The answer: no, and more completely than expected
## The two results that decide the product questions
**`[first-boot]` does not fire on an interactive install.** Measured, with a control from the *same*
image (one ISO, 15 GRUB entries):
**A locked root closes the PVE web interface.** Measured against the exact endpoint the UI logs in
through — `POST /api2/json/access/ticket`, realm `root@pam`**with a working control**: the
known-password install returns **HTTP 200 and a ticket**; the locked install returns 401 for every
password offered, and no password exists that could work. The control is what makes this a finding
rather than a broken probe. `passwd -S root` reports `L`, the shadow field is `literal-asterisk`, and
PVE uses the stock PAM stack.
- **Automated entry** → hook fires: `ttyS0` marker, marker file,
`/var/lib/proxmox-first-boot/proxmox-first-boot` (0700), activation symlink, unit `active`.
- **Terminal UI entry, normal manual install** → every one of those **absent**, and the
`proxmox-first-boot` **package is not installed at all**. A whole-filesystem grep for the marker
returns nothing.
**GRUB's recovery mode is closed too**`Cannot open access to console, the root account is locked.`
**but `init=/bin/bash` still works**, giving an unauthenticated `root@(none):/#`. So a locked-root
box is *not* unrecoverable, as the structural reading had feared; it is recoverable **operator-only, at
the physical console**. The installed GRUB has no password, which also means locking root is not a
physical-security measure and must never be described as one (**R-152**).
It is not that the hook fails to run — nothing that could run it is ever placed on the system. The
mechanism, cited: `Config.pm:118` defaults `first_boot.enabled => 0` and `set_first_boot_opt` is never
called anywhere in the Perl tree; `Install.pm:746` returns early without the flag; `Install.pm:1360`
skips the package. And `proxinstall`, the graphical installer, contains **zero** occurrences of
`first-boot`. **This is a property of the PVE installer, not a Felhom defect** — but nothing in our
tree records it, which is R-154.
## The mechanism that fixes the credential window
## The commercially relevant part
**`before-network` measured, A/B, same image, only `ordering` differing.** The hook **runs** (positive
marker, uptime 6.58 s) with entropy 256, a writable `/etc`, every binary it needs and a demonstrated
`openssl_rand_len=32` — while `ip_global` is **empty** and `listen_22_8006` is **0**. The `fully-up`
column is the converse proof: sshd and pveproxy already active, 3 sockets listening. **Zero credential
window, measured**, and the shipped unit (`Before=network-pre.target`, `Type=oneshot`) generalises it
to every machine.
**A delivery mechanism does exist.** `Install.pm:1343-1372` unpacks **every** `.deb` in the ISO's
`/proxmox/packages/` into the target on *every* install path — a fixed skip-list of known package
names, everything else installed — then `dpkg --configure -a` runs postinsts. That is exactly how PVE
ships `proxmox-first-boot` itself.
**And the reason it was never used is a comment that is wrong.** `answer.toml.tmpl:27` justifies
`fully-up` as needed "so pvesh/pct work", but `stub-first-boot.sh` makes no `pvesh`/`pct`/`pveum`/`qm`
call at all (grep `rc=1`); that usage lives in `felhom-bootstrap.sh`, under its own properly-ordered
unit. **R-148** — a comment asserting a *dependency* that does not exist, which blocked the fix.
**It is read from source, not measured**, and I have not proven either that our repack preserves an
added `.deb` or that a postinst running in the installer chroot can do the job (it runs *inside the
installer*, not on a booted system). Proving it is the top follow-up, ~45 min. Until then the honest
position is: the *insert-stick-install-type-claim-code* experience is **plausible but unvalidated**,
and the documented one-command manual path is what works today.
## Probes that failed, recorded as failed
Incidentally confirmed: the interactive installer shows the target disk and a summary listing
`Bootdisk(s) /dev/sda` before erasing. Your ruling needs no Felhom work for that warning.
Three, none massaged: a container probe that ran as uid 0 so `su` never invoked `pam_unix` (rc=0 even
with a wrong password); a GRUB probe that missed the **1-second** menu timeout and typed into the login
prompt; and a kernel-line edit that landed one line off — caught by a pre-typing verification
screendump before it was booted. All were rerun.
## The manual path is already most of the way there
Also corrected: the interim report's "Layer 1 teardown INCOMPLETE" was over-cautious. When demo-hp
returned, `/mnt/nvme-1tb/spike2/` did not exist and disk usage was unchanged — the staging `mkdir` had
been inside the SSH call that timed out as the link degraded, so **the fixture never landed**.
Two of the three things I was asked to look for already exist:
## The operator's decisions
- **The hub already renders the command with the customer-id filled in**
(`customer_unified.html:563-564`, plus a JS generator that adjusts the flags per role).
- **The URL is already short, public and always current** —
`felhom.eu/scripts/felhom-host-install.sh`, git-synced from `main` on a 30-second period.
- **Console display does not exist.** Nothing writes `/etc/issue`; PVE's own banner is what appears
after install. That file is the obvious carrier. Reported, not built.
**Escrow versus discard — no longer blocked, and reshaped twice.** "Discard" and "lock" are the same
outcome (nobody holds the password, so both `sulogin` and the web UI refuse), and **neither makes a box
unrecoverable** because `init=/bin/bash` always works. So escrow buys only this: the operator keeps the
web-UI and `sulogin` routes without physical presence. Against that stands D6 — a hub holding console
root for the whole fleet is what the hub alone must not be. **I recommend discard/lock for the public
image**, escrow remaining defensible for operator-built boxes. Your call; it is no longer waiting on a
measurement.
## Two things I'd flag
**The default boot entry — framed, unruled.** The disk evidence (Spike 1) and the recovery evidence
(this session) now point the same way: a tester whose unattended install succeeds but whose pairing
fails has no web UI and no recovery shell. The cost is the unattended experience. Two profiles from one
build is cheap, so operator-built and public need not match.
**A working gate is red and nobody runs it.** `configs.go:28` has `hostInstallVersion = "1.19.0"`
against `SCRIPT_VERSION = "1.22.0"`, and `scripts/hostinstall_gates.py` detects it and exits 1. The
comment saying drift "is now gated" is true of the gate's existence and false of its enforcement, so
the customer page's install-command generator targets a flag surface three minor versions stale.
**R-153.**
## Still open
**The cleanest public-image posture is currently blocked by our own tooling.** §6 of the findings
argues the public image should carry **no `answer.toml` at all** — which removes the baked root hash,
the disk profile, and every question Spikes 12 wrestled with, and makes "no `answer.toml`" a
one-line release gate. But `iso-repack.sh:100-106` refuses any ISO lacking `auto-installer-mode.toml`.
That guard is sound for its original case and wrong for this one. **R-155.**
One gap in Q3: **Felhom's real stub at `before-network`** is argued structurally, not observed — the
scenarios used the spike's own diagnostic as the hook. That is the one thing to measure before a spec
is written (~40 min). Also: the SSH-as-root leg used a wrong password in the control too, so it
discriminates nothing and is recorded as a gap rather than a result.
## Not reached
**Q3 — the real stub at `before-network`** was not run; driving the interactive install consumed the
session. It is recorded as not reached, not inferred. Spike 2's structural case stands unrefuted, and
§2 narrows its scope: with no first-boot hook on the interactive path, that question now governs
**operator-built images only**. ~40 min when wanted.
## R-rows
**R-148** (`fully-up` justified by a non-existent dependency) · **R-149** (three ordering values) ·
**R-150** (Condition-guarded hooks skip silently) · **R-151** (demo-felhom built from an uncommitted
profile) · **R-152** (a locked root is not a physical-security measure).
**R-153** (a working gate is red and unrun) · **R-154** (`[first-boot]` is automated-only and nothing
in our tree says so) · **R-155** (`iso-repack.sh` blocks the no-`answer.toml` posture).
## Teardown
All three layers verified positively. demo-hp is back to its exact pre-spike state: `spike2/` and
`/tmp/paia` removed, 0 QEMU processes, 0 loop devices, **disk usage 6.6 G identical to pre-spike**,
4 storages unchanged, nothing on `local-lvm`, `drill-r50` stopped and 9201 running, both untouched.
demo-felhom: probe binary removed, external USB drive observed only, still mounted as found. DooPlex:
scratchpad 1.7 GB → 60 K, control password `shred -u`'d and verified absent, `felhom-iso/out/`
untouched at 17 ISOs, no production file modified. **Hub-side: nothing created** — the VMs had no route
to it. Nothing published.
All three layers verified positively. demo-hp back to its exact pre-spike state `spike3/` removed,
0 QEMU processes, 0 loop devices, **disk usage 6.6 G, identical to pre-spike**, 4 storages unchanged,
nothing on `local-lvm`, `drill-r50` stopped and 9201 running. demo-felhom: nothing placed, external
drive observed only. DooPlex: scratchpad 4.4 GB → 80 K, both throwaway passwords destroyed,
`felhom-iso/out/` untouched at 17 ISOs, no production file modified. **Hub-side: nothing created**
verified by fetching and searching the customer list, not asserted. Nothing published.