Files
betreuungs-cockpit/scripts/wiederherstellung.sh
T
Florian Wilde 4d826ef55f Dokumente mitsichern - eine Akte ohne ihre Dateien ist wertlos
Der Sicherungsdienst machte taeglich ein pg_dump und sonst nichts. Nach
einem Plattenschaden waere die Akte vollstaendig wiederhergestellt worden
- mit allen Eintraegen, Verweisen und Pruefsummen, aber ohne eine einzige
Datei dahinter. Die Pruefsumme in der Akte haette dann ein Dokument
belegt, das es nicht mehr gibt. Gegenueber dem Betreuungsgericht ist das
der schlechteste denkbare Zustand.

Je Lauf entstehen jetzt zwei verschluesselte Dateien mit demselben
Zeitstempel: der Datenbankstand und ein Archiv des Ablageverzeichnisses.
Beide mit Pruefsumme, beide in der Aufbewahrung, beide im Monatsstand,
beide in der Selbstpruefung.

Schlaegt der Dokumententeil fehl, bricht der Lauf ab und raeumt den
angefangenen Datenbankstand wieder weg. Eine Sicherung, die nur die halbe
Wahrheit enthaelt, aber vollstaendig aussieht, ist schlimmer als gar
keine: Man verlaesst sich darauf und merkt es erst im Ernstfall.

## Abweichung von der urspruenglichen Idee

Im Issue stand "mc mirror statt rohem Verzeichnis". Beim Umsetzen zeigte
sich, dass der MinIO-Client in diesem Behaelter fehlt und bei jedem Lauf
aus dem Netz geholt werden muesste. Eine Internetabhaengigkeit im
Sicherungspfad ist genau das, was man dort nicht will.

Stattdessen wird das Verzeichnis direkt eingepackt. Das ist vertretbar,
weil MinIO ein Objekt erst vollstaendig unter .minio.sys/tmp schreibt und
dann an seinen Platz umbenennt: Ein laufender Upload liegt nie halbfertig
im Archiv, er fehlt hoechstens. Derselbe Weg deckt den Dateitreiber mit
ab.

## Nur lesend

Der Sicherungsdienst bekommt das Ablageverzeichnis mit ":ro" eingehaengt.
Er laeuft unbeaufsichtigt per cron; ein Fehler im Skript oder ein
kompromittierter Behaelter wuerde sonst Sicherung und Bestand auf einmal
treffen - genau der Fall, gegen den eine Sicherung da ist.

Deshalb schreibt die Wiederherstellung die Dateien auch nicht selbst an
ihren Platz, sondern packt sie nach ./backups aus und gibt den letzten
Handgriff als Einzeiler aus. In ein laufendes MinIO hineinzuschreiben
wuerde dessen Datenbestand ohnehin beschaedigen.

## Geprueft

Die Kernlogik einmal von Hand durchgespielt: packen, verschluesseln,
Selbstpruefung, zurueckholen - der Inhalt kam bitgleich zurueck, der
Klartext war im Archiv nicht auffindbar, und die Selbstpruefung schlug
sowohl bei einem beschaedigten Archiv als auch bei falschem Passwort an.

Dazu elf Pruefungen in tests/skripte.test.ts gegen vier Mutationen:
Dokumententeil entfernt, Abbruch entfernt, Aufraeumen halbiert,
Schreibrecht erteilt - jede wird rot.

Refs #41
2026-09-01 16:03:26 +02:00

161 lines
6.2 KiB
Bash
Executable File

