Add self-contained data-disk reinstall kit
This commit is contained in:
@@ -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-
|
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
|
Datenträger erhalten. Ohne diese Identität ist das verschlüsselte Bundle nicht
|
||||||
wiederherstellbar.
|
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-PARTITION> /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.
|
||||||
|
|||||||
+72
@@ -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" <<EOF
|
||||||
|
RECOVERY_COMMIT=$commit
|
||||||
|
RECOVERY_BUNDLE=recovery.tar.age
|
||||||
|
SOURCE_BUNDLE=source.git.bundle
|
||||||
|
AGE_IDENTITY=recovery.agekey
|
||||||
|
EOF
|
||||||
|
cat >"$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"
|
||||||
Executable
+100
@@ -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" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Resume MikeAI data-disk disaster recovery
|
||||||
|
After=network-online.target local-fs.target
|
||||||
|
Wants=network-online.target
|
||||||
|
RequiresMountsFor=/data
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=$KIT_DIR/reinstall-athena.sh --resume
|
||||||
|
StandardOutput=append:/var/log/mike-ai-data-reinstall.log
|
||||||
|
StandardError=append:/var/log/mike-ai-data-reinstall.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable mike-ai-data-reinstall.service
|
||||||
|
sync
|
||||||
|
systemctl reboot
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
[[ $status == 0 ]] || die "Wiederherstellung ist mit Status $status fehlgeschlagen."
|
||||||
|
|
||||||
|
systemctl disable mike-ai-data-reinstall.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'
|
||||||
Reference in New Issue
Block a user