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:
@@ -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
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_VERSION="1.25.0" # the SINGLE version source (F-1): -h and the run banners follow it.
|
||||
SCRIPT_VERSION="1.26.0" # the SINGLE version source (F-1): -h and the run banners follow it.
|
||||
# The hub used to carry a copy for its Setup tab; R-94 DELETED it
|
||||
# (2026-08-02) because the hub cannot know which version a box runs —
|
||||
# the Setup command fetches this script at run time. scripts/
|
||||
@@ -228,6 +228,10 @@ HUB_URL="https://hub.felhom.eu"
|
||||
VMID="9201"
|
||||
VMID_EXPLICIT=false # set true when --vmid is given; gates the auto-pick-a-free-vmid behavior
|
||||
GOLDEN_VOLID=""
|
||||
# R-297: true only when the operator named the archive with --golden, so a mismatch REFUSES
|
||||
# (never silently substitutes) rather than re-fetching.
|
||||
GOLDEN_VOLID_EXPLICIT=false
|
||||
GOLDEN_CHECK_WHY=""
|
||||
GOLDEN_VMID="9100"
|
||||
ARCHIVE_STORAGE="local"
|
||||
NODE=""
|
||||
@@ -1230,7 +1234,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--mode) MODE="$2"; shift 2 ;;
|
||||
--hub-url) HUB_URL="$2"; shift 2 ;;
|
||||
--vmid) VMID="$2"; VMID_EXPLICIT=true; shift 2 ;;
|
||||
--golden) GOLDEN_VOLID="$2"; shift 2 ;;
|
||||
--golden) GOLDEN_VOLID="$2"; GOLDEN_VOLID_EXPLICIT=true; shift 2 ;;
|
||||
--golden-vmid) GOLDEN_VMID="$2"; shift 2 ;;
|
||||
--archive-storage) ARCHIVE_STORAGE="$2"; shift 2 ;;
|
||||
--node) NODE="$2"; NODE_EXPLICIT=true; shift 2 ;;
|
||||
@@ -2642,12 +2646,81 @@ PY
|
||||
# Local auto-discovery is the default + fallback. When no local golden exists (or --force-gitea-golden),
|
||||
# fetch the golden from Gitea (git token), VERIFY its sha256 against the hub manifest, and import it
|
||||
# into the archive storage's dump dir under a valid vzdump name so the provision restore can use it.
|
||||
# R-297 — A LOCAL GOLDEN IS NOT A VOUCHED GOLDEN UNTIL IT HAS BEEN COMPARED TO THE MANIFEST.
|
||||
#
|
||||
# Step 7 used to short-circuit on ANY local archive: no version compare, no digest, no warning. The
|
||||
# manifest's sha256 — whose whole purpose is to vouch from a different trust root than the code host
|
||||
# — was consulted only on the fetch path. Local discovery is `sort | tail -1`: correct by RECENCY,
|
||||
# never by verification. A box with an old archive lying around therefore reinstalled from it
|
||||
# silently, and could 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.
|
||||
#
|
||||
# Two comparisons, cheapest first:
|
||||
# 1. DIGEST — if the local file's sha256 equals the manifest's, it IS the vouched artifact. Certain.
|
||||
# 2. VERSION — a locally BAKED golden is not byte-identical to the published one, so a digest
|
||||
# mismatch is not by itself proof of staleness. The controller version baked into the archive
|
||||
# (/etc/felhom-controller-image, the tag the bootstrap unit runs) is then compared to the
|
||||
# manifest's vouched version.
|
||||
# Neither matching ⇒ the archive is not what the operator approved.
|
||||
#
|
||||
# Reading the marker streams the archive until the entry is found (--occurrence=1 stops there), which
|
||||
# is why the digest is tried first: on a previously-FETCHED golden the cheap check settles it.
|
||||
golden_local_matches_manifest() {
|
||||
local volid="$1" path want_sha got_sha marker ver
|
||||
path=$(pvesm path "$volid" 2>/dev/null)
|
||||
if [[ -z "$path" || ! -f "$path" ]]; then
|
||||
GOLDEN_CHECK_WHY="the archive could not be resolved to a file on disk"
|
||||
return 1
|
||||
fi
|
||||
want_sha="$ART_GOLDEN_SHA"
|
||||
if [[ -z "$ART_GOLDEN_VER" || -z "$want_sha" ]]; then
|
||||
# We could not look. That must never read as "it is fine" — the whole finding is a stale
|
||||
# archive being taken on trust.
|
||||
GOLDEN_CHECK_WHY="the hub manifest has no vouched golden version/sha256 to compare against"
|
||||
return 1
|
||||
fi
|
||||
got_sha=$(sha256sum "$path" 2>/dev/null | awk '{print $1}')
|
||||
if [[ -n "$got_sha" && "$got_sha" == "$want_sha" ]]; then
|
||||
log_info " local golden digest matches the manifest (${got_sha:0:16}…) — this IS the vouched artifact"
|
||||
return 0
|
||||
fi
|
||||
marker=$(tar --zstd -xOf "$path" --occurrence=1 ./etc/felhom-controller-image 2>/dev/null | tr -d '[:space:]')
|
||||
ver="${marker##*:}"
|
||||
if [[ -z "$ver" ]]; then
|
||||
GOLDEN_CHECK_WHY="its digest does not match the manifest and it carries no readable controller version marker"
|
||||
return 1
|
||||
fi
|
||||
if [[ "$ver" == "$ART_GOLDEN_VER" ]]; then
|
||||
log_info " local golden is controller $ver — the vouched version (digest differs: locally baked, not the published file)"
|
||||
return 0
|
||||
fi
|
||||
GOLDEN_CHECK_WHY="it is controller $ver, but the vouched golden is $ART_GOLDEN_VER"
|
||||
return 1
|
||||
}
|
||||
|
||||
step_golden() {
|
||||
log_step "7/8 golden archive"
|
||||
|
||||
if [[ -n "$GOLDEN_VOLID" ]] && ! $FORCE_GITEA_GOLDEN; then
|
||||
log_skip " using local golden: $GOLDEN_VOLID"
|
||||
_state_mark golden; return 0
|
||||
# The manifest is needed to compare; resolve it here rather than trusting the archive.
|
||||
# resolve_artifacts dies with its own message if the manifest cannot be read. That is
|
||||
# deliberate: without a vouched version there is nothing to compare an archive against, and
|
||||
# "we could not look" must not resolve to "install whatever is lying around".
|
||||
[[ -n "$ART_GOLDEN_VER" ]] || resolve_artifacts
|
||||
GOLDEN_CHECK_WHY=""
|
||||
if golden_local_matches_manifest "$GOLDEN_VOLID"; then
|
||||
log_skip " using local golden: $GOLDEN_VOLID"
|
||||
_state_mark golden; return 0
|
||||
fi
|
||||
if $GOLDEN_VOLID_EXPLICIT; then
|
||||
# The operator named this archive. Never silently substitute a different one.
|
||||
die "refusing the golden you named ($GOLDEN_VOLID): ${GOLDEN_CHECK_WHY}.
|
||||
The vouched golden is ${ART_GOLDEN_VER:-<unknown>}. Either pass the archive that matches it,
|
||||
or re-run with --force-gitea-golden to fetch the vouched one from Gitea."
|
||||
fi
|
||||
log_warn " ignoring the local golden $GOLDEN_VOLID — ${GOLDEN_CHECK_WHY}"
|
||||
log_warn " fetching the vouched golden instead (this is what the manifest is for)"
|
||||
GOLDEN_VOLID=""
|
||||
fi
|
||||
|
||||
# Need the manifest + git creds (already resolved in step 5, but re-resolve on a fresh --resume run).
|
||||
|
||||
Reference in New Issue
Block a user