#!/bin/sh # ============================================================================= # Taegliche Sicherung der Datenbank # ============================================================================= # # Laeuft im Behaelter "sicherung" per cron um 03:15 Uhr. Von Hand: # # docker compose exec sicherung /bin/sh /sicherung.sh # # Was passiert: # 1. pg_dump im eigenen Format (-Fc), damit einzelne Tabellen # wiederherstellbar bleiben. # 2. Verschluesselung mit AES-256 und PBKDF2 aus SICHERUNG_PASSWORT. # Das Backup liegt oft auf fremdem Speicher - unverschluesselt waere # es ein Datenleck mit Ansage. # 3. SHA-256-Pruefsumme daneben. # 4. Staende, die aelter sind als SICHERUNG_AUFBEWAHRUNG_TAGE, entfernen. # Der jeweils erste Stand eines Monats bleibt stehen. # # WICHTIG: Dieses Backup enthaelt die verschluesselten Felder, aber NICHT # den Schluessel ENC_KEY_V1. Ohne ihn sind Diagnosen, IBANs und Notizen # auch aus einer wiederhergestellten Datenbank nicht lesbar. Den Schluessel # getrennt sichern - Passwortmanager oder ausgedruckt im Ordner. # ============================================================================= set -eu ZIEL="${SICHERUNG_ZIEL:-/backups}" AUFBEWAHRUNG="${SICHERUNG_AUFBEWAHRUNG_TAGE:-30}" ZEITSTEMPEL="$(date +%Y-%m-%d_%H%M)" TAG_IM_MONAT="$(date +%d)" DATEI="$ZIEL/cockpit_${ZEITSTEMPEL}.dump.enc" if [ -z "${SICHERUNG_PASSWORT:-}" ]; then echo "$(date -Iseconds) FEHLER: SICHERUNG_PASSWORT ist nicht gesetzt. Abbruch." >&2 exit 1 fi mkdir -p "$ZIEL" echo "$(date -Iseconds) Sicherung beginnt: $DATEI" # pg_dump schreibt direkt in die Verschluesselung - der Klartext landet # nie auf der Platte. pg_dump --format=custom --compress=6 --no-owner --no-privileges \ | openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt \ -pass env:SICHERUNG_PASSWORT \ -out "$DATEI" GROESSE="$(du -h "$DATEI" | cut -f1)" sha256sum "$DATEI" > "$DATEI.sha256" # Der jeweils erste Stand eines Monats bekommt eine Markierung und bleibt # beim Aufraeumen verschont. if [ "$TAG_IM_MONAT" = "01" ]; then touch "$DATEI.monatsstand" fi echo "$(date -Iseconds) Sicherung fertig ($GROESSE)" # --- Aufraeumen -------------------------------------------------------------- ENTFERNT=0 for alt in "$ZIEL"/cockpit_*.dump.enc; do [ -e "$alt" ] || continue [ -e "$alt.monatsstand" ] && continue if [ -n "$(find "$alt" -mtime "+$AUFBEWAHRUNG" 2>/dev/null)" ]; then rm -f "$alt" "$alt.sha256" ENTFERNT=$((ENTFERNT + 1)) fi done [ "$ENTFERNT" -gt 0 ] && echo "$(date -Iseconds) $ENTFERNT alte Staende entfernt" # --- Selbstpruefung ---------------------------------------------------------- # Ein Backup, das sich nicht entschluesseln laesst, ist kein Backup. # Deshalb wird sofort geprueft, ob die Datei wieder lesbar ist. # Kein "| head -c 5 | grep -q": Beide schliessen die Leitung, sobald sie # genug haben. openssl schreibt dann in ein geschlossenes Rohr, bekommt # SIGPIPE - und mit "set -o pipefail" gilt die ganze Kette als # gescheitert. Bei einer kleinen Datenbank faellt das nicht auf, weil # alles schon im Puffer steht; mit wachsender Datenbank kippt es, und # dann meldet die Selbstpruefung eine INTAKTE Sicherung als kaputt. # # "dd" liest eine feste Menge und beendet sich regulaer. KOPF="$(openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \ -pass env:SICHERUNG_PASSWORT -in "$DATEI" 2>/dev/null \ | dd bs=1 count=5 2>/dev/null || true)" if [ "$KOPF" = "PGDMP" ]; then echo "$(date -Iseconds) Selbstpruefung bestanden." else echo "$(date -Iseconds) FEHLER: Die Sicherung laesst sich nicht entschluesseln!" >&2 exit 1 fi