From c226af7bc76728f18ab932ee902e80a28921ceb4 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:20:37 +0200 Subject: [PATCH] Add self-contained data-disk reinstall kit --- docs/BARE_METAL_RECOVERY.md | 24 +++++ .../create-self-contained-data-kit.sh | 72 +++++++++++++ platform/recovery/reinstall-from-data.sh | 100 ++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100755 platform/recovery/create-self-contained-data-kit.sh create mode 100755 platform/recovery/reinstall-from-data.sh diff --git a/docs/BARE_METAL_RECOVERY.md b/docs/BARE_METAL_RECOVERY.md index c27d6fd..93f4079 100644 --- a/docs/BARE_METAL_RECOVERY.md +++ b/docs/BARE_METAL_RECOVERY.md @@ -142,3 +142,27 @@ Noch organisatorisch zwingend: Die private age-Identität muss eine zweite, vom Mac unabhängige Kopie in einem Passwortmanager oder auf einem Offline- Datenträger erhalten. Ohne diese Identität ist das verschlüsselte Bundle nicht wiederherstellbar. + +## Selbsttragender Recovery-Koffer auf der Data-SSD + +Für den speziellen Ausfall **nur der System-SSD** kann zusätzlich ein +vollständiger Recovery-Koffer unter `/data/mike-ai-recovery-kit` liegen. Er +enthält das verschlüsselte Bundle, den dafür benötigten Schlüssel, die gesamte +Git-Historie und ein eigenständiges Startskript. Auf einem frischen Debian: + +```bash +mount /data +sudo /data/mike-ai-recovery-kit/reinstall-athena.sh +``` + +Nach dem einmaligen Start läuft der Wiederaufbau selbständig. Falls NVIDIA- +Treiber oder der stabile Interface-Name einen Neustart erfordern, hinterlegt +das Skript einen systemd-Fortsetzer, bindet `/data` über die vorhandene UUID +dauerhaft ein und setzt den Ablauf nach dem Reboot fort. Nach drei erfolglosen +Versuchen bricht es gegen eine Bootschleife ab. + +Diese Bequemlichkeit besitzt bewusst eine andere Sicherheitsgrenze: Weil der +Entschlüsselungsschlüssel auf derselben Data-SSD liegt, kann eine Person mit +Lesezugriff auf diese SSD auch die enthaltenen Secrets entschlüsseln. Das +separate Off-Host-Bundle mit getrennt verwahrtem Schlüssel bleibt daher die +maßgebliche Sicherung gegen Diebstahl oder Verlust des gesamten Hosts. diff --git a/platform/recovery/create-self-contained-data-kit.sh b/platform/recovery/create-self-contained-data-kit.sh new file mode 100755 index 0000000..07d3590 --- /dev/null +++ b/platform/recovery/create-self-contained-data-kit.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +RECOVERY_BUNDLE=${1:-} +AGE_IDENTITY=${2:-} +SOURCE_BUNDLE=${3:-} +RELEASE_DIR=${4:-} +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) + +die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } +[[ $EUID -eq 0 ]] || die "Bitte als root ausführen." +[[ -s $RECOVERY_BUNDLE ]] || die "Recovery-Bundle fehlt." +[[ -s $AGE_IDENTITY ]] || die "age-Identität fehlt." +[[ -s $SOURCE_BUNDLE ]] || die "Git-Quellbundle fehlt." +[[ -n $RELEASE_DIR && $RELEASE_DIR == /data/* ]] || \ + die "Release-Ziel muss ein eindeutiger Pfad unter /data sein." +[[ ! -e $RELEASE_DIR ]] || die "Release-Ziel existiert bereits." +command -v age >/dev/null || die "age fehlt." +command -v git >/dev/null || die "git fehlt." + +stage=$(mktemp -d /tmp/mike-ai-data-kit.XXXXXX) +trap 'rm -rf "$stage"' EXIT +age -d -i "$AGE_IDENTITY" -o "$stage/bundle.tar.gz" "$RECOVERY_BUNDLE" +tar -C "$stage" -xzf "$stage/bundle.tar.gz" METADATA SHA256SUMS \ + host-config.tar.gz openwebui-data.tar.gz +(cd "$stage" && sha256sum -c SHA256SUMS) +commit=$(sed -n 's/^source_commit=//p' "$stage/METADATA" | head -n 1) +[[ $commit =~ ^[0-9a-f]{40}$ ]] || die "Recovery-Commit in METADATA ist ungültig." +git clone -q "$SOURCE_BUNDLE" "$stage/source-check" +git -C "$stage/source-check" cat-file -e "$commit^{commit}" + +install -d -m 0700 "$RELEASE_DIR" +install -m 0600 "$RECOVERY_BUNDLE" "$RELEASE_DIR/recovery.tar.age" +install -m 0600 "$AGE_IDENTITY" "$RELEASE_DIR/recovery.agekey" +install -m 0600 "$SOURCE_BUNDLE" "$RELEASE_DIR/source.git.bundle" +install -m 0700 "$ROOT_DIR/platform/recovery/reinstall-from-data.sh" \ + "$RELEASE_DIR/reinstall-athena.sh" +cat >"$RELEASE_DIR/kit.env" <"$RELEASE_DIR/README.txt" <<'EOF' +ATHENA SELF-CONTAINED DATA-DISK RECOVERY + +Auf einem frischen Debian die bestehende Data-SSD unter /data einhängen und +als root ausführen: + + /data/mike-ai-recovery-kit/reinstall-athena.sh + +Das Skript prüft alle Dateien, stellt einen persistenten /data-Mount her, +installiert minimale Werkzeuge und rekonstruiert den vollständigen MikeAI- +Stack. Erforderliche Treiber-Neustarts werden höchstens dreimal automatisch +fortgesetzt. + +SICHERHEIT: Dieses Kit enthält den Entschlüsselungsschlüssel. Wer die Data-SSD +lesen kann, kann daher auch die enthaltenen Infrastruktur-Secrets entschlüsseln. +EOF +chmod 0600 "$RELEASE_DIR/kit.env" "$RELEASE_DIR/README.txt" +( + cd "$RELEASE_DIR" + sha256sum recovery.tar.age recovery.agekey source.git.bundle \ + reinstall-athena.sh kit.env README.txt >SHA256SUMS +) +chmod 0600 "$RELEASE_DIR/SHA256SUMS" + +link=/data/mike-ai-recovery-kit +[[ ! -e $link || -L $link ]] || die "$link existiert und ist kein verwalteter Symlink." +ln -sfn "$(basename "$RELEASE_DIR")" "$link" +printf 'SELF_CONTAINED_DATA_KIT_OK %s -> %s\n' "$link" "$RELEASE_DIR" diff --git a/platform/recovery/reinstall-from-data.sh b/platform/recovery/reinstall-from-data.sh new file mode 100755 index 0000000..871a6d6 --- /dev/null +++ b/platform/recovery/reinstall-from-data.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Self-contained Athena reinstall entry point. This file is copied into a +# root-only recovery kit on /data; it is not run during normal installation. +set -Eeuo pipefail +umask 077 + +KIT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +CONFIG="$KIT_DIR/kit.env" +WORK_DIR=/var/lib/mike-ai-data-reinstall +SERVICE=/etc/systemd/system/mike-ai-data-reinstall.service + +die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } +log() { printf '\n==> %s\n' "$*"; } +[[ $EUID -eq 0 ]] || die "Bitte als root ausführen." +[[ -s $CONFIG ]] || die "kit.env fehlt im Recovery-Kit." +# shellcheck disable=SC1090 +source "$CONFIG" + +required=(RECOVERY_COMMIT RECOVERY_BUNDLE SOURCE_BUNDLE AGE_IDENTITY) +for name in "${required[@]}"; do + [[ -n ${!name:-} ]] || die "Pflichtwert $name fehlt." +done +for file in "$RECOVERY_BUNDLE" "$SOURCE_BUNDLE" "$AGE_IDENTITY"; do + [[ -s $KIT_DIR/$file ]] || die "Recovery-Datei fehlt: $file" +done + +log "Recovery-Kit kryptografisch prüfen" +(cd "$KIT_DIR" && sha256sum -c SHA256SUMS) + +log "Persistenten Mount für die Data-SSD sicherstellen" +[[ $(findmnt -n -o TARGET --target "$KIT_DIR") == /data ]] || \ + die "Recovery-Kit liegt nicht auf dem eingehängten /data-Dateisystem." +if ! findmnt -s -n -o TARGET | grep -qx /data; then + source_device=$(findmnt -n -o SOURCE --target "$KIT_DIR") + source_uuid=$(blkid -s UUID -o value "$source_device") + source_type=$(findmnt -n -o FSTYPE --target "$KIT_DIR") + [[ -n $source_uuid && -n $source_type ]] || \ + die "UUID oder Dateisystemtyp der Data-SSD konnte nicht bestimmt werden." + printf 'UUID=%s /data %s defaults,nofail,x-systemd.device-timeout=30 0 2\n' \ + "$source_uuid" "$source_type" >>/etc/fstab + systemctl daemon-reload +fi + +log "Minimale Wiederherstellungswerkzeuge installieren" +apt-get update +DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + age ca-certificates git rsync + +install -d -m 0700 "$WORK_DIR" +if [[ ! -d $WORK_DIR/repository/.git ]]; then + git clone "$KIT_DIR/$SOURCE_BUNDLE" "$WORK_DIR/repository" +fi +git -C "$WORK_DIR/repository" checkout --detach "$RECOVERY_COMMIT" +[[ $(git -C "$WORK_DIR/repository" rev-parse HEAD) == "$RECOVERY_COMMIT" ]] || \ + die "Der geforderte Recovery-Commit ist nicht ausgecheckt." + +attempt=0 +[[ ! -s $WORK_DIR/attempt ]] || attempt=$(<"$WORK_DIR/attempt") +(( attempt += 1 )) +printf '%s\n' "$attempt" >"$WORK_DIR/attempt" +(( attempt <= 3 )) || die "Mehr als drei automatische Recovery-Versuche; Abbruch gegen Bootschleife." + +log "Bare-Metal-Wiederherstellung ausführen (Versuch $attempt/3)" +set +e +"$WORK_DIR/repository/platform/recovery/restore-recovery-bundle.sh" \ + "$KIT_DIR/$RECOVERY_BUNDLE" "$KIT_DIR/$AGE_IDENTITY" +status=$? +set -e + +if [[ $status == 20 || $status == 21 ]]; then + log "Ein kontrollierter Treiber-/Netzwerk-Neustart ist erforderlich" + cat >"$SERVICE" </dev/null 2>&1 || true +rm -f "$SERVICE" "$WORK_DIR/attempt" +systemctl daemon-reload +printf '\nDATA_DISK_REINSTALL_OK\n' +printf 'OpenWebUI, Router, Modelle und MCPs wurden wiederhergestellt.\n'