From 65c1d57fa7b40b1befc2a7a80392b85d6f14cb83 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Aug 2026 06:05:56 +0000 Subject: [PATCH] Phase 13.5: Image Retention & Archive Control Plane - feste Subcommands archive/pin/verify-archive/verify-pin/verify-ready/dry-run restore (FAIL CLOSED, kein Overwrite, kein Coolify-Cleanup-Disable, kein coolify.managed-Spoof); Restore nur dry-run (real blockiert) --- .../control-plane/cp2a2_2b/rq-historical | 436 +++++++++++++++++- 1 file changed, 432 insertions(+), 4 deletions(-) diff --git a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical index 5b7389b..8b636c5 100644 --- a/red-queen-architecture/control-plane/cp2a2_2b/rq-historical +++ b/red-queen-architecture/control-plane/cp2a2_2b/rq-historical @@ -187,6 +187,32 @@ RUNNER_REMEDIATION_SHA="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524 # Candidate/Recovery duerfen code-identisch sein, aber verschiedene Image-IDs haben (KEIN Fehler). REM_NOOP_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7" +# ------------------------------------------------- IMAGE-RETENTION / ARCHIVE CONTROL PLANE +# Phase 13.5 · Schutzschicht gegen bestaetigten Coolify DockerCleanupJob +# (taegl. 00:00 UTC, force_docker_cleanup=true, rmi auf getaggte Images ohne +# coolify.managed-Label). ZWEI unabhaengige Ebenen je Critical Image: +# SCHICHT A = Stopped Pin Container (Image-Referenz via docker create, nie gestartet) +# SCHICHT B = Persistentes, sha256-verifiziertes docker save-Archiv +# Kein Coolify-Cleanup-Disable. Kein coolify.managed-Spoof. Kein Overwrite. +# Fester Archive-Root (Punkt 3) — root:root, 700, KEINE freien Pfade. +ARCHIVE_ROOT="/opt/rq-historical-image-archives" +# Pin-Container-Namen (Punkt 9) — fest, hardcoded, KEIN freier Name. +PIN_RECOVERY_NAME="rq-historical-pin-recovery-gold" +PIN_REMEDIATION_NAME="rq-historical-pin-phase13-5-runner-remediation" +# Unterordner je Rolle (Punkt 3) +declare -A ARCHIVE_SUB=( [recovery]="recovery" [remediation]="candidates" ) +# Archiv-Session-IDs: kurze Image-ID (), die den immutable +# Session-Ordner benennt. Recovery = bekannte Original-ID; Remediation = +# FULL_IMAGE_ID aus Build-Session (41e357a1..., aktuell im Store fehlend). +RECOVERY_ARCHIVE_ID="${RECOVERY_GOLD_IMAGE_ID#sha256:}" +RECOVERY_ARCHIVE_ID="${RECOVERY_ARCHIVE_ID:0:12}" +# Remediation-Archiv-Session-ID: kurze ID der Build-Session-Remendation (41e357a1...). +REMEDIATION_ARCHIVE_ID="41e357a12b90" +# Remediation-Archiv-Referenz (aus Build-Session falls vorhanden, sonst leer): +# wird zur Laufzeit per remediation_full_image_id() aufgeloest. +# Pin-Referenz-Ziel (Schicht A): exakt diese Image-ID muss der Pin-Container referenzieren. +PIN_REMEDIATION_TARGET_ID="" # zur Laufzeit gesetzt + # RUN_MODE (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING RUN_MODE=MUTATING @@ -1406,7 +1432,392 @@ validate_stage() { # ---- redact: maskiert Passwort-/Secret-Werte (fuer build-def Anzeige) ---- redact_line() { # Zeilen mit "PASSWORD=" / "SECRET=" / "TOKEN=" : Wert maskieren - echo "$1" | sed -E 's/(PASSWORD|PASSWORDS|SECRET|SECRETS|TOKEN|TOKENS|API_KEY|CLIENT_SECRET|PRIVATE_KEY)(["'"'"']?[[:space:]]*=[[:space:]]*["'"'"']?)[^[:space:]"'"'"']+/\1\2***REDACTED***/Ig' + echo "$1" | sed -E 's/(PASSWORD|PASSWORDS|SECRET|SECRETS|TOKEN|TOKENS|API_KEY|CLIENT_SECRET|PRIVATE_KEY)(["'"'"']?[[:space:]]*=[[:space:]]*["'"'"']?)[^[:space:]"'"'"']+/\\1\\2***REDACTED***/Ig' +} + +# ============================================================================= +# IMAGE-RETENTION / ARCHIVE CONTROL PLANE (Phase 13.5 Schutzschicht) +# Zwei unabhängige Ebenen je Critical Image: +# A) Stopped Pin Container (docker create, nie gestartet) -> Image-Referenz +# B) sha256-verifiziertes docker save-Archiv -> externes Archiv +# READY-Gate: IMAGE VERIFY + PIN VERIFY + ARCHIVE VERIFY alle PASS. +# KEIN Overwrite. KEIN Coolify-Cleanup-Disable/Managed-Spoof. Kein echter +# Rebuild/Restore/Recreate. Kein freies Input (feste Rollen: recovery / remediation). +# ============================================================================= +role_ok() { # $1 = role -> 0 wenn gueltig + case "$1" in recovery|remediation) return 0 ;; *) return 1 ;; esac +} +# Feste Rollen->Image/Identity-Bindung (Punkt 2/4/8/9/10) — hardcoded. +role_image_id() { # $1 = role -> echo erwartete FULL_IMAGE_ID + case "$1" in + recovery) echo "$RECOVERY_GOLD_IMAGE_ID" ;; # sha256:d2db926d... + remediation) echo "$REMEDIATION_TAG" ;; # Tag -> aufgeloest via inspect (Image fehlt aktuell) + esac +} +role_tag() { + case "$1" in + recovery) echo "$RECOVERY_GOLD_TAG_FINAL" ;; + remediation) echo "$REMEDIATION_TAG" ;; + esac +} +role_pin() { + case "$1" in + recovery) echo "$PIN_RECOVERY_NAME" ;; + remediation) echo "$PIN_REMEDIATION_NAME" ;; + esac +} +role_arch_sub() { echo "${ARCHIVE_SUB[$1]}" ; } +role_arch_dir() { echo "$ARCHIVE_ROOT/$(role_arch_sub "$1")" ; } + +# --- Schicht A: Stopped Pin Container --- +# Container-Referenz schuetzt Image-Objekt vor Coolify rmi (empirisch belegt: +# No-op ueberlebte, weil Container 2fe3d30c... es referenzierte). +pin_container_identity_ok() { # $1 = role, $2 = actual container id -> 0 wenn ok + local role="$1" actual="$2" expected + case "$role" in + recovery) expected="$RECOVERY_GOLD_IMAGE_ID" ;; + remediation) expected="$PIN_REMEDIATION_TARGET_ID" ;; + esac + [ "$actual" = "$expected" ] +} + +# --- Schicht B: Archive Verify --- +verify_archive() { # $1 = role (read-only) + local role="$1" adir sid + adir="$(role_arch_dir "$role")" + case "$role" in + recovery) sid="$RECOVERY_ARCHIVE_ID" ;; + remediation) sid="$REMEDIATION_ARCHIVE_ID" ;; + esac + # Archive existiert + [ -d "$adir" ] || die "verify-archive($role): kein Archiv-Root (FAIL CLOSED)" + local base="$adir/$sid" + [ -d "$base" ] || die "verify-archive($role): kein Session-Ordner $base (FAIL CLOSED)" + # Pflichtdateien + local f img meta manifest runtime + img="$base/image.tar"; meta="$base/session.meta"; manifest="$base/app.manifest"; runtime="$base/runtime-config.txt" + [ -f "$img" ] && [ ! -L "$img" ] || die "verify-archive($role): image.tar fehlt/kein File (FAIL CLOSED)" + [ -s "$img" ] || die "verify-archive($role): image.tar leer (FAIL CLOSED)" + for f in "$meta" "$manifest" "$runtime"; do + [ -f "$f" ] && [ ! -L "$f" ] || die "verify-archive($role): fehlt $(basename "$f") (FAIL CLOSED)" + done + [ "$(stat -c '%U' "$img")" = "root" ] || die "verify-archive($role): image.tar Owner != root (FAIL CLOSED)" + case "$(stat -c '%a' "$img")" in 600|640|700|400) ;; *) die "verify-archive($role): image.tar Mode unsicher ($(stat -c '%a' "$img")) (FAIL CLOSED)" ;; esac + # Sha256 stimmt (Punkt 7) + local want got + want="$(awk -F'=' '$1=="ARCHIVE_SHA256"{print $2}' "$meta" 2>/dev/null)" + [ -n "$want" ] && [[ "$want" =~ ^[a-f0-9]{64}$ ]] || die "verify-archive($role): ARCHIVE_SHA256 fehlt/ungueltig in session.meta (FAIL CLOSED)" + got="$(sha256sum "$img" | awk '{print $1}')" + [ "$got" = "$want" ] || die "verify-archive($role): image.tar SHA mismatch (FAIL CLOSED)" + # session.meta Vollstaendigkeit (Punkt 5) + local key + for key in ROLE TAG FULL_IMAGE_ID ARCHIVE_SHA256 ARCHIVE_SIZE CREATED_AT HOST_SOT_RUNNER_SHA BUILD_INPUT_SHA WRAPPER_SHA APP_MANIFEST_SHA RUNTIME_CONFIG_SHA; do + grep -q "^$key=.*" "$meta" || die "verify-archive($role): session.meta fehlt $key (FAIL CLOSED)" + done + local mrole + mrole="$(awk -F'=' '$1=="ROLE"{print $2}' "$meta" 2>/dev/null)" + [ "$mrole" = "$role" ] || die "verify-archive($role): ROLE in session.meta = '$mrole' (FAIL CLOSED)" + echo "VERIFY-ARCHIVE($role): PASS ($base)" +} + +# --- Ready-Gate (Punkt 14) --- +verify_ready() { # $1 = role + local role="$1" label + case "$role" in + recovery) label="RECOVERY READY" ;; + remediation) label="CANDIDATE READY" ;; + esac + echo "=== $label (READY-Gate: IMAGE VERIFY + PIN VERIFY + ARCHIVE VERIFY) ===" + # IMAGE VERIFY (zwingend inspectable + korrekt) + echo " [IMAGE] $(role_tag "$role"):" + verify_image_present "$role" || die "READY-Gate FAIL: Image nicht inspectable (FAIL CLOSED)" + echo " OK" + echo " [PIN] $(role_pin "$role"):" + verify_pin "$role" || die "READY-Gate FAIL: Pin nicht PASS (FAIL CLOSED)" + echo " OK" + echo " [ARCHIVE] $(role_arch_dir "$role"):" + verify_archive "$role" >/dev/null || die "READY-Gate FAIL: Archive nicht PASS (FAIL CLOSED)" + echo " OK" + echo "$label: PASS" +} + +# --- Image-Present-Verify (Schicht-Pruefung: Image inspectable + Identity) --- +verify_image_present() { # $1 = role (read-only) -> 0 wenn Image vorhanden+bindung ok + local role="$1" id tag actualid + tag="$(role_tag "$role")" + case "$role" in + recovery) + id="$RECOVERY_GOLD_IMAGE_ID" + # Tag muss exakt auf die immutable Recovery-ID zeigen (kein Overwrite-Erwartung) + actualid="$(docker image inspect -f '{{.Id}}' "$tag" 2>/dev/null || true)" + [ "$actualid" = "$id" ] || return 1 + ;; + remediation) + # Candidate: FULL_IMAGE_ID aus Build-Session; Tag -> ID Bindung + if ! remediation_session; then die "verify-image(remediation): keine gueltige Build-Session (FAIL CLOSED)"; fi + id="$(remediation_full_image_id)" + actualid="$(docker image inspect -f '{{.Id}}' "$id" 2>/dev/null || true)" + [ -n "$actualid" ] || return 1 + ;; + esac + return 0 +} + +# --- Schicht A: Pin-Verify (Punkt 12) --- +verify_pin() { # $1 = role (read-only) -> 0 wenn Pin PASS + local role="$1" name imgid nw mounts ports rp priv capadd expected state + name="$(role_pin "$role")" + docker container inspect "$name" >/dev/null 2>&1 || { echo " PIN($role): Container $name fehlt"; return 1; } + expected="$(role_pin_target_id "$role")" + imgid="$(docker container inspect -f '{{.Image}}' "$name" 2>/dev/null)" + # .Image ist die Image-ID des referenzierten Images (exakt FULL_IMAGE_ID) + [ -n "$expected" ] || die "verify-pin($role): Ziel-Image-ID nicht bekannt (FAIL CLOSED)" + # Image-ID kann lang (sha256:...) sein — Bindung: .Image == erwartete FULL_IMAGE_ID + [ "$imgid" = "$expected" ] || { echo " PIN($role): Image-ID $imgid != erwartet $expected"; return 1; } + state="$(docker container inspect -f '{{.State.Status}}' "$name" 2>/dev/null)" + case "$state" in created|exited|stopped) ;; *) echo " PIN($role): State=$state (nicht created/stopped — VERBOTEN running)"; return 1 ;; esac + nw="$(docker container inspect -f '{{.HostConfig.NetworkMode}}' "$name" 2>/dev/null)" + [ "$nw" = "none" ] || { echo " PIN($role): NetworkMode=$nw != none"; return 1; } + mounts="$(docker container inspect -f '{{json .Mounts}}' "$name" 2>/dev/null)" + [ "$mounts" = "[]" ] || { echo " PIN($role): Mounts != []"; return 1; } + ports="$(docker container inspect -f '{{json .HostConfig.PortBindings}}' "$name" 2>/dev/null)" + [ "$ports" = "{}" ] || { echo " PIN($role): PortBindings != {}"; return 1; } + rp="$(docker container inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$name" 2>/dev/null)" + [ "$rp" = "no" ] || { echo " PIN($role): RestartPolicy=$rp != no"; return 1; } + priv="$(docker container inspect -f '{{.HostConfig.Privileged}}' "$name" 2>/dev/null)" + [ "$priv" = "false" ] || { echo " PIN($role): Privileged=$priv"; return 1; } + capadd="$(docker container inspect -f '{{json .HostConfig.CapAdd}}' "$name" 2>/dev/null)" + [ "$capadd" = "null" ] || [ "$capadd" = "[]" ] || { echo " PIN($role): CapAdd=$capadd"; return 1; } + return 0 +} + +# --- Ziel-Image-ID fuer Pin (Schicht A bindet exakt FULL_IMAGE_ID) --- +role_pin_target_id() { # $1 = role + case "$1" in + recovery) echo "$RECOVERY_GOLD_IMAGE_ID" ;; + remediation) + if remediation_session; then echo "$(remediation_full_image_id)"; else echo ""; fi + ;; + esac +} + +# --- Archive-Metadaten-Generator (Punkt 5; kein Secret) --- +gen_archive_meta() { # $1 = role, $2 = session dir, $3 = image id, $4 = archive size, $5 = archive sha + local role="$1" dir="$2" image_id="$3" asize="$4" asha="$5" now + now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + printf 'ROLE=%s\nTAG=%s\nFULL_IMAGE_ID=%s\nARCHIVE_SHA256=%s\nARCHIVE_SIZE=%s\nCREATED_AT=%s\nHOST_SOT_RUNNER_SHA=%s\nBUILD_INPUT_SHA=%s\nWRAPPER_SHA=%s\nAPP_MANIFEST_SHA=%s\nRUNTIME_CONFIG_SHA=%s\n' \ + "$role" "$(role_tag "$role")" "$image_id" "$asha" "$asize" "$now" \ + "$HOST_SOT_RUNNER_SHA_CUR" "$BUILD_INPUT_SHA_CUR" "$WRAPPER_SHA_CUR" "$APP_MANIFEST_SHA_CUR" "$RUNTIME_CONFIG_SHA_CUR" \ + > "$dir/session.meta" + chown root:root "$dir/session.meta"; chmod 600 "$dir/session.meta" +} + +# --- runtime-config Evidence (Punkt 4: runtime-config.txt) — keine Secrets --- +write_runtime_config() { # $1 = role, $2 = session dir + local role="$1" dir="$2" tag="$(role_tag "$role")" id + id="$(docker image inspect -f '{{.Id}}' "$tag" 2>/dev/null)" + { + echo "RUNTIME CONFIG EVIDENCE (Phase 13.5 Archive) — keine Secrets" + echo "ROLE=$role" + echo "FULL_IMAGE_ID=$id" + echo "Entrypoint=$(docker image inspect -f '{{json .Config.Entrypoint}}' "$tag" 2>/dev/null)" + echo "Cmd=$(docker image inspect -f '{{json .Config.Cmd}}' "$tag" 2>/dev/null)" + echo "WorkingDir=$(docker image inspect -f '{{.Config.WorkingDir}}' "$tag" 2>/dev/null)" + echo "User=$(docker image inspect -f '{{.Config.User}}' "$tag" 2>/dev/null)" + echo "ExposedPorts=$(docker image inspect -f '{{json .Config.ExposedPorts}}' "$tag" 2>/dev/null)" + echo "ENV_KEYS=$(docker image inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$tag" 2>/dev/null | cut -d= -f1 | tr '\n' ',' | sed 's/,$//')" + } > "$dir/runtime-config.txt" + chown root:root "$dir/runtime-config.txt"; chmod 600 "$dir/runtime-config.txt" +} + +# --- app.manifest Evidence (Punkt 4) — Sha256 aller *.py im Image-Layer --- +write_app_manifest() { # $1 = role, $2 = session dir, $3 = image id/tag + local role="$1" dir="$2" ref="$3" tmpc tmpd + tmpc="rq-arch-manifest-$$" + tmpd="/tmp/rq-arch-manifest-$$" + rm -rf "$tmpd"; mkdir -p "$tmpd" + docker create --name "$tmpc" --entrypoint /bin/true "$ref" >/dev/null 2>&1 \ + || { rm -rf "$tmpd"; die "archive($role): Temp-Container Fehler (FAIL CLOSED)"; } + if ! docker cp "$tmpc:/app/app" "$tmpd/appapp" >/dev/null 2>&1; then + docker rm -f "$tmpc" >/dev/null 2>&1 || true; rm -rf "$tmpd" + die "archive($role): /app/app nicht lesbar (FAIL CLOSED)" + fi + docker rm -f "$tmpc" >/dev/null 2>&1 || true + ( cd "$tmpd/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}' ) > "$dir/app.manifest" + rm -rf "$tmpd" + chown root:root "$dir/app.manifest"; chmod 600 "$dir/app.manifest" +} + +# ============================================================================= +# COMMAND-FUNKTIONEN (Phase 13.5 Archive/Pin) — fail-closed, kein Overwrite, +# kein echter Rebuild/Restore/Recreate in dieser Mission. +# ============================================================================= + +# archive-recovery-image / archive-remediation-image +archive_image() { # $1 = role + local role="$1" + expect_zero "${@:2}" + echo "=== ARCHIVE-$role ($RUN_MODE) ===" + local tag id subdir sdir + tag="$(role_tag "$role")" + case "$role" in + recovery) id="$RECOVERY_GOLD_IMAGE_ID" ;; + remediation) + remediation_session || die "archive(remediation): keine gueltige Build-Session (FAIL CLOSED)" + id="$(remediation_full_image_id)" + PIN_REMEDIATION_TARGET_ID="$id" + ;; + esac + echo " FULL_IMAGE_ID=$id" + echo " Tag=$tag" + # Punkt 6: Image-inspect + Tag->ID Gate VOR save + echo " [inspect] Tag -> ID Gate:" + local tagid + tagid="$(docker image inspect -f '{{.Id}}' "$tag" 2>/dev/null || true)" + if [ -z "$tagid" ]; then + die "archive($role): Source Image nicht inspectable (SOURCE IMAGE NOT INSPECTABLE — FAIL CLOSED)" + fi + [ "$tagid" = "$id" ] || die "archive($role): Tag->ID Gate FAIL ($tagid != $id) (FAIL CLOSED)" + echo " OK ($tagid)" + # Punkt 7: Immutability — Archiv-Session darf NICHT existieren + local sdir + sdir="$(role_arch_dir "$role")" # = $ARCHIVE_ROOT/ + local sid + case "$role" in + recovery) sid="$RECOVERY_ARCHIVE_ID" ;; + remediation) sid="${id#sha256:}"; sid="${sid:0:12}" ;; + esac + local base="$sdir/$sid" + # Fail-closed wenn bereits existiert (kein Overwrite) + if [ -e "$base" ]; then die "archive($role): Session-Ordner existiert bereits $base (KEIN Overwrite, FAIL CLOSED)"; fi + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo "DRY_RUN_PASS: archive($role) wuerde anlegen: $base/ (image.tar + sha256 + session.meta + app.manifest + runtime-config.txt). Keine Aktion." + exit 0 + fi + mode_or_die + # MUTATION: Archive anlegen + docker save + mkdir -p "$base" || die "archive($role): Session-Ordner nicht anlegbar (FAIL CLOSED)" + # Session.meta ZUSAMMENBAUEN VOR save (fuer Size/SHA brauchen aber erst nach save) + # Reihenfolge: save -> size/sha -> meta -> manifest -> runtime + echo " [save] docker image save $id > image.tar" + if ! docker image save "$id" > "$base/image.tar" 2>/dev/null; then + rm -rf "$base"; die "archive($role): docker save fehlgeschlagen (FAIL CLOSED)" + fi + chown root:root "$base/image.tar"; chmod 600 "$base/image.tar" + local asha asize + asha="$(sha256sum "$base/image.tar" | awk '{print $1}')" + asize="$(stat -c '%s' "$base/image.tar")" + # SHAs der Metadaten (zur Laufzeit, aus Build-Session / Host-SoT) + HOST_SOT_RUNNER_SHA_CUR="$(sha256sum /opt/historical-v2/app/run_backfill_year.py 2>/dev/null | awk '{print $1}')" + BUILD_INPUT_SHA_CUR="$(build_context_manifest 2>/dev/null | sha256sum | awk '{print $1}')" + WRAPPER_SHA_CUR="$(sha256sum /usr/local/bin/rq-historical | awk '{print $1}')" + APP_MANIFEST_SHA_CUR="$(sha256sum "$base/app.manifest" 2>/dev/null | awk '{print $1}')" + RUNTIME_CONFIG_SHA_CUR="$(sha256sum "$base/runtime-config.txt" 2>/dev/null | awk '{print $1}')" + gen_archive_meta "$role" "$base" "$id" "$asize" "$asha" + write_runtime_config "$role" "$base" + write_app_manifest "$role" "$base" "$id" + # SHA erneut nach manifest/runtime (weil meta SHA referenziert) + APP_MANIFEST_SHA_CUR="$(sha256sum "$base/app.manifest" | awk '{print $1}')" + RUNTIME_CONFIG_SHA_CUR="$(sha256sum "$base/runtime-config.txt" | awk '{print $1}')" + gen_archive_meta "$role" "$base" "$id" "$asize" "$asha" + echo "ARCHIVE($role) OK: $base/ (${asize} bytes, sha256 ${asha:0:16}...)" + echo " image.tar.sha256:" + sha256sum "$base/image.tar" > "$base/image.tar.sha256" + chown root:root "$base/image.tar.sha256"; chmod 600 "$base/image.tar.sha256" +} + +# pin-recovery-image / pin-remediation-image +pin_image() { # $1 = role + local role="$1" + expect_zero "${@:2}" + echo "=== PIN-$role ($RUN_MODE) ===" + local name tag id + name="$(role_pin "$role")" + tag="$(role_tag "$role")" + case "$role" in + recovery) id="$RECOVERY_GOLD_IMAGE_ID" ;; + remediation) + remediation_session || die "pin(remediation): keine gueltige Build-Session (FAIL CLOSED)" + id="$(remediation_full_image_id)" + PIN_REMEDIATION_TARGET_ID="$id" + ;; + esac + # Punkt 11: Containername darf NICHT existieren (kein Delete/Overwrite/Replace) + if docker container inspect "$name" >/dev/null 2>&1; then + # Identitaet pruefen; wenn korrekt -> ok (keine Aenderung); wenn falsch -> FAIL CLOSED + local existing + existing="$(docker container inspect -f '{{.Image}}' "$name" 2>/dev/null)" + if pin_container_identity_ok "$role" "$existing"; then + echo "PIN($role): Container existiert bereits korrekt (Image=$existing). KEINE Aenderung." + exit 0 + else + die "pin($role): Container $name existiert mit falschem Image ($existing). Kein Replace (FAIL CLOSED)" + fi + fi + echo " FULL_IMAGE_ID=$id" + # Punkt 6: Image muss inspectable sein (Source Gate) + if ! docker image inspect "$id" >/dev/null 2>&1; then + die "pin($role): Source Image nicht inspectable (SOURCE IMAGE NOT INSPECTABLE — FAIL CLOSED)" + fi + if [ "$RUN_MODE" = "DRY_RUN" ]; then + echo "DRY_RUN_PASS: pin($role) wuerde docker create --name $name --network none --restart no $id (keine Aktion)." + exit 0 + fi + mode_or_die + # MUTATION: minimaler Stopped-Container (Punkt 8/10) + echo " [create] docker create --name $name --network none --restart no $id" + if ! docker create --name "$name" --network none --restart no "$id" >/dev/null 2>&1; then + die "pin($role): docker create fehlgeschlagen (FAIL CLOSED)" + fi + echo "PIN($role) OK: $name -> $id (Stopped, Network=none, Restart=no)" +} + +# verify-recovery-pin / verify-remediation-pin / verify-...-archive / verify-...-ready +verify_pin_cmd() { # $1 = role (read-only) + local role="$1"; expect_zero "${@:2}" + echo "=== VERIFY-PIN($role) ===" + if verify_pin "$role"; then echo "VERIFY-PIN($role): PASS"; exit 0 + else die "VERIFY-PIN($role): FAIL (FAIL CLOSED)"; fi +} +verify_archive_cmd() { # $1 = role (read-only) + local role="$1"; expect_zero "${@:2}" + echo "=== VERIFY-ARCHIVE($role) ===" + verify_archive "$role" || die "VERIFY-ARCHIVE($role): FAIL (FAIL CLOSED)" + exit 0 +} +verify_ready_cmd() { # $1 = role (read-only) + local role="$1"; expect_zero "${@:2}" + verify_ready "$role" || die "$(case $role in recovery) echo 'RECOVERY READY';; remediation) echo 'CANDIDATE READY';; esac): FAIL (FAIL CLOSED)" + exit 0 +} + +# restore-recovery-image-archive (DESIGN; realer Restore NICHT in dieser Mission ausfuehren) +restore_recovery_dry() { # dry-run + expect_zero "$@" + echo "=== RESTORE-RECOVERY-IMAGE-ARCHIVE (DRY-RUN — DESIGN; kein echter Restore) ===" + local sdir sid base img + sdir="$(role_arch_dir recovery)" + sid="$RECOVERY_ARCHIVE_ID" + base="$sdir/$sid" + [ -d "$base" ] || die "restore-dry: Archiv-Session fehlt $base (FAIL CLOSED)" + echo " 1. Archiv SHA pruefen: $(sha256sum "$base/image.tar" | awk '{print $1}')" + echo " 2. Metadaten pruefen: session.meta vorhanden" + echo " 3. docker load (in realer Mission; hier NUR Dry-Run)" + echo " 4. Resulting Image inspect" + echo " 5. Application Manifest pruefen" + echo " 6. Runtime Config pruefen" + echo " 7. Tag/Identity pruefen" + echo " 8. danach Pin Verify" + echo " HINWEIS (Punkt 15): Wenn neu geladene FULL_IMAGE_ID != Original-ID bei gleicher App-Identitaet -> NICHT automatisch identisch; sauber Docker-Image-Identity vs Application/Runtime-Equivalence unterscheiden." + echo "RESTORE-RECOVERY-DRY-RUN: DESIGN OK (kein echter Restore ausgefuehrt)" + exit 0 +} + +# restore-recovery-image-archive (real — in dieser Mission NICHT ausfuehren; nur definiert) +restore_recovery_real() { + expect_zero "$@" + die "restore-recovery-image-archive: Realer Restore ist in dieser Mission NICHT erlaubt (HARTER STOPP Punkt 30). NUR dry-run (FAIL CLOSED)" } SUBCMD="${1:-}" @@ -1418,8 +1829,8 @@ if [ "$SUBCMD" = "dry-run" ]; then SUBCMD="${1:-}" shift || true case "$SUBCMD" in - build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch|build-runner-remediation|recreate-runner-remediation-safe) ;; - *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe zulaessig" ;; + build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|pin-current-candidate|recreate-candidate-safe|rollback-to-recovery-gold|promote-runner-patch|rollback-runner-patch|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image) ;; + *) die "dry-run nur mit build-historical|recreate-historical|snapshot-recovery|build-recovery-gold|build-runner-remediation|recreate-runner-remediation-safe|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image zulaessig" ;; esac fi @@ -1819,6 +2230,23 @@ case "$SUBCMD" in recreate-historical) recreate_historical "$@" ;; verify-gold-reproduction) verify_gold_reproduction "$@" ;; - *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|pin-current-candidate|dry-run pin-current-candidate|verify-candidate-image|recreate-candidate-safe|dry-run recreate-candidate-safe|rollback-to-recovery-gold|dry-run rollback-to-recovery-gold|promote-runner-patch|dry-run promote-runner-patch|rollback-runner-patch|dry-run rollback-runner-patch|build-runner-remediation|dry-run build-runner-remediation|verify-runner-remediation-image|recreate-runner-remediation-safe|dry-run recreate-runner-remediation-safe|verify-remediation-build-input|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion" + # ============ IMAGE-RETENTION / ARCHIVE CONTROL PLANE (Phase 13.5) ============ + # Schicht B (Archiv, docker save; fail-closed, kein Overwrite) & Schicht A (Pin). + # Alle verify-* sind read-only. Realer Restore ist in dieser Mission BLOCKIERT + # (nur dry-run). Kein Coolify-Cleanup-Disable, kein coolify.managed-Spoof. + archive-recovery-image) archive_image recovery "$@" ;; + archive-remediation-image) archive_image remediation "$@" ;; + pin-recovery-image) pin_image recovery "$@" ;; + pin-remediation-image) pin_image remediation "$@" ;; + verify-recovery-archive) verify_archive_cmd recovery "$@" ;; + verify-remediation-archive) verify_archive_cmd remediation "$@" ;; + verify-recovery-pin) verify_pin_cmd recovery "$@" ;; + verify-remediation-pin) verify_pin_cmd remediation "$@" ;; + verify-recovery-ready) verify_ready_cmd recovery "$@" ;; + verify-remediation-ready) verify_ready_cmd remediation "$@" ;; + restore-recovery-image-archive) restore_recovery_real "$@" ;; + dry-restore-recovery-image-archive) restore_recovery_dry "$@" ;; + + *) die "Aufruf: ps|inspect|logs|run|code |hashes|code-list|stage-dir|stage-check |test |search-writes|healthcheck|backup |deploy |rollback |build-def|build-files|host-manifest|host-code

