diff --git a/docs/security/KEY-02-ROOTFS-EVIDENCE.md b/docs/security/KEY-02-ROOTFS-EVIDENCE.md new file mode 100644 index 00000000..bf3b3dec --- /dev/null +++ b/docs/security/KEY-02-ROOTFS-EVIDENCE.md @@ -0,0 +1,154 @@ +# KEY-02 — build-host evidence for the rootfs identity strip + +**Audit item:** C-4 of `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` (§868), which +belongs to finding **F-03** (fail-open, never-retried first-boot secret regeneration over +a fleet-shared rootfs). + +**Status: ⛔ UNVERIFIED — awaiting a run on a real ISO build host.** + +The code change is committed and unit-tested; the tar listing that proves its effect on a +real build has not been produced yet, because it requires a build host with podman/docker +and enough disk for a full rootfs rebuild. Do not read anything below the "Result" heading +as a passing check until it is filled in. + +| Field | Value | +|---|---| +| Builder commit (Task 1) | `21043096` — fail-closed first-boot regeneration | +| Builder commit (Task 2) | `408b328c` — rootfs identity strip | +| Builder file | `image-recipe/_archived/build-auto-installer-iso.sh` (LIVE; `image-recipe/build-debian-iso.sh` execs it) | +| Build host | _to be recorded_ | +| Date run | _to be recorded_ | +| RECIPE_HASH observed | _to be recorded — read it from the stamp file, see the caveat below_ | + +--- + +## The expectation is deliberately INVERTED relative to the audit + +This is the single most important thing to understand when comparing this document with the +audit, and the reason it is stated before the commands rather than after. + +The audit's C-4 entry says: + +> **Expected:** SSH host keys and the TLS key **present** (they are baked — see +> `build-auto-installer-iso.sh:345`, `:463-469`), `random-seed` **absent**, `machine-id` +> absent or zero-length. Anything else changes F-03's severity. + +That expectation described the **broken** state the audit found, and recording it was how the +audit measured the size of F-03. Phase 10 plan 10-03 Task 2 removed that material. So: + +**After this change, the audit's stated expectation is the FAILURE condition.** If SSH host +keys or the TLS private key still appear in the tar, the strip layer did not run — most +likely because a cached `archipelago-rootfs.tar` was reused. That is not a regression in the +check; it is the check working. + +The two negative findings the audit recorded are unchanged and must still hold: +`var/lib/systemd/random-seed` absent, `etc/machine-id` absent or zero-length. + +--- + +## Commands to run + +Run all of these **on the build host**, from the repo root, on a checkout that contains +commits `21043096` and `408b328c`. + +### 1. Force a full rebuild + +The strip layer lives inside the `RECIPE_HASH` region (between the `# STEP 1: Build complete +root filesystem` and `# STEP 2: Build minimal installer` markers), so the hash changes and the +cached tar is invalidated automatically. `--rebuild` is passed anyway so that a stale tar +cannot mask the result for any reason: + +```bash +UNBUNDLED=1 bash image-recipe/build-debian-iso.sh --rebuild +``` + +`UNBUNDLED=1` is mandatory per `CLAUDE.md` and project memory — the default env silently +builds the wrong full-bundle variant. + +### 2. List the identity artefacts in the shipped tar + +`WORK_DIR` is `image-recipe/build/auto-installer`, so: + +```bash +tar -tvf image-recipe/build/auto-installer/archipelago-rootfs.tar \ + | grep -E 'etc/ssh/ssh_host|etc/machine-id|var/lib/systemd/random-seed|archipelago/ssl/archipelago' +``` + +### 3. Expected result after this plan + +- **no** `etc/ssh/ssh_host_*` entries at all +- **no** `etc/archipelago/ssl/archipelago.key` and **no** `archipelago.crt` + (the `etc/archipelago/ssl/` **directory** must still be present — the first-boot staging + swap needs somewhere to land) +- **no** `var/lib/systemd/random-seed` +- `etc/machine-id` present with size **0**, or absent. Either satisfies "not shared"; record + which one was actually observed rather than generalising. + +### 4. Confirm the provenance file rode along + +```bash +tar -tvf image-recipe/build/auto-installer/archipelago-rootfs.tar | grep rootfs-identity-stripped +``` + +Expected: one entry, `opt/archipelago/rootfs-identity-stripped`. Its absence means the strip +layer did not execute and the whole check is void. + +### 5. Confirm the regeneration path is still shipped + +This is the brick check, and it is not optional. A stripped rootfs whose first-boot +regeneration script failed to ship would leave every flashed node with no SSH host key and +nothing to create one. + +```bash +ls -l image-recipe/build/auto-installer/installer-iso/archipelago/scripts/first-boot-secrets.sh \ + image-recipe/build/auto-installer/installer-iso/archipelago/scripts/archipelago-first-boot-secrets.service +``` + +Expected: both present, `first-boot-secrets.sh` executable. + +### 6. Record the RECIPE_HASH the builder actually used + +```bash +cat image-recipe/build/auto-installer/archipelago-rootfs.recipe.sha256 +``` + +**Caveat — do not compute this hash from the repo file.** `image-recipe/build-debian-iso.sh` +copies the archived builder to a temp path and rewrites its relative paths before exec'ing it, +and `RECIPE_HASH` hashes `"$0"` — the rewritten copy. The hashed region contains 35 such +rewritten path expressions, and `SCRIPT_DIR` is substituted with an absolute path, so the hash +is specific to the build host and checkout location. For reference, hashing the region of the +committed repo file directly gives `d2dc4df5427fe73d48227aab08cdf6debfe8dd554e6b18e3718f8d37ea9d675c`, +which is **expected to differ** from the stamp above. + +--- + +## Result + +_Paste the raw output of steps 2, 4, 5 and 6 here, then set the status at the top of this +document to VERIFIED with the date and build-host label._ + +```text +(pending — not yet run on a build host) +``` + +**Verdict:** _pending_ + +--- + +## What this does and does not prove + +**Proves (once run):** the rootfs tar extracted verbatim onto every disk flashed from the ISO +carries no SSH host key, no TLS private key and no populated machine-id — so a first-boot +regeneration failure degrades to "no key, the service refuses to start" rather than +"fleet-shared key, silently", which is the substance of F-03. + +**Does not prove:** that two nodes flashed from the same ISO actually end up with different +keys. That is audit item **C-3** (§779) and needs two physical machines; it remains +separately UNVERIFIED. C-4 is a build-host check only. + +**Also worth recording when C-3 is run:** the installer writes a per-install TLS keypair as a +fallback (`build-auto-installer-iso.sh`, the "Ensure SSL cert exists for nginx HTTPS" block). +That fallback fires on every install now that the rootfs ships without a keypair. It is +generated separately on each target machine, so it is per node and does not reopen F-03 — but +it does mean the two nodes' *TLS* certs will differ even if first-boot regeneration failed on +both. SSH host keys have no such fallback, so they are the sharper signal for C-3.