#!/bin/sh
# =============================================================================
# Sicherung zurueckspielen
# =============================================================================
#
# docker compose exec sicherung /bin/sh /wiederherstellung.sh /backups/cockpit_2026-08-22_0315.dump.enc
#
# Das Skript spielt eine verschluesselte Sicherung zurueck. Es fragt
# ausdruecklich nach, weil dabei der aktuelle Datenbestand ueberschrieben
# wird.
#
# DREI DINGE, DIE MAN VORHER WISSEN MUSS:
#
# 1. Die Sicherung enthaelt die anwendungsseitig verschluesselten Felder,
# aber NICHT den Schluessel. Ohne den passenden ENC_KEY_Vn aus der .env
# bleiben Diagnosen, IBANs, Versicherungsnummern und Notizen unlesbar -
# die Datenbank ist dann zwar da, aber halb blind.
#
# 2. Ein Backup, das nie zurueckgespielt wurde, ist kein Backup. Probiere
# das hier einmal im Jahr auf einer Testdatenbank aus, bevor du es im
# Ernstfall zum ersten Mal brauchst. Siehe README, Abschnitt
# "Wiederherstellung ueben".
#
# 3. Die Dokumente werden NICHT selbsttaetig an ihren Platz geschrieben.
# Dieses Skript packt sie nach ./backups aus und sagt, wie es
# weitergeht. Grund: Der Sicherungsdienst hat auf das Ablageverzeichnis
# bewusst nur Lesezugriff (siehe docker-compose.yml), und in ein
# laufendes MinIO hineinzuschreiben wuerde dessen Datenbestand
# beschaedigen. Der letzte Handgriff bleibt beim Menschen - er ist ein
# Einzeiler und im Klartext beschrieben.
# =============================================================================
set -eu
DATEI="${1:-}"
if [ -z "$DATEI" ]; then
echo "Aufruf: $0 <pfad-zur-sicherung.dump.enc>" >&2
echo "" >&2
echo "Vorhandene Sicherungen:" >&2
ls -lh "${SICHERUNG_ZIEL:-/backups}"/cockpit_*.dump.enc 2>/dev/null >&2 || echo " keine gefunden" >&2
exit 1
fi
if [ ! -f "$DATEI" ]; then
echo "FEHLER: $DATEI gibt es nicht." >&2
exit 1
fi
if [ -z "${SICHERUNG_PASSWORT:-}" ]; then
echo "FEHLER: SICHERUNG_PASSWORT ist nicht gesetzt." >&2
exit 1
fi
# --- Pruefsumme -------------------------------------------------------------
if [ -f "$DATEI.sha256" ]; then
echo "Pruefsumme wird geprueft ..."
if sha256sum -c "$DATEI.sha256" >/dev/null 2>&1; then
echo " in Ordnung."
else
echo "FEHLER: Die Pruefsumme stimmt nicht. Die Datei ist beschaedigt." >&2
exit 1
fi
else
echo "WARNUNG: Keine Pruefsummendatei gefunden - es wird ungeprueft fortgefahren."
fi
# --- Sicherheitsabfrage ------------------------------------------------------
echo ""
echo "Es wird zurueckgespielt nach:"
echo " Datenbank: ${PGDATABASE:-?} auf ${PGHOST:-?}"
echo " Aus Datei: $DATEI"
echo ""
echo "DER AKTUELLE DATENBESTAND WIRD DABEI UEBERSCHRIEBEN."
echo ""
if [ "${SICHERUNG_OHNE_RUECKFRAGE:-}" != "ja" ]; then
printf "Zum Fortfahren bitte JA eingeben: "
read -r antwort
if [ "$antwort" != "JA" ]; then
echo "Abgebrochen."
exit 1
fi
fi
# --- Zurueckspielen ----------------------------------------------------------
echo ""
echo "$(date -Iseconds) Wiederherstellung beginnt ..."
# --clean --if-exists raeumt vorhandene Objekte ab, bevor sie neu angelegt
# werden. --no-owner, weil der Eigentuemer auf einem anderen Server anders
# heissen kann.
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
-pass env:SICHERUNG_PASSWORT -in "$DATEI" \
| pg_restore --clean --if-exists --no-owner --no-privileges \
--dbname="${PGDATABASE:-postgres}" --exit-on-error
echo "$(date -Iseconds) Datenbank wiederhergestellt."
# --- Dokumente ---------------------------------------------------------------
# Zu "cockpit_2026-09-01_0315.dump.enc" gehoert
# "dokumente_2026-09-01_0315.tar.gz.enc" - gleicher Zeitstempel, anderer
# Name. Fehlt sie, stammt die Sicherung aus einer Fassung vor v1.11.1.
ZEITSTEMPEL="$(basename "$DATEI" | sed -n 's/^cockpit_\(.*\)\.dump\.enc$/\1/p')"
DOKUMENTE_DATEI="$(dirname "$DATEI")/dokumente_${ZEITSTEMPEL}.tar.gz.enc"
AUSPACKZIEL="$(dirname "$DATEI")/dokumente-wiederhergestellt_${ZEITSTEMPEL}"
if [ ! -f "$DOKUMENTE_DATEI" ]; then
echo ""
echo "WARNUNG: Zu dieser Sicherung gibt es kein Dokumentenarchiv."
echo " Erwartet wurde: $DOKUMENTE_DATEI"
echo " Sicherungen aus Fassungen vor v1.11.1 enthalten nur die"
echo " Datenbank. Die Akte ist dann vollstaendig, die Dateien"
echo " dahinter fehlen."
else
if [ -f "$DOKUMENTE_DATEI.sha256" ]; then
echo "Pruefsumme des Dokumentenarchivs wird geprueft ..."
if sha256sum -c "$DOKUMENTE_DATEI.sha256" >/dev/null 2>&1; then
echo " in Ordnung."
else
echo "FEHLER: Die Pruefsumme des Dokumentenarchivs stimmt nicht." >&2
exit 1
fi
fi
mkdir -p "$AUSPACKZIEL"
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
-pass env:SICHERUNG_PASSWORT -in "$DOKUMENTE_DATEI" \
| tar -xzf - -C "$AUSPACKZIEL"
ANZAHL="$(find "$AUSPACKZIEL" -type f | wc -l)"
echo "$(date -Iseconds) Dokumente ausgepackt: $ANZAHL Dateien nach"
echo " $AUSPACKZIEL"
fi
echo ""
echo "Naechste Schritte:"
echo " 1. Die SQL-Skripte erneut ausfuehren (Volltextsuche und Audit-Schutz):"
echo " docker compose exec app npm run db:sql"
echo " 2. Pruefen, ob ENC_KEY_V1 in der .env zu diesem Bestand passt -"
echo " ohne den richtigen Schluessel bleiben die verschluesselten"
echo " Felder unlesbar."
echo " 3. Die Dokumente an ihren Platz bringen. Dazu MinIO anhalten, das"
echo " Datenverzeichnis ersetzen und wieder starten - auf dem Host,"
echo " nicht in diesem Behaelter:"
echo ""
echo " docker compose stop minio app"
echo " docker run --rm \\"
echo " -v betreuungs-cockpit_minio-daten:/ziel \\"
echo " -v \"\$PWD/backups:/quelle:ro\" alpine \\"
echo " sh -c 'rm -rf /ziel/* /ziel/..?* && cp -a /quelle/dokumente-wiederhergestellt_*/. /ziel/'"
echo " docker compose up -d"
echo ""
echo " Steht mehr als ein ausgepacktes Verzeichnis unter ./backups,"
echo " vorher das richtige auswaehlen statt des Sternchens."
echo ""
echo " 4. In der Anwendung eine betreute Person oeffnen und nachsehen, ob"
echo " die Diagnosen im Klartext erscheinen. Dann ein Dokument"
echo " herunterladen. Erst wenn beides klappt, ist die"
echo " Wiederherstellung wirklich gelungen."