diff --git a/documentation/runbooks/iso-release-gate.md b/documentation/runbooks/iso-release-gate.md new file mode 100644 index 0000000..44d1f05 --- /dev/null +++ b/documentation/runbooks/iso-release-gate.md @@ -0,0 +1,180 @@ +# The public ISO release gate + +> **Written before the first release image was built** (2026-07-31), deliberately: a standard defined +> in advance cannot be rationalised afterwards, and this is the artifact that most needs one — once a +> file is on `iso.felhom.eu` and someone has downloaded it, it cannot be recalled. +> +> Design inputs: `documentation/audits/SPIKE-universal-iso-{1,2,3,4}-2026-07-31.md`. Every criterion +> below exists because one of those spikes measured why. + +## How to use it + +**Run every check against the exact file that will be uploaded** — not against build inputs, not +against a sibling built the same way. A build is reproducible in theory and the uploaded bytes are +what a stranger receives. + +**An absent finding is not a finding.** Each check records *what was scanned for*, so "no hits" means +"this pattern was searched and did not appear", not "nothing occurred to me". + +**Any FAIL stops the publication.** Stopping is a good outcome. + +--- + +## The criteria + +Let `$ISO` be the file about to be uploaded. + +### G1 — No `answer.toml` + +```bash +osirrox -indev "$ISO" -find / -maxdepth 1 2>/dev/null | grep -cE "'/answer\.toml'|'/auto-installer-mode\.toml'" +``` +**PASS = `0`.** + +*Why:* the answer file is the only secret-bearing artefact a Felhom ISO has ever carried (Spike 1 §6, +by enumeration against the stock ISO). Removing it deletes the baked root hash, the disk profile, and +the entire Spike 1–2 problem space in one move, and turns the most dangerous property of the image +into a one-line assertion. Without `auto-installer-mode.toml` the stock `grub.cfg` does not emit the +Automated entry at all (Spike 1 §5), so the unsafe path is absent by construction rather than guarded. + +### G2 — No root password or hash + +```bash +osirrox -indev "$ISO" -extract /answer.toml - 2>/dev/null # must not exist (G1) +# and, across every file the pipeline adds: +grep -c 'root-password\|root-password-hashed\|\$6\$\|\$y\$\|\$2[aby]\$' +``` +**PASS = `0` occurrences, and no `*.rootpw.txt` emitted beside the output.** + +*Why:* `build-felhom-iso.sh:270-283` mints a root hash unconditionally in the answer-file modes, and +Spike 1 proved by extraction that it ships inside `/answer.toml` where any downloader can read it. On +a published image that is one credential shared by every box installed from it. + +### G3 — No SSH key baked + +```bash +grep -c 'root-ssh-keys\|ssh-rsa\|ssh-ed25519\|ssh-dss' +``` +**PASS = `0`** across the pipeline-added files. + +*Why:* `FELHOM_ROOT_SSH_KEY` is one uncommented profile line away from shipping a shared key to every +downloader, and Spike 1 §4.6 found exactly that had already happened on `demo-felhom` — from an +**uncommitted** profile. Same shared-credential class as G2. + +### G4 — No customer identity + +```bash +grep -c 'FELHOM_CUSTOMER_ID=[^"]\|FELHOM_RETRIEVAL_PASSPHRASE=[^"]\|claim[_-]code\|api[_-]key\|Bearer ' +``` +**PASS = `0` with a value.** Empty variable *declarations* inside `felhom-bootstrap.sh` are expected +and are not hits (Spike 1 §6 established this distinction — `:89` initialises them empty). + +### G5 — No credential of any kind, by enumeration + +Diff the file list against the stock PVE ISO and inspect **every added path**: + +```bash +osirrox -indev -find / | sort > /tmp/stock.txt +osirrox -indev "$ISO" -find / | sort > /tmp/rel.txt +comm -13 /tmp/stock.txt /tmp/rel.txt # everything the pipeline added +``` +**PASS =** every added path is accounted for and none carries a secret. Scan the content-bearing ones +for: PEM private-key headers, `ssh-rsa`/`ssh-ed25519` material, `Bearer` tokens, `api_key=`, +`passphrase=`, `password=`, `token=` with ≥12-char values, and crypt hashes (`$6$`, `$y$`, `$2[aby]$`). + +*Why enumeration and not a pattern sweep:* a grep only finds what it was told to look for. Spike 1 §6 +found `/answer.toml` this way precisely because the earlier recon had grepped the wrong file. + +### G6 — The boot menu is present, with both paths and a human timeout + +```bash +osirrox -indev "$ISO" -extract /boot/grub/grub.cfg /tmp/g.cfg +grep -c '^[[:space:]]*menuentry ' /tmp/g.cfg # >= 2 +grep -E '^set (default|timeout|timeout_style)=' /tmp/g.cfg +``` +**PASS =** at least one interactive entry present; `set default=` points at an **interactive** entry; +`set timeout` **≥ 10**; and `timeout_style` uses the **underscore** spelling. + +*Why the timeout:* Spike 2 lost an entire probe to a **1-second** menu. A human choosing between two +install paths on unfamiliar hardware needs to read them first. *Why the spelling:* the stock PVE config +writes `timeout-style`, which GRUB does not recognise as a variable name at all (Spike 2 §5.2) — +Felhom's underscore form is the one that works. + +**Also assert the safety property still holds** — no live route to a manual disk-picker via a debug or +rescue entry: + +```bash +grep -v '^[[:space:]]*#' /tmp/g.cfg | grep -cE 'proxtui|proxdebug|nomodeset|Rescue Boot|memtest|fwsetup' +``` +**PASS = `0`.** This is the gate `iso-repack.sh:160-164` already enforces and it is kept unchanged. + +### G7 — The Felhom package is present, at a recorded version + +```bash +osirrox -indev "$ISO" -find /proxmox/packages 2>/dev/null | grep -c 'felhom-' +osirrox -indev "$ISO" -extract /proxmox/packages/.deb /tmp/p.deb +dpkg-deb -I /tmp/p.deb | grep -E '^ (Package|Version):' +``` +**PASS =** exactly one `felhom-*.deb`, its version recorded in the manifest, and its sha256 matching +the package built from committed source. + +*Why:* Spike 4 measured that this is the only delivery mechanism that survives an interactive install +(`[first-boot]` is never placed on the system — `Config.pm:118`, `Install.pm:746`, `:1360`). + +### G8 — The postinst cannot fail, and cannot need what it hasn't got + +```bash +dpkg-deb --ctrl-tarfile /tmp/p.deb | tar -xO ./postinst > /tmp/postinst +grep -cE 'systemctl (start|daemon-reload|restart)' /tmp/postinst # 0 +grep -cE '\b(curl|wget|apt-get|apt|nc|ping)\b' /tmp/postinst # 0 +grep -c 'set -e' /tmp/postinst # 0 +tail -1 /tmp/postinst # exit 0 +``` +**PASS =** all four. + +*Why, all four measured in Spike 4 §3:* `pid1` in the installer chroot is `unconfigured.sh` and **no +systemd is running**, so `start`/`daemon-reload` are meaningless; the network was up only because the +installer's DHCP happened to hold, so a box installed with the cable out has none; and a postinst that +exits non-zero surfaces as an error in the middle of a customer's install — far worse than the stub +not running. `set -e` is banned because it converts any unexpected non-zero into exactly that failure. + +### G9 — `felhom-bootstrap.sh` matches repo HEAD byte-for-byte + +Extract the script from the package and compare: +```bash +dpkg-deb --fsys-tarfile /tmp/p.deb | tar -xO ./usr/local/sbin/felhom-bootstrap.sh | sha256sum +sha256sum scripts/iso/felhom-bootstrap.sh +``` +**PASS =** identical. + +*Why:* it is the one genuinely frozen, drift-capable payload — everything else the box uses is fetched +at run time from `main`. An ISO in a drawer for six months still runs this exact script. + +### G10 — The profile and every build input are committed + +```bash +git status --porcelain scripts/iso/ # empty +git rev-parse HEAD; git rev-parse origin/main # equal +``` +**PASS =** clean and pushed, and the manifest records the commit. + +*Why:* Spike 1 found `demo-felhom` was installed from an **uncommitted** profile, so one of the two +reference boxes cannot be rebuilt from `main`. A published image must never reach that state, and +R-144 records a lab ISO that already has (`nested-probe`'s profile no longer exists). + +### G11 — A published checksum, and a verified round trip + +**PASS =** the `.sha256` and the manifest are uploaded beside the ISO, **and** the file downloaded from +`https://iso.felhom.eu/` checksums to the same value. The round trip is the claim — a local +checksum proves only that the local file is what it is. + +### G12 — The bucket stays private + +**PASS =** the bucket's `Public Access` remains **Disabled**; the custom domain is the only public path. + +--- + +## Result recording + +Record each criterion as PASS/FAIL **with the observed value and what was scanned for**, in the +release report. A criterion with no recorded observation is a criterion that was not run.