19c932a693
Findings only — no script, profile or build file changed; no ISO built, nothing published.
documentation/audits/SPIKE-universal-iso-2-2026-07-31.md
Both Tier 0 boxes went offline mid-session (provider cable fault; four routes tried, no Tier 2
fallback used) and returned. All three scenarios then ran to completion on real PVE, each signalled
by reboot-mode='power-off' rather than a disk hash.
- A LOCKED ROOT CLOSES THE PVE WEB INTERFACE. Measured at the exact endpoint the UI uses
(POST /api2/json/access/ticket, root@pam) WITH A WORKING CONTROL: known-password install returns
HTTP 200 + ticket; locked install returns 401 for every password and none can exist.
passwd -S root = L, shadow = literal-asterisk, PVE uses the stock PAM stack.
- GRUB recovery mode is also closed ('the root account is locked') — but init=/bin/bash still gives
an unauthenticated root@(none):/#. A locked box is recoverable, operator-only, at the console.
The installed GRUB has NO password, so locking root is not a physical-security measure. R-152.
- before-network MEASURED (A/B, same image): the hook RUNS (marker, uptime 6.58s) with entropy 256,
writable /etc, all binaries and openssl_rand_len=32, while ip_global is EMPTY and
listen_22_8006 = 0. fully-up is the converse: sshd+pveproxy active, 3 listening. Zero window.
- R-148: answer.toml.tmpl:27 justifies fully-up with a pvesh/pct dependency the stub does not have
(grep rc=1) — it blocked the ordering now measured as the fix.
- R-149 three ordering values; R-150 Condition-guarded hooks skip silently; R-151 demo-felhom built
from an uncommitted profile.
Three probes failed and are recorded as failed: a container probe that ran as uid 0, a GRUB probe
that missed the 1-second menu timeout, and a kernel-line edit one line off (caught by a pre-typing
verification screendump). The interim 'Layer 1 teardown INCOMPLETE' is corrected — the fixture had
never landed, because the staging mkdir was in the SSH call that timed out.
501 lines
28 KiB
Markdown
501 lines
28 KiB
Markdown
# SPIKE 2 — closing the last mechanisms, and the universal ISO's shape (2026-07-31)
|
||
|
||
> **Class: Spike.** Findings only. No production file changed, no ISO built, nothing published.
|
||
>
|
||
> Continues `SPIKE-universal-iso-2026-07-31.md`; its results are premises here and are not re-derived.
|
||
>
|
||
> **All four questions are answered, and every answer that mattered is measured on a real PVE install.**
|
||
> The session lost its venue mid-run (§0) and resumed when connectivity was restored.
|
||
|
||
---
|
||
|
||
## 0. Venue loss and recovery — what was tried
|
||
|
||
At 12:28 CEST both Tier 0 boxes went offline **simultaneously**. Routes attempted, in order:
|
||
|
||
| # | Route | Result |
|
||
|---|---|---|
|
||
| 1 | `ping 100.76.96.79` (tailnet direct) | 100 % loss |
|
||
| 2 | `ssh demo-hp` | `Connection timed out` |
|
||
| 3 | `ssh demo-hp-lan` (LAN, ProxyJump `felhom-pve`) | `Connection timed out during banner exchange` |
|
||
| 4 | `ssh felhom-pve` (the jump host) | `Connection timed out` |
|
||
| 5 | DooPlex tailscale pod | `Running 6d10h`, healthy |
|
||
| 6 | `tailscale status` from the pod | **both peers `offline`**, `last seen` rising |
|
||
|
||
Far end, not ours. **No fallback venue was used** — DooPlex is Tier 2, the 2026-07-25 ruling moved
|
||
drill VMs off it, and this task did not authorise it. An absent fence is not permission.
|
||
|
||
Connectivity was restored (operator: a provider cable fault nearby) and the boxes returned with
|
||
**uptime 7 days** — a pure network outage, no reboot. All probes then ran.
|
||
|
||
**One thing the outage revealed.** `/mnt/nvme-1tb/spike2/` did not exist when demo-hp came back, and
|
||
disk usage was unchanged at 6.6 GB: the staging `mkdir` had been inside the SSH call that timed out as
|
||
the link degraded, so **the fixture never landed and Layer 1 had nothing to clean**. The interim
|
||
report's "Layer 1 teardown INCOMPLETE" was therefore over-cautious rather than wrong — corrected here,
|
||
and the fixture was rebuilt from scratch.
|
||
|
||
---
|
||
|
||
## 1. Baselines
|
||
|
||
| Baseline | Value |
|
||
|---|---|
|
||
| `ISO_VERSION` | `1.25.0` — `scripts/iso/build-felhom-iso.sh:44` |
|
||
| `SCRIPT_VERSION` | `1.22.0` — `scripts/felhom-host-install.sh:187` |
|
||
| `felhom-bootstrap.sh` @ HEAD | `21bf6a6bde0cb13e3809e2f5c136a49929dcc82eb8d40bbdf6f290a886ee8ab7` |
|
||
| PVE base | `proxmox-ve_9.2-1.iso`, `4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c` |
|
||
| `felhom.eu` HEAD | **`ea00976`**, clean, `== origin/main` |
|
||
| `proxmox-first-boot` pkg | `9.2.5` (from the ISO's `/proxmox/packages/`) |
|
||
| installed guest kernel | `7.0.2-6-pve`, GRUB `2.12-9+pmx2` |
|
||
|
||
**Fixture provenance.** Probe ISO built on DooPlex from the baseline PVE ISO
|
||
(`prepare-iso --fetch-from partition --partition-label proxmox-ais --on-first-boot diag.sh`),
|
||
sha256 `dd97dba5518c70baf4300b119e1042eabf1012d5a046082acf31e60983efaeb9`, **verified byte-identical at
|
||
both ends** after transfer. Nested VMs ran under plain QEMU/KVM on **demo-hp** (Tier 0, the designated
|
||
drill host) under `/mnt/nvme-1tb/spike2/` — never `local-lvm`, no PVE storage registered, `drill-r50`
|
||
and guest 9201 untouched.
|
||
|
||
**Both of Spike 1's fixture defects are corrected in this one:** a NIC is present **at install time**
|
||
(its absence made the installed system's interface config unmatchable at boot), and completion is
|
||
signalled by `reboot-mode = "power-off"` — the guest powering itself off — never by a changed disk hash.
|
||
|
||
Three scenarios, all installed to completion:
|
||
|
||
| Scenario | root credential | `[first-boot].ordering` | Install |
|
||
|---|---|---|---|
|
||
| **A** | `root-password-hashed = "*"` (locked) | `fully-up` | completed, powered off after **191 s** |
|
||
| **B** | `root-password-hashed = "*"` (locked) | `before-network` | completed, powered off after **190 s** |
|
||
| **C** | a **known** password (control) | `fully-up` | completed, powered off after **191 s** |
|
||
|
||
**Evidence classes:** **[MEASURED-PVE]** on a real PVE install · **[MEASURED-MECH]** measured in an
|
||
isolated container · **[STRUCTURAL]** read from shipped source or unit files.
|
||
|
||
---
|
||
|
||
## 2. Q1 — is a locked root actually locked, and what else does it lock out?
|
||
|
||
### 2.1 The account's real state — **[MEASURED-PVE]**
|
||
|
||
From the first-boot diagnostic on both locked installs (A and B), emitted to `ttyS0`:
|
||
|
||
```
|
||
passwd_S_root : root L 2026-07-31 0 99999 7 -1
|
||
shadow_shape : literal-asterisk
|
||
sshd_PermitRoot : yes
|
||
pam_common_auth : 1
|
||
pve_pam_realm : 0
|
||
```
|
||
|
||
**The account is genuinely locked** — `passwd -S` reports `L`, and the `*` reached `/etc/shadow`
|
||
verbatim. Note `sshd_PermitRoot: yes`: PVE ships `PermitRootLogin yes`, so the lock is doing the work,
|
||
not sshd's configuration.
|
||
|
||
`pve_pam_realm: 0` — there is no PVE-specific file in `/etc/pam.d/` matching `proxmox|pve`, and
|
||
`pam_unix` is present in `common-auth`. **PVE uses the stock PAM stack**, which is why the §2.2
|
||
container measurement transfers to it rather than merely resembling it.
|
||
|
||
### 2.2 What can authenticate — **[MEASURED-MECH]**, first attempt invalid
|
||
|
||
**The first probe produced nothing and is recorded as failed:** it ran everything as uid 0 so `su`
|
||
never invoked `pam_unix` (`rc=0` even with a deliberately wrong password), `sulogin` died on
|
||
`tcgetattr failed` with no TTY, and a `sed` mangled the control hash. Four cases, zero observations.
|
||
|
||
Corrected (`debian:trixie`, `util-linux 2.41`, non-root caller, real pty via `script`, `chpasswd`):
|
||
|
||
| Shadow field | `passwd -S root` | `su root` as a non-root user | `sulogin` (real pty) |
|
||
|---|---|---|---|
|
||
| real hash (**control**) | `P` | **correct pw → `SU_OK`**; wrong pw → `Authentication failure` | — |
|
||
| `*` | `L` | `Authentication failure` (any password) | `Cannot open access to console, the root account is locked.` |
|
||
| `!` | `L` | `Authentication failure` (any password) | `Cannot open access to console, the root account is locked.` |
|
||
|
||
The control makes it readable: the same harness succeeds with the right password and fails with the
|
||
wrong one, so the locked rows are the module refusing rather than the harness misfiring. **`*` and `!`
|
||
are equivalent.**
|
||
|
||
**On PVE, root SSH was refused on every scenario** — but that row is **uninformative and is not
|
||
evidence**: the probe supplied a deliberately wrong password in the control scenario too, so it cannot
|
||
distinguish "locked" from "wrong password". Recorded as a gap, not a result.
|
||
|
||
### 2.3 Does the Proxmox web interface still work? — **[MEASURED-PVE]. No. It is closed.**
|
||
|
||
The product-level answer, measured against the **exact endpoint the web UI logs in through**
|
||
(`POST /api2/json/access/ticket`, realm `root@pam`), with a working control:
|
||
|
||
| Scenario | Password offered | Result |
|
||
|---|---|---|
|
||
| **C (control, known password)** | **correct** | **HTTP 200, TICKET ISSUED — login would SUCCEED** |
|
||
| C (control) | wrong | HTTP 401, no ticket |
|
||
| C (control) | empty | HTTP 401, no ticket |
|
||
| **A (locked root)** | wrong | HTTP 401, no ticket |
|
||
| **A (locked root)** | empty | HTTP 401, no ticket |
|
||
|
||
The control is what makes this a finding rather than a broken probe: **the same endpoint on the same
|
||
image demonstrably issues a ticket when the password is right.** On the locked install no password can
|
||
produce one, because no password exists.
|
||
|
||
**Therefore a locked root closes the PVE web interface completely — to the customer and to the
|
||
operator alike.** This is a product decision, not a detail: it removes the browser-based route into
|
||
the box entirely, on top of the console route (§3.2).
|
||
|
||
Reachability, incidentally measured on all three: **SSH banner at +20 s, pveproxy TLS at +26–29 s**
|
||
after power-on.
|
||
|
||
### 2.4 Does anything else in PVE break? — **not investigated**
|
||
|
||
Cron, backup jobs and other consumers of a root credential were not examined. `pveproxy` itself runs
|
||
and serves TLS (it answered `/api2/json/version`), so the daemon is unaffected — only authentication
|
||
through it is.
|
||
|
||
---
|
||
|
||
## 3. Q2 — is there a way back into a box whose setup failed?
|
||
|
||
### 3.1 Does the installed GRUB expose a recovery entry? — **[MEASURED-PVE]. Yes.**
|
||
|
||
The installed system's GRUB is PVE's own, unaffected by Felhom's ISO menu collapse (§4). Screendumped
|
||
from the locked install:
|
||
|
||
```
|
||
*Proxmox VE GNU/Linux
|
||
Advanced options for Proxmox VE GNU/Linux
|
||
Memory test (memtest86+x64.bin)
|
||
Memory test (memtest86+x64.bin, serial console)
|
||
```
|
||
|
||
and inside *Advanced options*:
|
||
|
||
```
|
||
*Proxmox VE GNU/Linux, with Linux 7.0.2-6-pve
|
||
Proxmox VE GNU/Linux, with Linux 7.0.2-6-pve (recovery mode)
|
||
```
|
||
|
||
**A recovery entry exists, the menu is editable (`e`), a GRUB command line is available (`c`), and
|
||
there is no GRUB password.** Its timeout is **1 second** — short enough that a first probe missed it
|
||
entirely (§3.5).
|
||
|
||
### 3.2 `sulogin` with a locked root — **[MEASURED-PVE]. It refuses.**
|
||
|
||
Booting *(recovery mode)* on the locked install reaches rescue mode and then stops:
|
||
|
||
```
|
||
Started rescue.service - Rescue Shell.
|
||
Reached target rescue.target - Rescue Mode.
|
||
You are in rescue mode. After logging in, type "journalctl -xb" …
|
||
|
||
Cannot open access to console, the root account is locked.
|
||
See sulogin(8) man page for more details.
|
||
|
||
Press Enter to continue.
|
||
```
|
||
|
||
**The advertised recovery path is closed on a locked-root box** — verbatim the container result,
|
||
now confirmed on real PVE. `systemd` invokes `sulogin` for both `rescue.target` and
|
||
`emergency.target`, so both are closed by the same refusal.
|
||
|
||
### 3.3 What is left — **[MEASURED-PVE]. `init=/bin/bash` works.**
|
||
|
||
Appending `init=/bin/bash` to the kernel line in the GRUB editor and booting produced:
|
||
|
||
```
|
||
Found volume group "pve" using metadata type lvm2
|
||
3 logical volume(s) in volume group "pve" now active
|
||
/dev/mapper/pve-root: clean, 60332/561936 files, 1141450/2243584 blocks
|
||
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
|
||
bash: no job control in this shell
|
||
root@(none):/#
|
||
```
|
||
|
||
**An unauthenticated root shell on a locked-root system.** No password is involved because PID 1 *is*
|
||
the shell — `sulogin` is never reached.
|
||
|
||
| Path | Works? | Realistic for whom |
|
||
|---|---|---|
|
||
| PVE web console (`:8006`) | **No** (§2.3) | — |
|
||
| GRUB *(recovery mode)* → `sulogin` | **No** (§3.2) | — |
|
||
| GRUB editor → `init=/bin/bash` | **Yes** (measured) | **Operator only.** Requires catching a 1-second menu, editing a kernel line, and knowing the incantation. Not a non-technical customer action. |
|
||
| Live USB / rescue media | not tested | Operator only |
|
||
| SSH by key | n/a | Only if a key were baked — which R-142 says must not happen in a public image |
|
||
|
||
**So a locked-root box is recoverable, but only by someone competent at the physical console.** That is
|
||
a materially weaker objection than "no path back at all", which is what the structural reading
|
||
suggested before it was measured.
|
||
|
||
**A security corollary that is worth stating plainly: the installed GRUB has no password, so locking
|
||
root buys nothing against physical access.** Anyone at the keyboard can take a root shell in under a
|
||
minute. Locked root is a defence against *remote* credential reuse — the shared-ISO-password problem —
|
||
and not a physical-security measure. **R-152.**
|
||
|
||
### 3.4 Does locked differ from rotated? — **[MEASURED]. Yes, at exactly one point.**
|
||
|
||
- **Locked (`*`/`!`)** — `sulogin` refuses (§3.2); the web UI refuses (§2.3); `init=/bin/bash` works.
|
||
- **Rotated to a per-box value** — `pam_unix` accepts the correct password (the §2.2 control proves
|
||
this) and the web UI issues a ticket for it (the §2.3 control proves this), so **both** the console
|
||
and the browser open **to whoever holds it**; `init=/bin/bash` works as well.
|
||
|
||
**And this is what settles the escrow question.** A rotated password that is *discarded* is
|
||
operationally identical to a locked account: nobody holds it, so `sulogin` and the web UI refuse
|
||
either way. The only thing escrow buys is that the **operator** keeps the web-UI and `sulogin` routes.
|
||
It does not buy recoverability as such, because `init=/bin/bash` is available in every case.
|
||
|
||
### 3.5 A probe that failed, recorded as failed
|
||
|
||
The first GRUB attempt waited 3 seconds before acting; the menu's timeout is **1 second**, so the
|
||
system had already booted and the keystrokes went into the login prompt — the arrow-key escape
|
||
sequences (`^[[B`) are visible there in the screendump. It tested nothing. A later attempt typed
|
||
`init=/bin/bash` onto the `echo` line one below `linux`, which the **pre-typing verification
|
||
screendump caught** before it was booted. Both were rerun; the numbers above are from the corrected
|
||
runs.
|
||
|
||
---
|
||
|
||
## 4. Q3 — does the Felhom stub survive `before-network`?
|
||
|
||
### 4.1 Does it execute? — **[MEASURED-PVE]. Yes.**
|
||
|
||
The diagnostic emits `@@SPIKE2_HOOK_RAN@@ BEGIN`/`END` to `ttyS0`. On scenario B
|
||
(`ordering = "before-network"`) **both markers are present** (count `2`). This is a positive
|
||
observable the hook produced, which §4.4 explains is the only acceptable form of proof here.
|
||
|
||
### 4.2 Does it have what it needs that early? — **[MEASURED-PVE]. Yes, everything.**
|
||
|
||
### 4.3 Does it complete before anything is network-reachable? — **[MEASURED-PVE + STRUCTURAL]. Yes.**
|
||
|
||
The A/B comparison, same image, same diagnostic, only `ordering` differs:
|
||
|
||
| Diagnostic | **A — `fully-up`** | **B — `before-network`** |
|
||
|---|---|---|
|
||
| hook markers | 2 (ran) | **2 (ran)** |
|
||
| `uptime_s` at hook | 18.32 | **6.58** |
|
||
| `entropy_avail` | 256 | **256** |
|
||
| `etc_writable` | YES | **YES** |
|
||
| `openssl`/`chpasswd`/`systemctl`/`passwd`/`curl`/`ip` | all present | **all present** |
|
||
| `openssl_rand_len` | 32 | **32** |
|
||
| `ip_global` | `vmbr0 10.0.2.15/24` | **(empty)** |
|
||
| `network.target` | active | **inactive** |
|
||
| `network-online.target` | active | **inactive** |
|
||
| `pveproxy` | active | **inactive** |
|
||
| `sshd` | active | **inactive** |
|
||
| **`listen_22_8006`** | **3** | **0** |
|
||
|
||
**At `before-network` the hook runs with entropy, a writable `/etc`, every binary it would need, and a
|
||
demonstrated ability to generate a 32-character random string — while the box has no IP address and
|
||
nothing is listening on `:22` or `:8006`.** A credential rotation placed there has a **zero-length
|
||
exposure window, measured.**
|
||
|
||
Equally, the `fully-up` column is the measured proof of the converse: **today's ordering runs the hook
|
||
when sshd and pveproxy are already listening.** The window Spike 1 argued for structurally is now
|
||
observed.
|
||
|
||
The structural half agrees and generalises it. The shipped unit
|
||
(`proxmox-first-boot` 9.2.5, `/usr/lib/systemd/system/proxmox-first-boot-network-pre.service`):
|
||
|
||
```
|
||
After=systemd-remount-fs.service
|
||
Before=network-pre.target
|
||
Wants=network-pre.target
|
||
ConditionPathExists=/var/lib/proxmox-first-boot/pending-first-boot-setup
|
||
ConditionPathIsReadWrite=/var/lib
|
||
Type=oneshot
|
||
```
|
||
|
||
`Before=network-pre.target` + `Type=oneshot` makes it a hard barrier ahead of all network
|
||
configuration — so this holds on every machine, not just this VM. Both installs then booted to full
|
||
reachability (SSH +20 s, pveproxy +26–29 s), so the early hook does not break the boot.
|
||
|
||
**A third ordering value exists that Spike 1 did not record.** `validate-answer` accepts
|
||
**`before-network`, `network-online`, `fully-up`**; it rejects `network-pre`, `multi-user` and a bogus
|
||
value, so this is a real enum. The shipped package contains a matching unit for each.
|
||
`network-online` is the unevaluated middle option. **R-149.**
|
||
|
||
### 4.4 Does the existing pairing flow still work? — **[STRUCTURAL]**, and it is the one gap left
|
||
|
||
**Not measured.** The scenarios installed the spike's own diagnostic as the first-boot hook, not
|
||
Felhom's `stub-first-boot.sh`, so stub-plus-pairing coexistence is argued, not observed.
|
||
|
||
The structural case is strong and has a correction in it:
|
||
|
||
**The stub does not need `fully-up`.** `stub-first-boot.sh` writes three files, then
|
||
`systemctl daemon-reload`, `enable`, and `start --no-block felhom-bootstrap.service`. A grep for
|
||
`pvesh`, `pct`, `pveum` and `qm` across it returns **no match** (`rc=1`). The unit it starts carries its
|
||
own `After=network-online.target pve-cluster.service pveproxy.service`, so `--no-block` queues the job
|
||
and systemd holds it until those are satisfied.
|
||
|
||
**So `answer.toml.tmpl:27` justifies the current ordering with a dependency the stub does not have** —
|
||
it reads *"fully-up = the stub runs after pveproxy so pvesh/pct work"*, but that usage lives in
|
||
`felhom-bootstrap.sh`, which never runs under the stub's ordering. A comment asserting a **dependency**
|
||
that does not exist, and it has constrained the design ever since. **R-148.**
|
||
|
||
**Why §4.1's positive marker is load-bearing.** All four `proxmox-first-boot-*` units are guarded by
|
||
`ConditionPathExists=` / `ConditionPathIsReadWrite=`, and in systemd **a failed condition is a skip,
|
||
not a failure** — no error, successful boot. A hook that never ran and one that ran perfectly are
|
||
indistinguishable from the absence of errors. **R-150.**
|
||
|
||
---
|
||
|
||
## 5. Q4 — the boot menu, and which entry is the default
|
||
|
||
### 5.1 Where the collapse happens — **[STRUCTURAL]**, and it is not a simple removal
|
||
|
||
`iso-repack.sh` replaces the stock `grub.cfg` wholesale, rendering `grub/grub.cfg.tmpl` over it
|
||
(`:144-148`) after lifting the `linux`/`initrd` lines verbatim from the stock *Install Proxmox VE
|
||
(Automated)* entry (`:108-131`) so a PVE bump tracks automatically.
|
||
|
||
**Four independent gates enforce the single entry:**
|
||
|
||
| Gate | Location | Effect |
|
||
|---|---|---|
|
||
| exactly one `menuentry` | `:153-155` | `exit 14` |
|
||
| zero `submenu` | `:156` | `exit 14` |
|
||
| no live `proxtui`, `proxdebug`, `nomodeset`, `Rescue Boot`, `memtest`, `fwsetup` | `:160-164` | `exit 14` |
|
||
| **re-extracted from `final.iso`**: one entry, theme present, entry named `Felhom telepítés` | `:314-319` | `exit 15` |
|
||
|
||
The fourth proves the menu in the shipped image rather than the build tree — the seam-wiring lesson
|
||
applied correctly.
|
||
|
||
**What a second entry would cost:** the two count gates move from `== 1` to `== 2`, or to a named
|
||
allowlist. **The banned-token gate passes unchanged** for a stock *Install Proxmox VE (Graphical)*
|
||
entry — it uses none of those six tokens. The safety property the gates exist for (no route to a
|
||
debug/rescue/manual-disk-picker path) survives a second graphical-installer entry intact.
|
||
|
||
### 5.2 Are default and timeout settable? — **[STRUCTURAL]. Already are.**
|
||
|
||
`grub/grub.cfg.tmpl` sets `set timeout_style=menu`, `set timeout=5`, `set default=0`. The template also
|
||
records a real defect it fixed: the stock PVE config writes `timeout-style` (hyphen), **which GRUB does
|
||
not recognise as a variable name**, so Felhom's underscore spelling is the one that works.
|
||
|
||
### 5.3 The design question — **framed with evidence, deliberately not ruled**
|
||
|
||
**For interactive-as-default.** Spike 1 proved the automated path cannot be made safe on unseen
|
||
hardware: no property distinguishes a system disk from a customer's backup drive, and a filter matching
|
||
two disks silently wipes one. A tester who boots and walks away on multi-disk hardware is walking away
|
||
from an unattended destructive operation whose target cannot be predicted.
|
||
|
||
**This session strengthens that case independently.** If the public image also ships a locked root,
|
||
then a box whose install succeeds but whose pairing fails has **no web UI** (§2.3) and **no working
|
||
recovery-mode shell** (§3.2) — only an operator-grade `init=/bin/bash` (§3.3). An unattended default
|
||
maximises the number of testers who reach that state without having read anything.
|
||
|
||
**Against.** Unattended install is a selling point and most first boxes are single-disk, where the
|
||
automated path is fine.
|
||
|
||
**Operator-built and public need not match, and two profiles from one build is cheap.** An
|
||
operator-built image targets known disks, so automated-default is safe there; the public image is the
|
||
one facing unseen hardware. That split is available however the default is ruled.
|
||
|
||
---
|
||
|
||
## 6. Q5 — the resulting shape, as constraints
|
||
|
||
Constraints on a build spec, each with a release-gate criterion. **Not an implementation.**
|
||
|
||
| # | Constraint | Release gate |
|
||
|---|---|---|
|
||
| 1 | **No disk selection by property on unseen hardware** (Spike 1 §3.2/§3.3). | Rendered answer contains no `filter.*` key, **or** the image is operator-built for a named machine. |
|
||
| 2 | **The interactive installer must be reachable from the boot menu** (§5.1). | `final.iso`'s `grub.cfg` has ≥1 interactive entry **and** still no live `proxtui`/`proxdebug`/`nomodeset`/`Rescue Boot`/`memtest`/`fwsetup` — keep gate 3, relax gates 1 and 4 to an allowlist. |
|
||
| 3 | **No usable baked credential** — `root-password-hashed = "*"` installs cleanly (§1, three scenarios). | Rendered `root-password-hashed` is exactly `*`, **and no `.rootpw.txt` is emitted**. Two one-line assertions. |
|
||
| 4 | **Any per-box password is set at `before-network`** — the only ordering with a measured zero window (§4.3). | Rendered `[first-boot].ordering` is `before-network`, **and** the hook's positive marker is observed in a boot test (§4.4, R-150). |
|
||
| 5 | **No `root-ssh-keys`, ever, in a public image** (R-142). | `grep -c root-ssh-keys` on the rendered answer is `0`. |
|
||
| 6 | **The profile must be committed** — demo-felhom's baked key comes from an *uncommitted* profile, so a reference box is not reproducible from `main`. | `--profile` resolves inside the repo tree **and** `git status --porcelain` is empty for it. **R-151.** |
|
||
| 7 | **The artefact must be reproducible** (R-144: `nested-probe`'s profile is gone). | Manifest records the committed profile path + repo commit; both resolve. |
|
||
| 8 | **The recovery path must be documented, and named operator-only if it is.** §2.3 + §3.2 + §3.3: with a locked root the only way in is `init=/bin/bash` at the console. | Release notes state it, in those terms. |
|
||
|
||
**What the image must not contain:** a usable root password hash, any `root-ssh-keys`, any customer id,
|
||
claim code, retrieval passphrase or hub token, and any `filter.*` selector in the public variant.
|
||
Spike 1 §6 established by enumeration that `/answer.toml` is the only secret-bearing file, so gates 3
|
||
and 5 over that one rendered file cover the credential surface.
|
||
|
||
---
|
||
|
||
## 7. The operator's decisions
|
||
|
||
**Decision 1 — escrow or discard a per-box root password.** §3.4 reshapes this twice over:
|
||
|
||
- **Discard and lock are the same thing** for recovery — if nobody holds the password, both `sulogin`
|
||
and the web UI refuse.
|
||
- **Neither makes a box unrecoverable.** `init=/bin/bash` works in every case (§3.3), so an operator at
|
||
the console can always get in. The earlier fear that locking root leaves "no path back at all" is
|
||
**measured to be false**.
|
||
|
||
So what escrow actually buys is narrower than it looked: **the operator keeps the web-UI route (§2.3)
|
||
and the `sulogin` route (§3.2) without needing physical presence.** Against that stands D6 — a hub
|
||
holding console root for the whole fleet is exactly what the hub alone must not be.
|
||
|
||
**Recommendation, offered because the measurements now support one:** discard/lock for the **public**
|
||
image, and accept operator-only physical recovery, because the credential that would be escrowed is
|
||
the same one whose fleet-wide sharing started this whole line of work. Escrow remains defensible for
|
||
**operator-built** boxes, where the operator installed the machine anyway. The decision is still the
|
||
operator's; it is no longer blocked on a measurement.
|
||
|
||
**Decision 2 — the default boot entry for a public image.** Framed in §5.3, unruled. The disk evidence
|
||
and the recovery evidence now point the same way (interactive), and the cost is the unattended
|
||
experience. The operator-built-versus-public split is cheap and available either way.
|
||
|
||
---
|
||
|
||
## 8. What is still unknown, and what it would take
|
||
|
||
| # | Unknown | What it takes |
|
||
|---|---|---|
|
||
| 1 | **Felhom's real stub at `before-network`** (§4.4) — coexistence with pairing is structural, not observed. The single remaining gap in Q3. | Build a probe ISO with `stub-first-boot.sh` as the hook and `ordering = "before-network"`, boot, confirm `felhom-bootstrap.service` still starts and pairing completes. ~40 min. **Do this before the spec.** |
|
||
| 2 | **Whether root SSH is refused *because* of the lock** (§2.2) — the probe used a wrong password in the control too, so it discriminates nothing. | Re-run the SSH leg with the control's correct password. ~10 min. |
|
||
| 3 | **What else in PVE assumes a usable root credential** (§2.4) — cron, backup jobs. | Inspect a locked install over a longer uptime. |
|
||
| 4 | **What `network-online` ordering buys** (§4.3) — a real third option, never evaluated. | One extra scenario. |
|
||
| 5 | **Whether the multi-match pick is deterministic by enumeration order** (Spike 1, still open). | Re-run Spike 1's S1 with reversed disk order. ~15 min. |
|
||
| 6 | **Whether the installer excludes its own boot medium** (Spike 1, still open). | USB boot on real hardware — HUMAN. |
|
||
| 7 | **Whether a relaxed-gate two-entry menu actually boots the stock installer** (§5.1). | Not attempted — it would mean changing repo files, which §4 forbids. |
|
||
|
||
---
|
||
|
||
## 9. R-rows opened
|
||
|
||
Continuing from R-147 (Spike 1).
|
||
|
||
| R | Severity | Finding |
|
||
|---|---|---|
|
||
| **R-148** | MEDIUM | **`answer.toml.tmpl:27` justifies `ordering = "fully-up"` with a dependency the stub does not have.** `stub-first-boot.sh` contains no `pvesh`/`pct`/`pveum`/`qm` call (grep `rc=1`); that usage is in `felhom-bootstrap.sh`, under its own `After=network-online.target … pveproxy.service` unit. A comment asserting a **dependency** rather than an invariant — and it blocked the ordering that §4.3 now measures as giving a zero credential window. |
|
||
| **R-149** | LOW | **`[first-boot].ordering` has three values, not two** — `before-network`, `network-online`, `fully-up` validate; `network-pre` and `multi-user` are rejected. The middle option has never been evaluated. |
|
||
| **R-150** | MEDIUM | **A first-boot hook that never runs is indistinguishable from one that succeeded.** All four `proxmox-first-boot-*.service` units are `Condition*`-guarded, and a failed condition is a systemd **skip, not a failure** — no error, successful boot. Any credential-rotation-in-first-boot design must be verified by a positive marker the hook emitted. |
|
||
| **R-151** | MEDIUM | **A reference box is not reproducible from the repository.** `demo-felhom` was installed from an ISO built with the **uncommitted** `n100-demo.profile`, which is where its baked SSH key comes from (Spike 1 §4.6). Gate 6 in §6 exists to stop a public image inheriting that shape. |
|
||
| **R-152** | LOW | **A locked root is not a physical-security measure, and should never be described as one.** The installed PVE GRUB has **no password** (§3.1), so `init=/bin/bash` yields an unauthenticated root shell in under a minute (§3.3, measured). Locking root defends against *remote* reuse of a shared ISO credential and nothing else. If a physical-access threat model is ever wanted, it needs a GRUB password, which is a separate decision with its own lockout risk. |
|
||
|
||
---
|
||
|
||
## 10. Teardown
|
||
|
||
All three layers, each verified positively.
|
||
|
||
**Layer 1 — demo-hp.**
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| QEMU processes | `0` |
|
||
| `/mnt/nvme-1tb/spike2/` | removed — `No such file or directory` |
|
||
| `/tmp/paia` | removed |
|
||
| loop devices / `/mnt/s2ans` | `0` / removed |
|
||
| `/mnt/nvme-1tb/` contents | `dump e2d-images felhom-data images lost+found` — the pre-spike set |
|
||
| disk usage | **6.6 G — identical to pre-spike** |
|
||
| `drill-r50` (VM 300) | `stopped`, untouched (R-93 fixture intact) |
|
||
| guest 9201 | `running`, untouched |
|
||
| `/etc/pve/storage.cfg` | **4 storages, unchanged**; no scratch storage ever registered |
|
||
| `local-lvm` | never written |
|
||
|
||
**Layer 2 — demo-felhom.** `/tmp/paia` removed; guest 9201 `running`; the external USB drive
|
||
`931.5G usb /mnt/hdd_1` still attached and mounted exactly as found — never attached, detached,
|
||
partitioned or written. Only `device-info` reads were ever performed there.
|
||
|
||
**Layer 3 — DooPlex.**
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| scratchpad | **1.7 GB → 60 K** (probe ISO, screendumps, squashfs/`.deb` extracts, schema test dirs) |
|
||
| control password (`s2/.ctlpw`) and `scen/C.toml` | `shred -u`'d / removed; verified absent. Never printed, never committed |
|
||
| mechanism-probe container | `docker run --rm`; **no global Docker cleanup run** (fence) |
|
||
| `felhom-iso/out/` | untouched, **17 ISOs** |
|
||
| production files | none modified — no `build-felhom-iso.sh`, no `iso-repack.sh`, no profile, no `felhom-bootstrap.sh` |
|
||
|
||
**Hub-side disposition — nothing to dispose.** No customer, host or appliance was created: the nested
|
||
VMs ran under plain QEMU with user-mode networking and no route to the hub, and no install ever
|
||
contacted it. **R-131 gains no row.**
|
||
|
||
**Published artefacts: none.** Nothing went to R2.
|