GL-6 Gate-0 ruling (G3): v1.11.2 — anonymous artifact fetch supported

Operator ruling at the GL-6 drill's Gate 0: the Felhom artifacts are
world-readable by design; an EMPTY git.username/git.token in the customer
config now WARNS and fetches anonymously instead of dying at step 5/8
(the hub-vouched sha256 stays the integrity root; a configured credential
is still used when present; curl auth args are conditional because -u with
an empty token 401s even on public content). All 12 installer fetch
targets validated 200 anonymously before shipping. Harness: +GL6-ANON
shape case; GL4-C2 assertion updated for the v1.11.1 pinned constants
(the benign overrides-notice is not a die). Drill record carries the
deviation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-08 12:28:51 +02:00
parent bd97bc3474
commit 0640aa06ea
3 changed files with 129 additions and 12 deletions
+36 -10
View File
@@ -37,6 +37,11 @@
# --operator-pubkey-file write authz.signers into the agent config at step 6; empty keys keep a
# preserved config's signers (never clobber a manually-pinned box); verify reports armed/dormant.
#
# v1.11.2 (GL-6 Gate-0 operator ruling, G3): ANONYMOUS artifact fetch supported — empty
# git.username/git.token in the customer config now WARNS and fetches anonymously (the Felhom
# artifacts are world-readable by ruling; the hub-vouched sha256 stays the integrity root).
# A configured credential is still used when present.
#
# Grounding: documentation/audits/SPIKE-day0-firstboot-handshake-2026-06-26.md
#
# Usage:
@@ -144,7 +149,7 @@
set -euo pipefail
SCRIPT_VERSION="1.11.1" # keep in sync with the header line at the top of this file
SCRIPT_VERSION="1.11.2" # keep in sync with the header line at the top of this file
# Operator signing keys pinned at day-0 (GL-4; doc 04 §3 two-key model). EMPTY by default — the pin
# CEREMONY is an operator step: generate the real keypairs OFFLINE, then fill these four constants
@@ -349,24 +354,43 @@ resolve_artifacts() {
# Resolve the Gitea fetch credential (git username + token) from the customer's controller.yaml —
# the SAME secret config-retrieve already hands out (NO new credential). Sets GIT_USER / GIT_TOKEN.
# Parses the git: block without a YAML lib (fresh PVE has no PyYAML).
# v1.11.2 (GL-6 Gate-0 operator ruling, G3): EMPTY credentials are ALLOWED — the fetches then run
# ANONYMOUSLY (the Felhom artifacts are deliberately world-readable; integrity comes from the
# hub-vouched sha256 + visudo/sh -n validation, never from transport auth). Loud warn, never die:
# a customer config without a git token must not block day-0 while the anonymous path works.
resolve_git_creds() {
local yaml
yaml=$(curl -fsS "$HUB_URL/api/v1/config/$CUSTOMER_ID" -H "X-Retrieval-Password: $PASSPHRASE") \
|| die "controller.yaml fetch failed (for the git fetch token)"
|| die "controller.yaml fetch failed (for the git fetch credential)"
GIT_USER=$(awk '/^[^[:space:]#]/{ingit=($1=="git:")} ingit&&$1=="username:"{print $2}' <<<"$yaml" | head -1)
GIT_TOKEN=$(awk '/^[^[:space:]#]/{ingit=($1=="git:")} ingit&&$1=="token:"{print $2}' <<<"$yaml" | head -1)
# strip any surrounding quotes
GIT_USER="${GIT_USER%\"}"; GIT_USER="${GIT_USER#\"}"
GIT_TOKEN="${GIT_TOKEN%\"}"; GIT_TOKEN="${GIT_TOKEN#\"}"
[[ -n "$GIT_TOKEN" ]] || die "no git token in controller.yaml — cannot fetch artifacts from Gitea"
if [[ -z "$GIT_TOKEN" ]]; then
log_warn " no git credential in controller.yaml — fetching artifacts ANONYMOUSLY (they are world-readable; sha256 verification unchanged)"
fi
}
# Fetch a Gitea generic-package URL to a dest with the git token, then VERIFY its sha256 against the
# expected (hub-vouched) value. Aborts on any mismatch — verify-before-use. $1=url $2=dest $3=expected_sha
# _git_auth_args populates the caller's curl arg array with basic-auth ONLY when a credential is
# present — empty creds = anonymous fetch (v1.11.2). `curl -u user:` with an empty token would NOT
# fall back to anonymous (a wrong credential 401s even on world-readable content), hence the array.
_git_auth_args() {
local -n _out=$1
_out=()
if [[ -n "$GIT_TOKEN" ]]; then
_out=(-u "${GIT_USER}:${GIT_TOKEN}")
fi
}
# Fetch a Gitea generic-package URL to a dest (authed when a credential exists, else anonymous),
# then VERIFY its sha256 against the expected (hub-vouched) value. Aborts on any mismatch —
# verify-before-use; the sha is the integrity root either way. $1=url $2=dest $3=expected_sha
fetch_verify() {
local url="$1" dest="$2" want="$3"
[[ -n "$want" ]] || die "refusing to install an artifact with no expected sha256 (manifest incomplete): $url"
curl -fsS -u "${GIT_USER}:${GIT_TOKEN}" -o "$dest" "$url" || die "fetch failed: $url"
local -a _auth; _git_auth_args _auth
curl -fsS "${_auth[@]}" -o "$dest" "$url" || die "fetch failed: $url"
local got; got=$(sha256sum "$dest" | awk '{print $1}')
if [[ "$got" != "$want" ]]; then
rm -f "$dest"
@@ -375,12 +399,14 @@ fetch_verify() {
log_success " verified sha256 ${got:0:16}… matches the hub manifest"
}
# Fetch a raw config file (the canonical unit/sudoers) from the agent repo with the git token. These
# are non-executable text (not the integrity-checked binary); the sudoers is `visudo -cf`-validated
# before install, which catches corruption/tampering that would matter. $1=repo-path $2=dest
# Fetch a raw config file (the canonical unit/sudoers) from the agent repo (authed when a credential
# exists, else anonymous). These are non-executable text (not the integrity-checked binary); the
# sudoers is `visudo -cf`-validated before install, which catches corruption/tampering that would
# matter. $1=repo-path $2=dest
fetch_raw() {
local path="$1" dest="$2"
curl -fsS -u "${GIT_USER}:${GIT_TOKEN}" -o "$dest" \
local -a _auth; _git_auth_args _auth
curl -fsS "${_auth[@]}" -o "$dest" \
"$GITEA_BASE/$GITEA_OWNER/$AGENT_REPO/raw/branch/main/$path" \
|| die "raw fetch failed: $path"
[[ -s "$dest" ]] || die "raw fetch empty: $path"