From d6394ef3278d34daa056f4b58ca2dacf43fc9c46 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 31 Aug 2026 12:54:03 +0000 Subject: [PATCH] RAIN: Phase 13.5 Emergency Rollback CP Repair - target-image-based rollback pre-gates (current/recovery gold). SoT local, no push. --- .../control-plane/cp2a2_2b/rq-historical | 110 ++++++++++++++++-- 1 file changed, 103 insertions(+), 7 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 491bcd3..8a985ad 100755 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -1305,6 +1305,101 @@ verify_candidate_image() { exit 0 } +# ==== RAIN EMERGENCY ROLLBACK CP REPAIR: Target-spezifische image-only Verifikation ==== +# ROLLBACK-PRE-GATES muessen das jeweilige ROLLBACK-ZIEL-IMAGE direkt verifizieren, +# NICHT den laufenden historical-service Container (der traegt bewusst ein anderes +# Image, z.B. Remediation). Originale verify_critical_files/verify_recovery_gold_image +# vergleichen docker exec "$HISTORICAL_SERVICE" (Z.916) -> falscher FAIL bei laufendem +# Remediation-Container. Diese Helper prüfen ausschliesslich ein festes Ziel-Image. +# KEIN freies CLI-Argument, KEIN latest, KEINE auto-discovery. + +# Critical-File-SHAs eines ZIEL-IMAGES gegen Gold. NUR Image, KEIN Container-Vergleich. +# $1 = Basisverzeichnis mit app/ (kopierter Target-Tree). FAIL CLOSED. +verify_critical_files_image_only() { + local base_dir="$1" allok=1 k path is gold + for k in "${!CRITICAL_SHA[@]}"; do + path="${CRITICAL_PATH[$k]#app/}" + is="MISSING" + [ -f "$base_dir/$path" ] && is="$(sha256sum "$base_dir/$path" | awk '{print $1}')" + gold="${CRITICAL_SHA[$k]}" + if [ "$is" = "$gold" ]; then + printf ' MATCH %-22s image=%s gold=%s\n' "$k" "${is:0:12}" "${gold:0:12}" + else + printf ' FAIL %-22s image=%s gold=%s\n' "$k" "${is:0:12}" "${gold:0:12}" + allok=0 + fi + done + [ "$allok" = "1" ] || die "verify: Critical-File-SHA-Mismatch im Ziel-IMAGE (FAIL CLOSED)" +} + +# Runtime-Config eines ZIEL-IMAGES gegen erwartete Baseline. $1 = Target-ID/Tag. +# NUR Image-inspect, KEIN Container. FAIL CLOSED. ENV/secrets redigiert. +verify_image_config_target() { + local TGT="$1" + local exp_entry="[]" exp_workdir="/app" exp_user="" exp_expose="map[8000/tcp:{}]" + echo " [config] Entrypoint, Cmd, WorkingDir, User, ExposedPorts:" + local c_entry c_cmd c_wd c_user c_expose + c_entry="$(docker image inspect -f '{{json .Config.Entrypoint}}' "$TGT" 2>/dev/null)" + c_cmd="$(docker image inspect -f '{{json .Config.Cmd}}' "$TGT" 2>/dev/null)" + c_wd="$(docker image inspect -f '{{.Config.WorkingDir}}' "$TGT" 2>/dev/null)" + c_user="$(docker image inspect -f '{{.Config.User}}' "$TGT" 2>/dev/null)" + c_expose="$(docker image inspect -f '{{json .Config.ExposedPorts}}' "$TGT" 2>/dev/null)" + printf ' Entrypoint = %s (erwartet %s)\n' "${c_entry:-}" "$exp_entry" + printf ' Cmd = %s\n' "${c_cmd:-}" + printf ' WorkingDir = %s (erwartet %s)\n' "${c_wd:-}" "$exp_workdir" + printf ' User = %s (erwartet leer)\n' "${c_user:-}" + printf ' Exposed = %s (erwartet 8000)\n' "${c_expose:-}" + [ "$c_wd" = "$exp_workdir" ] || die "verify: WorkingDir unerwartet '$c_wd' (FAIL CLOSED)" + echo " [config] OK (ENV/secrets NICHT ausgegeben)" +} + +# Kompletter image-only Gold-Verify eines festen ZIEL-IMAGES. +# $1 = feste Target-Image-ID (CANDIDATE_IMAGE_ID | RECOVERY_GOLD_IMAGE_ID), intern gebunden. +# inspectable -> 63/63 Gold-Manifest -> Critical SHAs -> Config. KEIN Container-Vergleich. +verify_gold_target_image() { + local TARGET_ID="$1" + echo " [target] Target-Image inspectable:" + docker image inspect "$TARGET_ID" >/dev/null 2>&1 || die "target FAIL: Ziel-Image nicht inspectable (FAIL CLOSED)" + echo " OK ($TARGET_ID)" + echo " [target] 63/63 Gold-Manifest + Critical SHAs + Config (IMAGE-ONLY, kein Container-Vergleich):" + local tmpc tmpdir tmpf gret + tmpc="rq-goldtgt-$(date +%s)"; tmpdir="/tmp/rq-goldtgt-copy-$$" + rm -rf "$tmpdir"; mkdir -p "$tmpdir" + docker create --name "$tmpc" --entrypoint /bin/true "$TARGET_ID" >/dev/null 2>&1 || die "target: Temp-Container Fehler (FAIL CLOSED)" + docker cp "$tmpc:/app/app" "$tmpdir/appapp" >/dev/null 2>&1 || { docker rm -f "$tmpc" >/dev/null 2>&1; rm -rf "$tmpdir"; die "target: /app/app nicht lesbar (FAIL CLOSED)"; } + docker rm -f "$tmpc" >/dev/null 2>&1 || true + tmpf="$(mktemp)" + ( cd "$tmpdir/appapp" && find . -type f -name '*.py' ! -path '*/__pycache__/*' -printf '%P\n' 2>/dev/null | sort \ + | while IFS= read -r rel; do [ -f "$rel" ] && sha256sum "$rel"; done | awk '{print $1" app/"$2}' ) > "$tmpf" + classify_and_report "$tmpf" + gret="$G_RET"; rm -f "$tmpf" + if [ "$gret" != "ok" ]; then + rm -rf "$tmpdir"; die "target FAIL: Ziel-Image != Gold-Manifest (FAIL CLOSED)" + fi + echo " TARGET vs GOLD: 63 MATCH / 0 DIVERGED / 0 MISSING / 0 unerlaubte EXTRA" + echo " [target] Critical-Files (IMAGE-ONLY):" + verify_critical_files_image_only "$tmpdir/appapp" + rm -rf "$tmpdir" + verify_image_config_target "$TARGET_ID" + echo " VERDICT: TARGET GOLD OK (63/63, Critical SHAs + Config PASS, IMAGE-ONLY)" + return 0 +} + +# SEPARATER Current-Service-Safety-Check. Prueft, DASS der historical-service laeuft +# und healthy ist. Prueft NICHT gegen das Ziel-Gold-Manifest (der Sinn des Rollbacks +# ist, aus einem anderen Zustand zurueckzukehren). Intern, kein freies Argument. +current_service_safety() { + echo " [service] historical-service Running + health:" + local running rh + running="$(docker inspect -f '{{.State.Running}}' "$HISTORICAL_SERVICE" 2>/dev/null)" + [ "$running" = "true" ] || die "service FAIL: historical-service nicht Running (FAIL CLOSED)" + rh="$(docker exec "$HISTORICAL_SERVICE" python -c 'import urllib.request;print(urllib.request.urlopen("http://localhost:8000/health",timeout=5).status)' 2>/dev/null || echo N/A)" + [ "$rh" = "200" ] || die "service FAIL: health != 200 (FAIL CLOSED)" + echo " OK (running, /health=$rh)" + echo " [service] laufendes Image (known state, NICHT gegen Gold geprueft):" + docker inspect -f ' {{.Image}}' "$HISTORICAL_SERVICE" 2>/dev/null +} + # ---- recreate-candidate-safe (NUR historical-service; nur Red Queen) ---- recreate_candidate_safe() { expect_zero "$@" @@ -1343,8 +1438,10 @@ rollback_to_recovery_gold() { local rt; rt="$(docker image inspect -f '{{.Id}}' "$RECOVERY_GOLD_TAG_FINAL" 2>/dev/null || true)" [ "$rt" = "$RECOVERY_GOLD_IMAGE_ID" ] || die "pre FAIL: Recovery-Tag != Recovery-ID (FAIL CLOSED)" echo " OK" - echo " [pre] verify-recovery-gold-image PASS:" - ( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK" || die "pre FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)" + echo " [pre] Current Service Safety (running + health, NICHT Gold-geprueft):" + current_service_safety + echo " [pre] RECOVERY TARGET direkt verifizieren (IMAGE-ONLY, kein Container-Vergleich):" + verify_gold_target_image "$RECOVERY_GOLD_IMAGE_ID" echo " [pre] historical-db healthy:" ensure_db_run && echo " OK" if [ "$RUN_MODE" = "DRY_RUN" ]; then @@ -1372,11 +1469,10 @@ rollback_to_current_gold_anchor() { echo " Ziel-Service : $HISTORICAL_SERVICE (NUR dieser; KEIN db, KEINE Volumes)" echo " Image : $GOLD_ANCHOR (fixed, KEIN latest, KEIN Tag erforderlich)" echo " Preconditions:" - echo " [pre] Gold-Anker inspectable:" - docker image inspect "$GOLD_ANCHOR" >/dev/null 2>&1 || die "pre FAIL: Gold-Anker nicht inspectable (FAIL CLOSED)" - echo " OK" - echo " [pre] Gold-Anker 63/63 Gold-MATCH:" - ( verify_recovery_gold_image >/dev/null 2>&1 ) && echo " OK (Recovery-Gold 63/63; Gold-Anker separat verifiziert)" || die "pre FAIL: Recovery-Gold nicht verifiziert (FAIL CLOSED)" + echo " [pre] Current Service Safety (running + health, NICHT Gold-geprueft):" + current_service_safety + echo " [pre] PRIMARY TARGET direkt verifizieren (IMAGE-ONLY, kein Container-Vergleich):" + verify_gold_target_image "$GOLD_ANCHOR" echo " [pre] Recovery READY:" ( verify_ready recovery >/dev/null 2>&1 ) || die "pre FAIL: Recovery-READY nicht PASS (FAIL CLOSED)" echo " OK"