From 96acd388d7ff6984c6021a3d7c89e3592bad7509 Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 9 Aug 2026 04:52:18 -0400 Subject: [PATCH] fix(catalog): generate against the fleet's trust floor, not the repo's host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sign-catalog.sh refused to sign a freshly regenerated catalog: REFUSING: the catalog names registry hosts the deployed fleet does not trust. source.archipelago-foundation.org — 78 image refs It was right, and the check earned its place. apps/*/manifest.yml moved to the public domain in 8e814ca0, but releases/registry-trust-floor.json still lists only docker.io, ghcr.io, localhost and the OVH host — the migration is ship-binary -> confirm-fleet -> promote-floor -> regenerate, and only the first move happened. A full regen therefore produced a catalog naming a host no deployed binary trusts, which would have failed every install in the field with "not from a trusted registry". Verifying that an image PULLS from the domain (it does, including from a fleet node) does not verify that shipped binaries ACCEPT it — different checks. Two fixes, no manifest churn: - image-versions.sh honours a caller-supplied ARCHY_REGISTRY instead of overwriting it, so generation can target a host inside the floor. - the generator retargets OUR registry host inside embedded manifests to whatever it is generating against, leaving docker.io/ghcr.io refs alone. REGISTRY moves above the embed block, which now depends on it. The repo keeps naming the public domain; only the generated artifact is pinned to what the fleet can actually use. When the floor is promoted, generating against the domain becomes a no-op here. Verified: check-catalog-registry-trust.py passes, 11 docker.io/ghcr.io refs untouched, and the diff against the signed catalog is exactly one app — bitcoin-knots, :latest/20260508 -> :29.3.knots20260210 across all four fields. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/generate-app-catalog.sh | 36 +++++++++++++++++++++++++++++++-- scripts/image-versions.sh | 11 +++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/scripts/generate-app-catalog.sh b/scripts/generate-app-catalog.sh index 265691f6..1a75eb74 100755 --- a/scripts/generate-app-catalog.sh +++ b/scripts/generate-app-catalog.sh @@ -143,6 +143,39 @@ for app_id, comps in STACK.items(): # into an AppManifest. Apps not already in SINGLE/STACK get a new entry whose # version comes from the manifest. A bad embed is harmless: the node validates and # falls back to its disk manifest. +# Embedded manifests must name a registry the DEPLOYED fleet trusts, which is +# not necessarily the one the repo names. apps/*/manifest.yml moved to the public +# domain in 8e814ca0, but releases/registry-trust-floor.json still lists only the +# OVH host — the migration is ship-binary -> confirm-fleet -> promote-floor -> +# regenerate, and the later steps have not happened. Embedding the repo's host +# verbatim produced a catalog naming 78 untrusted refs, which would have made +# every install in the field fail with "not from a trusted registry". The signer +# refused it, which is how this was caught. +# +# So rewrite OUR registry host to whatever REGISTRY is generating against, and +# leave every other host (docker.io, ghcr.io, ...) untouched. When the floor is +# promoted, generating against the domain becomes a no-op here. +REGISTRY = os.environ.get("ARCHY_REGISTRY", "source.archipelago-foundation.org/lfg2025") + +_KNOWN_ARCHY_REGISTRY_HOSTS = ( + "source.archipelago-foundation.org/lfg2025", + "146.59.87.168:3000/lfg2025", +) + + +def _retarget_registry(node): + if isinstance(node, dict): + return {k: _retarget_registry(v) for k, v in node.items()} + if isinstance(node, list): + return [_retarget_registry(v) for v in node] + if isinstance(node, str): + for host in _KNOWN_ARCHY_REGISTRY_HOSTS: + if host != REGISTRY and node.startswith(host + "/"): + return REGISTRY + node[len(host):] + return node + return node + + embedded = 0 apps_dir = os.environ.get("APPS_DIR") if os.environ.get("EMBED_MANIFESTS") and apps_dir: @@ -159,7 +192,7 @@ if os.environ.get("EMBED_MANIFESTS") and apps_dir: continue entry = apps.setdefault(str(app_id), {}) entry.setdefault("version", str(app.get("version", "")) or "0") - entry["manifest"] = data + entry["manifest"] = _retarget_registry(data) embedded += 1 # Multi-version support (docs/bitcoin-multi-version-design.md §3 Phase 1): @@ -172,7 +205,6 @@ if os.environ.get("EMBED_MANIFESTS") and apps_dir: # image.sh (Phase 0) publishes more tagged images, e.g.: # {"version": "30.0", "image": f"{REGISTRY}/bitcoin:30.0"}, # {"version": "27.2", "image": f"{REGISTRY}/bitcoin:27.2", "deprecated": True, "eol": "2026-12-31"}, -REGISTRY = os.environ.get("ARCHY_REGISTRY", "source.archipelago-foundation.org/lfg2025") VERSIONS = { # Curated Core set (latest patch per major, current → 25). Images built + # verified (SHA-256 + OpenPGP, fail-closed) and pushed by diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh index 1a5b4f11..646bc075 100644 --- a/scripts/image-versions.sh +++ b/scripts/image-versions.sh @@ -10,7 +10,16 @@ # to verify against the registry. # Archipelago app registries (primary + fallback) -ARCHY_REGISTRY="source.archipelago-foundation.org/lfg2025" +# +# Honour a caller-supplied ARCHY_REGISTRY instead of overwriting it. Catalog +# generation MUST be able to target a host inside releases/registry-trust-floor.json +# — the hosts binaries already deployed to the fleet are known to trust. This +# file's default is the public domain, which the fleet does NOT trust yet: the +# migration is ship-binary -> confirm-fleet -> promote-in-floor -> regenerate, +# and only step 0 has happened. Generating the catalog against an untrusted host +# makes every install in the field fail with "not from a trusted registry"; the +# signer refuses to sign such a catalog, which is how this was caught. +ARCHY_REGISTRY="${ARCHY_REGISTRY:-source.archipelago-foundation.org/lfg2025}" # No fallback registry: the old tx1138 registry host was retired (2026-06-13); empty disables the fallback path. ARCHY_REGISTRY_FALLBACK=""