155 lines
6.6 KiB
Markdown
155 lines
6.6 KiB
Markdown
# 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.
|