From a9d7da14f5a73c38f790dfb0cdf53b3c9d3254e0 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Aug 2026 11:10:07 +0000 Subject: [PATCH] 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 --- .../control-plane/cp2a2_2b/rq-historical | 168 ++++++++++++++++-- 1 file changed, 156 insertions(+), 12 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 3811cfe..20f673f 100755 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -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/' 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."