Beim Sprung von 1.5.0 auf 1.10.1 endete scripts/update.sh mitten in der Ausgabe. Kein Fehler, keine Zeile, einfach zurueck zur Eingabeaufforderung. Ursache war eine abgeschnittene Leitung: git log --oneline "HEAD..$ZIEL" | head -15 | sed ... head schliesst die Leitung nach 15 Zeilen, git log schreibt weiter und bekommt SIGPIPE (141). Mit "set -o pipefail" gilt die ganze Kette als gescheitert, "set -e" beendet das Skript - stumm. Zeitabhaengig, deshalb monatelang unauffaellig: Solange git log fertig wurde, bevor head zumachte, ging es gut. Die Schwelle liegt exakt bei 15 Commits, und v1.5.0..v1.10.0 waren die ersten 16. Behoben, indem der Erzeuger selbst begrenzt (git log -15) statt die Leitung zu kappen. Dieselbe Behandlung fuer die beiden Tag-Listen. Dieselbe Falle steckte in der Selbstpruefung von sicherung.sh: openssl enc -d ... | head -c 5 | grep -q "PGDMP" Beide Leser schliessen frueh. Bei kleiner Datenbank steht alles im Puffer und es faellt nicht auf; mit wachsender Datenbank kippt es - und dann meldet das Skript eine INTAKTE Sicherung als kaputt und verweigert das Update. Jetzt liest dd eine feste Menge und endet regulaer. Damit so etwas nicht wieder stumm passiert: - Eine ERR-Falle nennt Zeilennummer und Exitcode und erklaert bei 141, dass es SIGPIPE ist und ein Fehler im Skript, nicht beim Benutzer. - Ein Test verbietet "| head" in Skripten mit pipefail und nennt die Zeile. Gegenprobe gemacht: Er schlaegt an. - Bei mehr als 15 Commits steht "... und N weitere" darunter, statt sie wortlos wegzulassen. Nachgestellt: der Ablauf von 1.5.0 auf 1.10.1 laeuft durch.
96 lines
3.6 KiB
Bash
Executable File
96 lines
3.6 KiB
Bash
Executable File
#!/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
|