Fix #41: Dokumente mitsichern #48
@@ -155,9 +155,20 @@ services:
|
|||||||
PGDATABASE: ${POSTGRES_DB:-betreuungs_cockpit}
|
PGDATABASE: ${POSTGRES_DB:-betreuungs_cockpit}
|
||||||
SICHERUNG_PASSWORT: ${SICHERUNG_PASSWORT:?SICHERUNG_PASSWORT fehlt in der .env}
|
SICHERUNG_PASSWORT: ${SICHERUNG_PASSWORT:?SICHERUNG_PASSWORT fehlt in der .env}
|
||||||
SICHERUNG_AUFBEWAHRUNG_TAGE: ${SICHERUNG_AUFBEWAHRUNG_TAGE:-30}
|
SICHERUNG_AUFBEWAHRUNG_TAGE: ${SICHERUNG_AUFBEWAHRUNG_TAGE:-30}
|
||||||
|
SICHERUNG_DOKUMENTE_QUELLE: /dokumente
|
||||||
TZ: Europe/Berlin
|
TZ: Europe/Berlin
|
||||||
volumes:
|
volumes:
|
||||||
- ./backups:/backups
|
- ./backups:/backups
|
||||||
|
# Die Dokumente, ausdruecklich NUR LESEND.
|
||||||
|
#
|
||||||
|
# Der Sicherungsdienst laeuft unbeaufsichtigt per cron. Ein Dienst,
|
||||||
|
# der die Daten zerstoeren kann, die er sichern soll, ist ein Risiko
|
||||||
|
# ohne Gegenwert - ein Fehler im Skript oder ein kompromittierter
|
||||||
|
# Behaelter wuerde beides auf einmal treffen.
|
||||||
|
#
|
||||||
|
# Zurueckspielen geschieht deshalb nicht hierher, sondern in ein
|
||||||
|
# Verzeichnis unter ./backups; siehe scripts/wiederherstellung.sh.
|
||||||
|
- minio-daten:/dokumente:ro
|
||||||
- ./scripts/sicherung.sh:/sicherung.sh:ro
|
- ./scripts/sicherung.sh:/sicherung.sh:ro
|
||||||
- ./scripts/wiederherstellung.sh:/wiederherstellung.sh:ro
|
- ./scripts/wiederherstellung.sh:/wiederherstellung.sh:ro
|
||||||
entrypoint: ['/bin/sh', '-c']
|
entrypoint: ['/bin/sh', '-c']
|
||||||
|
|||||||
+39
-8
@@ -731,13 +731,19 @@ Erwartete Ausgabe, in dieser Reihenfolge:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Sicherung beginnt: /backups/cockpit_2026-08-22_1430.dump.enc
|
Sicherung beginnt: /backups/cockpit_2026-08-22_1430.dump.enc
|
||||||
Sicherung fertig (1.2M)
|
Datenbank gesichert (1.2M)
|
||||||
Selbstpruefung bestanden.
|
Dokumente gesichert (18M)
|
||||||
|
Selbstpruefung bestanden - Datenbank und Dokumente.
|
||||||
```
|
```
|
||||||
|
|
||||||
Die **Selbstprüfung** ist der eigentliche Punkt: Das Skript entschlüsselt
|
Die **Selbstprüfung** ist der eigentliche Punkt: Das Skript entschlüsselt
|
||||||
die eben geschriebene Datei sofort wieder. Schlägt das fehl, endet der
|
beide Dateien sofort wieder und liest sie einmal durch. Schlägt das fehl,
|
||||||
Lauf mit einem Fehler statt mit einem stillen Scheinerfolg.
|
endet der Lauf mit einem Fehler statt mit einem stillen Scheinerfolg.
|
||||||
|
|
||||||
|
Fehlt die Zeile `Dokumente gesichert`, ist das Ablageverzeichnis nicht in
|
||||||
|
den Sicherungsdienst eingehängt. Das Skript bricht dann ab und räumt den
|
||||||
|
angefangenen Datenbankstand wieder weg — eine Sicherung, die nur die halbe
|
||||||
|
Wahrheit enthält, aber vollständig aussieht, wäre schlimmer als gar keine.
|
||||||
|
|
||||||
### Und jetzt einmal zurückspielen
|
### Und jetzt einmal zurückspielen
|
||||||
|
|
||||||
@@ -760,6 +766,25 @@ docker compose exec db psql -U cockpit probe \
|
|||||||
docker compose exec db dropdb -U cockpit probe
|
docker compose exec db dropdb -U cockpit probe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Beim Zurückspielen packt das Skript auch das Dokumentenarchiv aus — nach
|
||||||
|
`backups/dokumente-wiederhergestellt_<datum>/`. An ihren Platz schreibt es
|
||||||
|
die Dateien **nicht**: Der Sicherungsdienst hat auf das Ablageverzeichnis
|
||||||
|
bewusst nur Lesezugriff, und in ein laufendes MinIO hineinzuschreiben würde
|
||||||
|
dessen Datenbestand beschädigen. Wie der letzte Handgriff aussieht, gibt
|
||||||
|
das Skript am Ende selbst aus.
|
||||||
|
|
||||||
|
Für die Übung genügt ein Blick in das ausgepackte Verzeichnis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find backups/dokumente-wiederhergestellt_*/ -type f | head
|
||||||
|
rm -rf backups/dokumente-wiederhergestellt_*
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Im Ernstfall gehört beides zusammen.** Eine Datenbank ohne die
|
||||||
|
> zugehörigen Dateien ergibt eine Akte, deren Prüfsummen Dokumente
|
||||||
|
> belegen, die es nicht mehr gibt. Spiel immer das Paar mit demselben
|
||||||
|
> Zeitstempel zurück.
|
||||||
|
|
||||||
### Kontrolle
|
### Kontrolle
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -767,12 +792,18 @@ ls -lh backups/
|
|||||||
# cockpit_....dump.enc und cockpit_....dump.enc.sha256
|
# cockpit_....dump.enc und cockpit_....dump.enc.sha256
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Was das Backup nicht enthält:** den Schlüssel `ENC_KEY_V1` und die
|
> **Was das Backup enthält:** die Datenbank *und* die Dokumente. Je Lauf
|
||||||
> Dateien im Objektspeicher. Für die Dateien:
|
> entstehen zwei verschlüsselte Dateien mit demselben Zeitstempel:
|
||||||
>
|
>
|
||||||
> ```bash
|
|
||||||
> docker compose exec minio mc mirror lokal/betreuungs-cockpit /backups/dokumente
|
|
||||||
> ```
|
> ```
|
||||||
|
> cockpit_2026-09-01_0315.dump.enc <- Akte, Fristen, Buchungen
|
||||||
|
> dokumente_2026-09-01_0315.tar.gz.enc <- Bescheide, Schreiben, Belege
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> **Was es nicht enthält:** den Schlüssel `ENC_KEY_V1`. Ohne ihn bleiben
|
||||||
|
> auch aus einer vollständig zurückgespielten Sicherung die Diagnosen,
|
||||||
|
> IBANs, Notizen und Dokumente unlesbar. Er gehört an die zwei Orte aus
|
||||||
|
> Schritt 11.
|
||||||
>
|
>
|
||||||
> Und kopier die Sicherungen regelmäßig **weg vom Server** — ein Backup,
|
> Und kopier die Sicherungen regelmäßig **weg vom Server** — ein Backup,
|
||||||
> das neben dem Original liegt, hilft bei einem Serverausfall nicht.
|
> das neben dem Original liegt, hilft bei einem Serverausfall nicht.
|
||||||
|
|||||||
+81
-18
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Taegliche Sicherung der Datenbank
|
# Taegliche Sicherung: Datenbank UND Dokumente
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
#
|
#
|
||||||
# Laeuft im Behaelter "sicherung" per cron um 03:15 Uhr. Von Hand:
|
# Laeuft im Behaelter "sicherung" per cron um 03:15 Uhr. Von Hand:
|
||||||
@@ -10,17 +10,44 @@
|
|||||||
# Was passiert:
|
# Was passiert:
|
||||||
# 1. pg_dump im eigenen Format (-Fc), damit einzelne Tabellen
|
# 1. pg_dump im eigenen Format (-Fc), damit einzelne Tabellen
|
||||||
# wiederherstellbar bleiben.
|
# wiederherstellbar bleiben.
|
||||||
# 2. Verschluesselung mit AES-256 und PBKDF2 aus SICHERUNG_PASSWORT.
|
# 2. Die abgelegten Dokumente als Archiv.
|
||||||
|
# 3. Beides verschluesselt mit AES-256 und PBKDF2 aus SICHERUNG_PASSWORT.
|
||||||
# Das Backup liegt oft auf fremdem Speicher - unverschluesselt waere
|
# Das Backup liegt oft auf fremdem Speicher - unverschluesselt waere
|
||||||
# es ein Datenleck mit Ansage.
|
# es ein Datenleck mit Ansage.
|
||||||
# 3. SHA-256-Pruefsumme daneben.
|
# 4. SHA-256-Pruefsumme je Datei.
|
||||||
# 4. Staende, die aelter sind als SICHERUNG_AUFBEWAHRUNG_TAGE, entfernen.
|
# 5. Staende, die aelter sind als SICHERUNG_AUFBEWAHRUNG_TAGE, entfernen.
|
||||||
# Der jeweils erste Stand eines Monats bleibt stehen.
|
# Der jeweils erste Stand eines Monats bleibt stehen.
|
||||||
#
|
#
|
||||||
# WICHTIG: Dieses Backup enthaelt die verschluesselten Felder, aber NICHT
|
# ## Warum die Dokumente dazugehoeren
|
||||||
# den Schluessel ENC_KEY_V1. Ohne ihn sind Diagnosen, IBANs und Notizen
|
#
|
||||||
# auch aus einer wiederhergestellten Datenbank nicht lesbar. Den Schluessel
|
# Bis v1.11.0 sicherte dieses Skript nur die Datenbank. Nach einem
|
||||||
# getrennt sichern - Passwortmanager oder ausgedruckt im Ordner.
|
# 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.
|
||||||
|
#
|
||||||
|
# ## Warum das Ablageverzeichnis roh eingepackt wird
|
||||||
|
#
|
||||||
|
# Der naheliegende Weg waere "mc mirror" aus dem MinIO-Client. Der ist in
|
||||||
|
# diesem Behaelter aber nicht vorhanden und muesste bei jedem Lauf aus dem
|
||||||
|
# Netz geholt werden - eine Internetabhaengigkeit im Sicherungspfad ist
|
||||||
|
# genau das, was man dort nicht will.
|
||||||
|
#
|
||||||
|
# Stattdessen wird das Datenverzeichnis 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
|
||||||
|
# also nie halbfertig im Archiv, er fehlt hoechstens. Bei einem Lauf um
|
||||||
|
# 03:15 Uhr auf einem Einzelplatzsystem ist auch das unwahrscheinlich.
|
||||||
|
#
|
||||||
|
# Derselbe Weg deckt den Dateitreiber mit ab - dort liegen die Dateien
|
||||||
|
# ebenfalls einfach im Verzeichnis.
|
||||||
|
#
|
||||||
|
# WICHTIG: Dieses Backup enthaelt die verschluesselten Felder und die
|
||||||
|
# verschluesselten Dateien, aber NICHT den Schluessel ENC_KEY_V1. Ohne ihn
|
||||||
|
# sind Diagnosen, IBANs, Notizen und Dokumente auch aus einer
|
||||||
|
# wiederhergestellten Sicherung nicht lesbar. Den Schluessel getrennt
|
||||||
|
# sichern - Passwortmanager oder ausgedruckt im Ordner.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
@@ -30,6 +57,11 @@ AUFBEWAHRUNG="${SICHERUNG_AUFBEWAHRUNG_TAGE:-30}"
|
|||||||
ZEITSTEMPEL="$(date +%Y-%m-%d_%H%M)"
|
ZEITSTEMPEL="$(date +%Y-%m-%d_%H%M)"
|
||||||
TAG_IM_MONAT="$(date +%d)"
|
TAG_IM_MONAT="$(date +%d)"
|
||||||
DATEI="$ZIEL/cockpit_${ZEITSTEMPEL}.dump.enc"
|
DATEI="$ZIEL/cockpit_${ZEITSTEMPEL}.dump.enc"
|
||||||
|
DOKUMENTE_DATEI="$ZIEL/dokumente_${ZEITSTEMPEL}.tar.gz.enc"
|
||||||
|
|
||||||
|
# Wo die Dokumente liegen. Der Behaelter bekommt das Ablageverzeichnis
|
||||||
|
# eingehaengt - siehe docker-compose.yml, Dienst "sicherung".
|
||||||
|
DOKUMENTE_QUELLE="${SICHERUNG_DOKUMENTE_QUELLE:-/dokumente}"
|
||||||
|
|
||||||
if [ -z "${SICHERUNG_PASSWORT:-}" ]; then
|
if [ -z "${SICHERUNG_PASSWORT:-}" ]; then
|
||||||
echo "$(date -Iseconds) FEHLER: SICHERUNG_PASSWORT ist nicht gesetzt. Abbruch." >&2
|
echo "$(date -Iseconds) FEHLER: SICHERUNG_PASSWORT ist nicht gesetzt. Abbruch." >&2
|
||||||
@@ -50,17 +82,39 @@ pg_dump --format=custom --compress=6 --no-owner --no-privileges \
|
|||||||
GROESSE="$(du -h "$DATEI" | cut -f1)"
|
GROESSE="$(du -h "$DATEI" | cut -f1)"
|
||||||
sha256sum "$DATEI" > "$DATEI.sha256"
|
sha256sum "$DATEI" > "$DATEI.sha256"
|
||||||
|
|
||||||
# Der jeweils erste Stand eines Monats bekommt eine Markierung und bleibt
|
echo "$(date -Iseconds) Datenbank gesichert ($GROESSE)"
|
||||||
# beim Aufraeumen verschont.
|
|
||||||
if [ "$TAG_IM_MONAT" = "01" ]; then
|
# --- Dokumente ---------------------------------------------------------------
|
||||||
touch "$DATEI.monatsstand"
|
# Schlaegt dieser Teil fehl, bricht der ganze Lauf ab (set -e). Das ist
|
||||||
|
# Absicht: 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.
|
||||||
|
if [ ! -d "$DOKUMENTE_QUELLE" ]; then
|
||||||
|
echo "$(date -Iseconds) FEHLER: Das Ablageverzeichnis $DOKUMENTE_QUELLE ist nicht eingehaengt." >&2
|
||||||
|
echo " Ohne die Dokumente ist die Sicherung unvollstaendig." >&2
|
||||||
|
echo " Siehe docker-compose.yml, Dienst \"sicherung\"." >&2
|
||||||
|
rm -f "$DATEI" "$DATEI.sha256"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$(date -Iseconds) Sicherung fertig ($GROESSE)"
|
# "tar" ohne Leitung an einen Verbraucher, der frueh schliesst - sonst
|
||||||
|
# SIGPIPE, siehe die Anmerkung zur Selbstpruefung weiter unten.
|
||||||
|
tar -czf - -C "$DOKUMENTE_QUELLE" . | openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt -pass env:SICHERUNG_PASSWORT -out "$DOKUMENTE_DATEI"
|
||||||
|
|
||||||
|
DOK_GROESSE="$(du -h "$DOKUMENTE_DATEI" | cut -f1)"
|
||||||
|
sha256sum "$DOKUMENTE_DATEI" > "$DOKUMENTE_DATEI.sha256"
|
||||||
|
|
||||||
|
echo "$(date -Iseconds) Dokumente gesichert ($DOK_GROESSE)"
|
||||||
|
|
||||||
|
# Der jeweils erste Stand eines Monats bekommt eine Markierung und bleibt
|
||||||
|
# beim Aufraeumen verschont. Beide Teile, sonst faellt die Haelfte weg.
|
||||||
|
if [ "$TAG_IM_MONAT" = "01" ]; then
|
||||||
|
touch "$DATEI.monatsstand" "$DOKUMENTE_DATEI.monatsstand"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Aufraeumen --------------------------------------------------------------
|
# --- Aufraeumen --------------------------------------------------------------
|
||||||
ENTFERNT=0
|
ENTFERNT=0
|
||||||
for alt in "$ZIEL"/cockpit_*.dump.enc; do
|
for alt in "$ZIEL"/cockpit_*.dump.enc "$ZIEL"/dokumente_*.tar.gz.enc; do
|
||||||
[ -e "$alt" ] || continue
|
[ -e "$alt" ] || continue
|
||||||
[ -e "$alt.monatsstand" ] && continue
|
[ -e "$alt.monatsstand" ] && continue
|
||||||
|
|
||||||
@@ -87,9 +141,18 @@ KOPF="$(openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
|
|||||||
-pass env:SICHERUNG_PASSWORT -in "$DATEI" 2>/dev/null \
|
-pass env:SICHERUNG_PASSWORT -in "$DATEI" 2>/dev/null \
|
||||||
| dd bs=1 count=5 2>/dev/null || true)"
|
| dd bs=1 count=5 2>/dev/null || true)"
|
||||||
|
|
||||||
if [ "$KOPF" = "PGDMP" ]; then
|
if [ "$KOPF" != "PGDMP" ]; then
|
||||||
echo "$(date -Iseconds) Selbstpruefung bestanden."
|
echo "$(date -Iseconds) FEHLER: Der Datenbankstand laesst sich nicht entschluesseln!" >&2
|
||||||
else
|
|
||||||
echo "$(date -Iseconds) FEHLER: Die Sicherung laesst sich nicht entschluesseln!" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Dasselbe fuer die Dokumente: entschluesseln und das Archiv auflisten
|
||||||
|
# lassen. "tar -tzf" liest die ganze Datei und meldet jeden Schaden - das
|
||||||
|
# ist bei den Groessenordnungen hier bezahlbar und sagt mehr aus als ein
|
||||||
|
# Blick auf die ersten Bytes.
|
||||||
|
if ! openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 -pass env:SICHERUNG_PASSWORT -in "$DOKUMENTE_DATEI" 2>/dev/null | tar -tzf - >/dev/null 2>&1; then
|
||||||
|
echo "$(date -Iseconds) FEHLER: Das Dokumentenarchiv laesst sich nicht lesen!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date -Iseconds) Selbstpruefung bestanden - Datenbank und Dokumente."
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# ausdruecklich nach, weil dabei der aktuelle Datenbestand ueberschrieben
|
# ausdruecklich nach, weil dabei der aktuelle Datenbestand ueberschrieben
|
||||||
# wird.
|
# wird.
|
||||||
#
|
#
|
||||||
# ZWEI DINGE, DIE MAN VORHER WISSEN MUSS:
|
# DREI DINGE, DIE MAN VORHER WISSEN MUSS:
|
||||||
#
|
#
|
||||||
# 1. Die Sicherung enthaelt die anwendungsseitig verschluesselten Felder,
|
# 1. Die Sicherung enthaelt die anwendungsseitig verschluesselten Felder,
|
||||||
# aber NICHT den Schluessel. Ohne den passenden ENC_KEY_Vn aus der .env
|
# aber NICHT den Schluessel. Ohne den passenden ENC_KEY_Vn aus der .env
|
||||||
@@ -20,6 +20,14 @@
|
|||||||
# das hier einmal im Jahr auf einer Testdatenbank aus, bevor du es im
|
# das hier einmal im Jahr auf einer Testdatenbank aus, bevor du es im
|
||||||
# Ernstfall zum ersten Mal brauchst. Siehe README, Abschnitt
|
# Ernstfall zum ersten Mal brauchst. Siehe README, Abschnitt
|
||||||
# "Wiederherstellung ueben".
|
# "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
|
set -eu
|
||||||
@@ -87,7 +95,44 @@ openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
|
|||||||
| pg_restore --clean --if-exists --no-owner --no-privileges \
|
| pg_restore --clean --if-exists --no-owner --no-privileges \
|
||||||
--dbname="${PGDATABASE:-postgres}" --exit-on-error
|
--dbname="${PGDATABASE:-postgres}" --exit-on-error
|
||||||
|
|
||||||
echo "$(date -Iseconds) Wiederherstellung abgeschlossen."
|
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 ""
|
||||||
echo "Naechste Schritte:"
|
echo "Naechste Schritte:"
|
||||||
echo " 1. Die SQL-Skripte erneut ausfuehren (Volltextsuche und Audit-Schutz):"
|
echo " 1. Die SQL-Skripte erneut ausfuehren (Volltextsuche und Audit-Schutz):"
|
||||||
@@ -95,6 +140,21 @@ echo " docker compose exec app npm run db:sql"
|
|||||||
echo " 2. Pruefen, ob ENC_KEY_V1 in der .env zu diesem Bestand passt -"
|
echo " 2. Pruefen, ob ENC_KEY_V1 in der .env zu diesem Bestand passt -"
|
||||||
echo " ohne den richtigen Schluessel bleiben die verschluesselten"
|
echo " ohne den richtigen Schluessel bleiben die verschluesselten"
|
||||||
echo " Felder unlesbar."
|
echo " Felder unlesbar."
|
||||||
echo " 3. In der Anwendung eine betreute Person oeffnen und nachsehen, ob"
|
echo " 3. Die Dokumente an ihren Platz bringen. Dazu MinIO anhalten, das"
|
||||||
echo " die Diagnosen im Klartext erscheinen. Erst dann ist die"
|
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."
|
echo " Wiederherstellung wirklich gelungen."
|
||||||
|
|||||||
@@ -132,3 +132,90 @@ describe('Skripte mit pipefail', () => {
|
|||||||
).toEqual([])
|
).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Sicherung muss die Dokumente enthalten - nicht nur die Datenbank.
|
||||||
|
*
|
||||||
|
* Bis v1.11.0 sicherte scripts/sicherung.sh ausschliesslich die Datenbank.
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Aufgefallen ist das nicht durch einen Test, sondern beim Nachsehen, wo
|
||||||
|
* die Daten eigentlich liegen. Die folgenden Pruefungen lesen deshalb den
|
||||||
|
* Skripttext: Sie koennen die Sicherung nicht ausfuehren - dafuer braeuchte
|
||||||
|
* es Postgres, MinIO und einen Behaelter -, aber sie schlagen an, sobald
|
||||||
|
* einer der Bausteine wieder herausfaellt.
|
||||||
|
*/
|
||||||
|
describe('Sicherung umfasst die Dokumente', () => {
|
||||||
|
const sicherung = readFileSync('scripts/sicherung.sh', 'utf8')
|
||||||
|
const wiederherstellung = readFileSync('scripts/wiederherstellung.sh', 'utf8')
|
||||||
|
|
||||||
|
it('packt das Ablageverzeichnis ein', () => {
|
||||||
|
expect(sicherung).toMatch(/tar -czf - -C "\$DOKUMENTE_QUELLE"/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('verschlüsselt das Archiv mit demselben Verfahren wie den Datenbankstand', () => {
|
||||||
|
// Zwei Aufrufe: einer fuer den Dump, einer fuer die Dokumente.
|
||||||
|
const verschluesselt = sicherung.match(/openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt/g)
|
||||||
|
expect(verschluesselt).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('legt zu beiden Teilen eine Prüfsumme', () => {
|
||||||
|
expect(sicherung).toMatch(/sha256sum "\$DATEI"/)
|
||||||
|
expect(sicherung).toMatch(/sha256sum "\$DOKUMENTE_DATEI"/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bricht ab, wenn das Ablageverzeichnis nicht eingehängt ist', () => {
|
||||||
|
// Eine Sicherung, die nur die halbe Wahrheit enthaelt, aber
|
||||||
|
// vollstaendig aussieht, ist schlimmer als gar keine.
|
||||||
|
expect(sicherung).toMatch(/if \[ ! -d "\$DOKUMENTE_QUELLE" \]/)
|
||||||
|
expect(sicherung).toMatch(/rm -f "\$DATEI" "\$DATEI\.sha256"/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prüft am Ende beide Teile, nicht nur den Datenbankstand', () => {
|
||||||
|
expect(sicherung).toMatch(/tar -tzf -/)
|
||||||
|
expect(sicherung).toMatch(/Datenbank und Dokumente/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('räumt alte Dokumentenarchive genauso auf wie alte Datenbankstände', () => {
|
||||||
|
expect(sicherung).toMatch(/dokumente_\*\.tar\.gz\.enc/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('verschont beim Monatsstand beide Teile', () => {
|
||||||
|
expect(sicherung).toMatch(/touch "\$DATEI\.monatsstand" "\$DOKUMENTE_DATEI\.monatsstand"/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('holt die Dokumente beim Zurückspielen mit zurück', () => {
|
||||||
|
expect(wiederherstellung).toMatch(/DOKUMENTE_DATEI=/)
|
||||||
|
expect(wiederherstellung).toMatch(/tar -xzf - -C "\$AUSPACKZIEL"/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sagt beim Zurückspielen deutlich, wenn kein Dokumentenarchiv dabei ist', () => {
|
||||||
|
// Sonst haelt man eine halbe Wiederherstellung fuer eine ganze.
|
||||||
|
expect(wiederherstellung).toMatch(/kein Dokumentenarchiv/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('schreibt die Dokumente nicht selbst an ihren Platz', () => {
|
||||||
|
// Der Sicherungsdienst hat auf das Ablageverzeichnis nur Lesezugriff.
|
||||||
|
// In ein laufendes MinIO hineinzuschreiben wuerde dessen Datenbestand
|
||||||
|
// beschaedigen - der letzte Handgriff bleibt beim Menschen.
|
||||||
|
expect(wiederherstellung).toMatch(/docker compose stop minio app/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Sicherungsdienst darf die Daten, die er sichert, nicht zerstoeren
|
||||||
|
* koennen.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
describe('Sicherungsdienst greift nur lesend auf die Dokumente zu', () => {
|
||||||
|
it('hängt das Ablageverzeichnis mit :ro ein', () => {
|
||||||
|
const compose = readFileSync('docker-compose.yml', 'utf8')
|
||||||
|
expect(compose).toMatch(/minio-daten:\/dokumente:ro/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user