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:
@@ -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 1–2 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.
|
||||
|
||||
Reference in New Issue
Block a user