Phase 13.5 Recovery Rebuild Guard Source Rotation: rebuild_guards Guard A/B/C auf Gold-Source-Session (verify-recovered-gold-build-source + verify_recovery_build_input), Host-SoT-Drift non-blocking Diagnostic, kein /opt/historical-v2 als Recovery-Build-Blocker

This commit is contained in:
root 2026-08-30 11:10:07 +00:00
parent d7db920116
commit a9d7da14f5

View file

@ -208,6 +208,12 @@ RECOVERY_IMAGE_STATE="$CONTROL_PLANE_STATE_DIR/recovery-image.state"
GOLD_DOCKERIGNORE_SHA="70bac51783f5ee956796cf2216ab17a550fcb646092edad3504763dbfabb881f"
# Historische Recovery-Image-ID (VERLOREN) — NUR Evidence, KEINE zwingende Output-ID.
HISTORICAL_RECOVERY_IMAGE_ID="sha256:d2db926dc6f5f7cf2f8e85d271a9b16fcc3cfb54b6557604d3d784f7961a4bc2"
# Recovery-Source-State-Felder (zur Laufzeit via load_recovery_source_state gesetzt).
# NUR der SOURCE-State (existiert VOR Build); der IMAGE-State existiert erst NACH Build.
RECOVERY_GOLD_SOURCE_SESSION="" # GOLD_SOURCE_SESSION aus RECOVERY_SOURCE_STATE
RECOVERY_GOLD_BUILD_INPUT_SHA="" # FINAL_BUILD_INPUT_SHA aus RECOVERY_SOURCE_STATE
RECOVERY_GOLD_SOURCE_CONTAINER_ID="" # SOURCE_CONTAINER_ID aus RECOVERY_SOURCE_STATE
RECOVERY_GOLD_SOURCE_IMAGE_ID="" # SOURCE_IMAGE_ID aus RECOVERY_SOURCE_STATE
# ------------------------------------------------- PERSISTENT-HOST-SOT-PROMOTION
@ -382,6 +388,59 @@ load_recovery_state() {
RECOVERY_BUILD_DOCKERFILE="$GOLD_SOURCE_ROOT/$sess/Dockerfile"
}
# load_recovery_source_state(): laedt NUR den Recovery-SOURCE-State (existiert VOR Build).
# Setzt RECOVERY_BUILD_CONTEXT/DOCKERFILE + die Source-Identitaets-Felder.
# Verlangt NICHT den Recovery-IMAGE-State (der existiert erst NACH Build).
# FAIL CLOSED wenn SOURCE-State fehlt/ungueltig. Kein freier Input, kein statischer Fallback.
load_recovery_source_state() {
if [ ! -f "$RECOVERY_SOURCE_STATE" ]; then
die "load-recovery-source-state: Kein Recovery-Source-State ($RECOVERY_SOURCE_STATE). NUR nach realem recover (FAIL CLOSED)"
fi
local sess sha cid iid
sess="$(awk -F'=' '$1=="GOLD_SOURCE_SESSION"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
[[ "$sess" =~ ^[0-9]{8}_[0-9]{6}$ ]] || die "load-recovery-source-state: GOLD_SOURCE_SESSION ungueltig/fehlt (FAIL CLOSED)"
sha="$(awk -F'=' '$1=="FINAL_BUILD_INPUT_SHA"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
[[ "$sha" =~ ^[a-f0-9]{64}$ ]] || die "load-recovery-source-state: FINAL_BUILD_INPUT_SHA ungueltig/fehlt (FAIL CLOSED)"
cid="$(awk -F'=' '$1=="SOURCE_CONTAINER_ID"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
[[ "$cid" =~ ^[a-f0-9]{64}$ ]] || die "load-recovery-source-state: SOURCE_CONTAINER_ID ungueltig/fehlt (FAIL CLOSED)"
iid="$(awk -F'=' '$1=="SOURCE_IMAGE_ID"{print $2}' "$RECOVERY_SOURCE_STATE" 2>/dev/null)"
[[ "$iid" =~ ^sha256:[a-f0-9]{64}$ ]] || die "load-recovery-source-state: SOURCE_IMAGE_ID ungueltig/fehlt (FAIL CLOSED)"
RECOVERY_GOLD_SOURCE_SESSION="$sess"
RECOVERY_GOLD_BUILD_INPUT_SHA="$sha"
RECOVERY_GOLD_SOURCE_CONTAINER_ID="$cid"
RECOVERY_GOLD_SOURCE_IMAGE_ID="$iid"
RECOVERY_BUILD_CONTEXT="$GOLD_SOURCE_ROOT/$sess"
RECOVERY_BUILD_DOCKERFILE="$GOLD_SOURCE_ROOT/$sess/Dockerfile"
}
# write_recovery_image_state(): atomarer Write des Recovery-IMAGE-State NACH erfolgreichem Build.
# $1 = FULL_IMAGE_ID (sha256:...). Verlangt: State ABSENT (kein Overwrite, FAIL CLOSED).
# Schreibt via Temp-Datei im selben Filesystem + fsync + atomares mv. Kein partial state sichtbar.
write_recovery_image_state() {
local fullid="$1" tmp
[[ "$fullid" =~ ^sha256:[a-f0-9]{64}$ ]] || die "write-recovery-image-state: FULL_IMAGE_ID ungueltig (FAIL CLOSED)"
mkdir -p "$CONTROL_PLANE_STATE_DIR" || die "write-recovery-image-state: State-Dir nicht anlegbar (FAIL CLOSED)"
if [ -e "$RECOVERY_IMAGE_STATE" ]; then
die "write-recovery-image-state: Recovery-Image-State existiert bereits $RECOVERY_IMAGE_STATE (KEIN Overwrite, FAIL CLOSED)"
fi
tmp="$CONTROL_PLANE_STATE_DIR/.recovery-image.state.tmp.$$"
{
echo "NEW_RECOVERY_FULL_IMAGE_ID=$fullid"
echo "RECOVERY_TAG=$RECOVERY_REBUILD_TAG"
echo "BUILD_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "BUILD_INPUT_SHA=$RECOVERY_GOLD_BUILD_INPUT_SHA"
echo "SOURCE_SESSION=$RECOVERY_GOLD_SOURCE_SESSION"
echo "SOURCE_CONTAINER_ID=$RECOVERY_GOLD_SOURCE_CONTAINER_ID"
echo "SOURCE_IMAGE_ID=$RECOVERY_GOLD_SOURCE_IMAGE_ID"
echo "WRAPPER_SHA=$(sha256sum /usr/local/bin/rq-historical | awk '{print $1}')"
} > "$tmp" || { rm -f "$tmp"; die "write-recovery-image-state: Temp-Write fehlgeschlagen (FAIL CLOSED)"; }
chown root:root "$tmp" 2>/dev/null || true
chmod 600 "$tmp" || { rm -f "$tmp"; die "write-recovery-image-state: chmod fehlgeschlagen (FAIL CLOSED)"; }
sync -f "$tmp" 2>/dev/null || true
mv -f "$tmp" "$RECOVERY_IMAGE_STATE" || { rm -f "$tmp"; die "write-recovery-image-state: atomares mv fehlgeschlagen (FAIL CLOSED)"; }
sync -f "$RECOVERY_IMAGE_STATE" 2>/dev/null || true
}
# Manifest einer Quelle (Gold-Container / neuer Temp-Container) erzeugen:
# gibt zeilenweise 'sha app/<rel>' auf stdout; Quelle ist IMMER laufender Container
gen_manifest() { # $1 = docker-Container-Name
@ -605,14 +664,76 @@ verify_host_gold_context() {
[ $d -eq 0 ] && [ $m -eq 0 ] && [ $x -eq 0 ]
}
# verify_recovery_build_input(): beweist den tatsaechlichen finalen Build Input der
# Gold-Source-Session (NICHT Host-SoT). Prueft exakt:
# - Build-Support SHAs (Dockerfile, requirements.txt, .dockerignore) gegen GOLD_BUILD_SUPPORT
# - .dockerignore exakt (GOLD_DOCKERIGNORE_SHA)
# - Dockerfile exakt erwartet
# - keine forbidden extras in der Session
# - FINAL_BUILD_INPUT_SHA exakt (aus Control-Plane-SOURCE-State)
# FAIL CLOSED bei jeder Abweichung. Kein freier Input, kein /opt/historical-v2.
verify_recovery_build_input() {
local ctx="$RECOVERY_BUILD_CONTEXT"
[ -d "$ctx" ] || die "verify-recovery-build-input: Build-Context fehlt $ctx (FAIL CLOSED)"
# Build-Support SHAs exakt (gegen die Session-Dateien, nicht Host)
local key spec src exp f s
for key in "${!GOLD_BUILD_SUPPORT[@]}"; do
spec="${GOLD_BUILD_SUPPORT[$key]}"; src="${spec%%|*}"; exp="${spec##*|}"
f="$ctx/$(basename "$src")"
[ -f "$f" ] || die "verify-recovery-build-input: fehlt $f (FAIL CLOSED)"
s="$(sha256sum "$f" | awk '{print $1}')"
[ "$s" = "$exp" ] || die "verify-recovery-build-input: Build-Support SHA mismatch $key (FAIL CLOSED)"
done
# .dockerignore exakt
local dis
dis="$(sha256sum "$ctx/.dockerignore" 2>/dev/null | awk '{print $1}')"
[ "$dis" = "$GOLD_DOCKERIGNORE_SHA" ] || die "verify-recovery-build-input: .dockerignore SHA mismatch (FAIL CLOSED)"
# Dockerfile exakt erwartet
dockerfile_expected || die "verify-recovery-build-input: Dockerfile nicht exakt erwartet (FAIL CLOSED)"
# keine forbidden extras in der Session
local rel
while IFS= read -r rel; do
[ -n "$rel" ] || continue
if [ -n "${GOLD_FORBIDDEN_EXTRA[$rel]:-}" ]; then
die "verify-recovery-build-input: forbidden extra $rel (FAIL CLOSED)"
fi
done < <( cd "$ctx" && find . -type f -printf '%P\n' 2>/dev/null | sort )
# FINAL_BUILD_INPUT_SHA exakt (aus SOURCE-State)
local got
got="$( ( cd "$ctx" && find . -type f ! -name 'session.meta' ! -name 'application.manifest' ! -name 'build-support.manifest' ! -name 'final-build-input.manifest' -printf '%P\n' 2>/dev/null | sort | while IFS= read -r f; do [ -f "$f" ] && sha256sum "$f"; done ) | sha256sum | awk '{print $1}' )"
[ "$got" = "$RECOVERY_GOLD_BUILD_INPUT_SHA" ] || die "verify-recovery-build-input: FINAL_BUILD_INPUT_SHA mismatch (Source veraendert) (FAIL CLOSED)"
return 0
}
rebuild_guards() {
echo " [guard A] verify-sot-recovery 13/13:"
verify_recovery_ok >/dev/null && echo " OK (13/13 MATCH)"
echo " [guard B] Host-Tree vs Gold-Manifest (63 MATCH / 0 DIVERGED / 0 MISSING; nur erlaubte Legacy-EXTRA):"
verify_host_gold_context || die "Guard B FAIL: Host-Gold-Kontext nicht reproduzierbar (FAIL CLOSED)"
echo " [guard A] verify-recovered-gold-build-source (Build-Source 63/63):"
verify_recovered_gold_build_source >/dev/null || die "Guard A FAIL: Recovery-Build-Source nicht verifiziert (FAIL CLOSED)"
echo " OK (63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA)"
echo " [guard B] Recovery-Build-Input (Build-Support SHAs + .dockerignore + Dockerfile + forbidden extras + FINAL_BUILD_INPUT_SHA):"
verify_recovery_build_input || die "Guard B FAIL: Recovery-Build-Input nicht reproduzierbar (FAIL CLOSED)"
echo " Guard B OK"
echo " [guard C] Build-Pollution ausgeschlossen (.dockerignore exakt):"
exclusion_valid && echo " OK (nur app/app/ + app/eligibility.py)"
echo " [guard C] Host-SoT-Drift (NON-BLOCKING Diagnostic):"
local hm hmf d=0 m=0 x=0 mat=0 l=0 goldrel goldsha nrel nsha
hmf="$(mktemp)"; gen_host_manifest > "$hmf"
while read -r goldsha goldrel; do
goldrel="${goldrel#./}"; [ -n "$goldrel" ] || continue
l=$((l+1))
nsha="$(awk -v r="$goldrel" '$2==r{print $1}' "$hmf")"
if [ -z "$nsha" ]; then m=$((m+1));
elif [ "$nsha" = "$goldsha" ]; then mat=$((mat+1));
else d=$((d+1)); fi
done < "$GOLD_MANIFEST"
while read -r nsha nrel; do
nrel="${nrel#./}"
found="$(awk -v r="$nrel" '$2==r{print $1}' "$GOLD_MANIFEST")"
[ -z "$found" ] && x=$((x+1))
done < "$hmf"
rm -f "$hmf"
if [ $d -eq 0 ] && [ $m -eq 0 ] && [ $x -eq 0 ]; then
echo " HOST_SOT_DRIFT=NONE (MATCH=$mat)"
else
echo " HOST_SOT_DRIFT=DETECTED (MATCH=$mat DIVERGED=$d MISSING=$m EXTRA=$x) — NON-BLOCKING, Host-SoT wird nach RECOVERY READY repariert"
fi
echo " [guard D] Dockerfile exakt erwartet:"; dockerfile_expected && echo " OK"
echo " [guard E] Gold-Container Running + /health + /health/ready:"
ensure_gold_run
@ -705,16 +826,19 @@ verify_image_config() {
echo " [config] OK (ENV/secrets NICHT ausgegeben; nur redigierte Struktur)"
}
# ---- build-recovery-gold (NUR Build aus Host-SoT; DRY_RUN = kein Build) ----
# ---- build-recovery-gold (NUR Build aus verifizierter Gold Source; DRY_RUN = kein Build) ----
build_recovery_gold() {
expect_zero "$@"
echo "=== BUILD-RECOVERY-GOLD ($RUN_MODE) ==="
# Build Context Rotation (Punkt 11): ausschliesslich aus kontrolliertem State.
# Kein statisches /opt/historical-v2. Keine User-Argumente.
load_recovery_state
echo " Build Context : $RECOVERY_BUILD_CONTEXT (aus Control-Plane-State)"
echo " Dockerfile : $RECOVERY_BUILD_DOCKERFILE (aus Control-Plane-State)"
# Build Context Rotation (Punkt 11): ausschliesslich aus kontrolliertem SOURCE-State.
# Kein statisches /opt/historical-v2. Keine User-Argumente. Der IMAGE-State existiert
# erst NACH dem Build und ist hier KEIN Input (zirkuläres Gate behoben).
load_recovery_source_state
echo " Build Context : $RECOVERY_BUILD_CONTEXT (aus Control-Plane-SOURCE-State)"
echo " Dockerfile : $RECOVERY_BUILD_DOCKERFILE (aus Control-Plane-SOURCE-State)"
echo " Recovery-Tag : $RECOVERY_REBUILD_TAG (fix, direkt getaggt, KEIN latest)"
echo " Source-Session: $RECOVERY_GOLD_SOURCE_SESSION"
echo " Build-Input : $RECOVERY_GOLD_BUILD_INPUT_SHA"
echo " Guards:"
# Build Preflight (Punkt 12): verify-recovered-gold-build-source PASS zwingend.
echo " [preflight] verify-recovered-gold-build-source:"
@ -724,15 +848,35 @@ build_recovery_gold() {
# Build-Context-Manifest (Punkt 5) — read-only RECOVERY_GOLD_BUILD_INPUT
echo " [build-input] RECOVERY_GOLD_BUILD_INPUT (Build-Context-Manifest):"
build_context_manifest | while IFS= read -r line; do echo " $line"; done
# Punkt 7: FINAL BUILD INPUT TOCTOU — unmittelbar vor docker build neu berechnen/verifizieren.
echo " [build-input-sha] FINAL_BUILD_INPUT_SHA (TOCTOU, unmittelbar vor Build):"
local got_sha
got_sha="$( ( cd "$RECOVERY_BUILD_CONTEXT" && find . -type f ! -name 'session.meta' ! -name 'application.manifest' ! -name 'build-support.manifest' ! -name 'final-build-input.manifest' -printf '%P\n' 2>/dev/null | sort | while IFS= read -r f; do [ -f "$f" ] && sha256sum "$f"; done ) | sha256sum | awk '{print $1}' )"
[ "$got_sha" = "$RECOVERY_GOLD_BUILD_INPUT_SHA" ] || die "build-recovery-gold: FINAL_BUILD_INPUT_SHA mismatch (Source veraendert) (FAIL CLOSED)"
echo " == $got_sha (OK)"
if [ "$RUN_MODE" = "DRY_RUN" ]; then
echo " [post-build] wuerde Image-ID ueber festen Tag ermitteln: docker image inspect $RECOVERY_REBUILD_TAG --format '{{.Id}}'"
echo " [post-build] wuerde atomar recovery-image.state erzeugen: $RECOVERY_IMAGE_STATE"
echo "DRY_RUN_PASS: build-recovery-gold wuerde docker build -f $RECOVERY_BUILD_DOCKERFILE -t $RECOVERY_REBUILD_TAG $RECOVERY_BUILD_CONTEXT (keine Aktion)."
exit 0
fi
mode_or_die
echo " [build] docker build -f $RECOVERY_BUILD_DOCKERFILE -t $RECOVERY_REBUILD_TAG $RECOVERY_BUILD_CONTEXT"
docker build -f "$RECOVERY_BUILD_DOCKERFILE" -t "$RECOVERY_REBUILD_TAG" "$RECOVERY_BUILD_CONTEXT" || die "build-recovery-gold fehlgeschlagen (FAIL CLOSED)"
# Punkt 8: Build-Output-Identity — Image-ID NICHT aus Textausgabe erraten, sondern
# kontrolliert ueber den fest verdrahteten Recovery-Tag ermitteln. Keine freie Tag-Eingabe.
local newid
newid="$(docker image inspect -f '{{.Id}}' "$RECOVERY_REBUILD_TAG" 2>/dev/null)"
# Punkt 9: Image-ID-Validierung — vorhanden, sha256:, vollstaendig, inspectable, exakt zum Tag.
[[ "$newid" =~ ^sha256:[a-f0-9]{64}$ ]] || die "build-recovery-gold: Image-ID ungueltig/fehlt (FAIL CLOSED)"
docker image inspect "sha256:${newid#sha256:}" >/dev/null 2>&1 || die "build-recovery-gold: Image nicht per ID inspectable (FAIL CLOSED)"
local tagid
tagid="$(docker image inspect -f '{{.Id}}' "$RECOVERY_REBUILD_TAG" 2>/dev/null)"
[ "$tagid" = "$newid" ] || die "build-recovery-gold: Tag->ID nicht eindeutig ($tagid != $newid) (FAIL CLOSED)"
echo " [post-build] Image-ID (aus festem Tag): $newid"
# Punkt 11: State-Write NUR NACH erfolgreichem Build + Tag->ID PASS. Atomar.
write_recovery_image_state "$newid"
echo " [post-build] recovery-image.state atomar geschrieben: $RECOVERY_IMAGE_STATE"
echo "RECOVERY_GOLD_IMAGE_ID=$newid"
echo "KLASSIFIZIERUNG: REBUILT RECOVERY GOLD IMAGE (NICHT das Original-Gold-Image; Original f1a6f33e... = UNAVAILABLE)"
echo "BUILD-RECOVERY-GOLD OK. KEIN Recreate, KEIN Runner. latest unangetastet."