|container-manifest|container-code

|tree-diff|backup-sot|sot-session|recover-sot |recover-sot-list|verify-sot-recovery|restore-sot|snapshot-recovery|dry-run snapshot-recovery|verify-recovery-snapshot|build-recovery-gold|dry-run build-recovery-gold|verify-recovery-gold-image|pin-current-candidate|dry-run pin-current-candidate|verify-candidate-image|recreate-candidate-safe|dry-run recreate-candidate-safe|rollback-to-recovery-gold|dry-run rollback-to-recovery-gold|promote-runner-patch|dry-run promote-runner-patch|rollback-runner-patch|dry-run rollback-runner-patch|build-runner-remediation|dry-run build-runner-remediation|verify-runner-remediation-image|recreate-runner-remediation-safe|dry-run recreate-runner-remediation-safe|verify-remediation-build-input|build-historical|dry-run build-historical|recreate-historical|dry-run recreate-historical|verify-gold-reproduction|create-build-exclusion|show-build-exclusion|archive-recovery-image|archive-remediation-image|pin-recovery-image|pin-remediation-image|verify-recovery-archive|verify-remediation-archive|verify-recovery-pin|verify-remediation-pin|verify-recovery-ready|verify-remediation-ready|dry-restore-recovery-image-archive|restore-recovery-image-archive" ;; esac