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)

This commit is contained in:
root 2026-08-30 06:05:56 +00:00
parent 8187a4bc1d
commit 65c1d57fa7

View file

@ -187,6 +187,32 @@ RUNNER_REMEDIATION_SHA="6cca3c55bb559d75be2f58e59c231003902f124a54ada25582970524
# Candidate/Recovery duerfen code-identisch sein, aber verschiedene Image-IDs haben (KEIN Fehler). # Candidate/Recovery duerfen code-identisch sein, aber verschiedene Image-IDs haben (KEIN Fehler).
REM_NOOP_IMAGE_ID="sha256:1f3ba210780c5a00d4b73c5ee7817a88671fee2607a4ae0d5b1cefef1a1261c7" 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 (<IMAGE_ID_SHORT>), 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 (Mutation Guard): READ_ONLY | DRY_RUN | MUTATING
RUN_MODE=MUTATING RUN_MODE=MUTATING
@ -1406,7 +1432,392 @@ validate_stage() {
# ---- redact: maskiert Passwort-/Secret-Werte (fuer build-def Anzeige) ---- # ---- redact: maskiert Passwort-/Secret-Werte (fuer build-def Anzeige) ----
redact_line() { redact_line() {
# Zeilen mit "PASSWORD=" / "SECRET=" / "TOKEN=" : Wert maskieren # 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/<recovery|candidates>
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:-}" SUBCMD="${1:-}"
@ -1418,8 +1829,8 @@ if [ "$SUBCMD" = "dry-run" ]; then
SUBCMD="${1:-}" SUBCMD="${1:-}"
shift || true shift || true
case "$SUBCMD" in 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) ;; 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 zulaessig" ;; *) 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 esac
fi fi
@ -1819,6 +2230,23 @@ case "$SUBCMD" in
recreate-historical) recreate_historical "$@" ;; recreate-historical) recreate_historical "$@" ;;
verify-gold-reproduction) verify_gold_reproduction "$@" ;; verify-gold-reproduction) verify_gold_reproduction "$@" ;;
*) die "Aufruf: ps|inspect|logs|run|code <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|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 <K>|hashes|code-list|stage-dir|stage-check <K>|test <K>|search-writes|healthcheck|backup <K>|deploy <K>|rollback <K>|build-def|build-files|host-manifest|host-code <p>|container-manifest|container-code <p>|tree-diff|backup-sot|sot-session|recover-sot <K>|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 esac