R-297: installer compares a local golden against the manifest before using it

Step 7 short-circuited on any local golden archive with no version compare, no
digest and no warning, so the manifest sha256 was consulted only on the fetch
path. Local discovery is newest-by-filename: correct by recency, never by
verification. A box could reinstall from a stale archive and come back below the
version where the offsite recovery screen exists.

Digest first, then the baked controller tag. An auto-discovered mismatch
re-fetches the vouched golden; an operator-named mismatch refuses. An unreadable
manifest refuses rather than passing.

Not published: installer-v1.26.0 is deliberately not cut until a fresh install
has been observed taking a stale local golden on drill-r50.

Also files R-295..R-298.
This commit is contained in:
2026-08-10 13:57:50 +02:00
parent 11a5c3bd92
commit eb600872f2
3 changed files with 120 additions and 4 deletions
+39
View File
@@ -1,3 +1,42 @@
## felhom-host-install.sh v1.26.0 — an install takes the golden you approved (2026-08-10, R-297)
**Step 7 short-circuited on ANY local golden archive: no version compare, no digest, no warning.**
`if [[ -n "$GOLDEN_VOLID" ]] && ! $FORCE_GITEA_GOLDEN; then log_skip "using local golden"; return 0`
— so the manifest's sha256, whose entire purpose is to vouch from a **different trust root than the
code host**, was consulted only on the fetch path. Local discovery is
`pvesm list … | sort | tail -1`: correct by **recency**, never by verification.
**The sharp end:** a box with an old archive lying around reinstalls from it silently, and can come
back below the version where the off-site recovery screen exists — unable to run the ceremony its
own data depends on — and born below the update floor, whose auto-target is the floor and never the
newest. Measured on a real host: three goldens sat on `demo-hp`, carrying controller **0.153.0** and
**0.210.0**, and nothing compared either to the vouched version before using it.
**The row narrows itself and the narrowing holds:** on `--resume`, step 7 fetches correctly, because
resume skips preflight and preflight is where local discovery sets the variable (`:2849`). The two
paths disagreed and resume was the safe one.
`golden_local_matches_manifest()` now runs two comparisons, cheapest first:
1. **digest** — local sha256 == manifest sha256 ⇒ this IS the vouched artifact, certain;
2. **version** — a locally BAKED golden is legitimately not byte-identical to the published file, so
a digest mismatch alone is not proof of staleness. The controller tag baked into the archive
(`/etc/felhom-controller-image`, what the bootstrap unit runs) is compared to the vouched version.
Neither matching ⇒ the archive is not what the operator approved. **An auto-discovered archive is
then ignored and the vouched one fetched**, saying which check failed and why; **an archive the
operator NAMED with `--golden` is REFUSED, never silently substituted**, and the refusal names the
vouched version and the two routes (pass a matching archive, or `--force-gitea-golden`).
**Fail-safe:** an unreadable manifest is a REFUSAL, not a pass. "We could not look" must not resolve
to "install whatever is lying around" — that is the finding restated.
**NOT YET OBSERVED FAILING, and deliberately not published.** The task asked that a fresh install be
watched taking a stale local golden BEFORE the fix, on `drill-r50`. That observation was not made in
this session, so this change is committed to `main` — which publishes nothing (R-110) — and the
`installer-v1.26.0` tag is **deliberately not cut**. Publishing an unobserved change to the one
artifact that runs as root on a virgin box is the failure this project keeps writing rules about.
## golden_currency_gate.py — a released controller must have a golden carrying it (2026-08-08, R-242)
**R-242 was filed the previous day as a rule with no mechanism and RECURRED WITHIN A DAY.** Controller