Backups, die sich nicht wiederherstellen ließen
Ein Backup-Skript lief jede Nacht fehlerfrei und speicherte leere Dateien, bis zum ersten Wiederherstellungstest. Was fehlte und wie jetzt geprüft wird.
- Firma
- Sprachschule mit Online-Kursbuchung, 14 Beschäftigte
- Team
- Ein interner Entwickler, ein externer Administrator nach Bedarf
- Umgebung
- Ein VPS, Docker Compose: Anwendung, PostgreSQL, Nginx
- Backups
- Nächtlich per cron: pg_dump auf die Serverplatte und eine Kopie nach Amazon S3
- Rahmen
- Eine Wiederherstellung wurde nie probiert, überwacht wurde nur die Erreichbarkeit
01Ausgangslage
Die Datenbank läuft in einem Container mit ihren Daten in einem Docker-Volume. Hochgeladene Dateien (Verträge, Rechnungen, Fotos der Lehrkräfte) liegen in einem Verzeichnis auf dem Server, das in den Anwendungscontainer eingebunden ist. Ein cron-Skript startete jede Nacht pg_dump über docker exec, komprimierte die Ausgabe mit gzip und kopierte die Datei in einen S3-Bucket. Der Schlüssel auf dem Server hatte volle Rechte auf diesen Bucket.
- Internet
- Nginx
- Anwendung (Docker)
- PostgreSQL (Docker-Volume)
- cron: pg_dump | gzip
- Amazon S3: Backups
02Das Problem
Eine Systemprüfung vor dem Ausbau der Buchungen enthielt einen Wiederherstellungstest: das letzte Backup herunterladen und in einen sauberen Container einspielen. Das letzte Backup war 20 Byte groß. Ebenso alle der vorangegangenen sieben Wochen.
Wäre in dieser Zeit eine Platte ausgefallen oder der Server verschlüsselt worden, hätte die Schule sieben Wochen Anmeldungen, Zahlungen und Stundenplanänderungen verloren. Hochgeladene Dateien wären gar nicht wiederherstellbar gewesen, weil sie nie gesichert wurden.
Dazu ein drittes Problem: Der Schlüssel auf dem Server durfte im Bucket auch löschen. Ein Angreifer, der den Server übernimmt, hätte Datenbank und Backups in einem Zug löschen können.
03Technische Ursache
Sieben Wochen zuvor war die Datenbank auf eine neue Hauptversion von PostgreSQL umgezogen, und der Container bekam dabei einen anderen Namen. Seitdem schlug docker exec im Skript fehl, weil es keinen Container mit dem alten Namen mehr gab.
Das Skript merkte es nicht. Ohne set -o pipefail ist der Rückgabewert der Pipeline docker exec ... | gzip > backup.gz der des letzten Befehls, also gzip, und der war erfolgreich: Er komprimierte eine leere Eingabe. Eine leere gzip-Datei ist genau 20 Byte groß.
Die Fehlermeldung ging an die Fehlerausgabe des cron-Jobs. Cron verschickt sie per E-Mail, aber auf dem Server war kein Mailversand eingerichtet, also wurde sie verworfen. Hochgeladene Dateien fehlten von Anfang an; das Skript sicherte nur die Datenbank.
04Untersuchung
Die Dateigrößen im Bucket zeigten den genauen Tag, seit dem die Backups leer sind. Er stimmte mit dem Tag des Versionswechsels in der Historie des Repositorys mit der Docker-Compose-Konfiguration überein.
Ein manueller Lauf des Skripts gab Error response from daemon: No such container aus. Das Systemlog bestätigte, dass cron die Ausgabe verworfen hatte, weil kein Mail-Agent installiert war.
Das letzte nicht leere Backup ließ sich einspielen, und die Daten seitdem lagen noch in der laufenden Datenbank. Verloren ging nichts. Es war ein Risiko, kein Verlust.
05Behebung
Noch am selben Tag: ein manuelles Backup von Datenbank und Dateien und die Prüfung, dass es sich einspielen lässt. Erst danach die Korrekturen.
Das Skript wurde so neu geschrieben, dass ein Fehler in jedem Schritt den ganzen Lauf stoppt, dass es den Docker-Compose-Dienst statt des Containernamens anspricht und seinen Erfolg selbst meldet.
#!/usr/bin/env bash
set -euo pipefail
source /etc/backup.env # HC_UUID and the AWS credentials, mode 600
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
FILE="/var/backups/db/app-$STAMP.dump"
trap 'rm -f "$FILE"' ERR
cd /srv/app
# the service name survives a renamed container; -T: no pseudo-TTY
docker compose exec -T db pg_dump -U app -Fc app > "$FILE"
# an empty or unreadable archive fails here, not on the day of a restore
docker compose exec -T db pg_restore --list < "$FILE" > /dev/null
aws s3 cp "$FILE" "s3://skola-zalohy/db/app-$STAMP.dump"
aws s3 sync /srv/app/uploads "s3://skola-zalohy/uploads/" # no --delete
# only a run that got this far reports in; silence raises the alarm
curl -fsS -m 10 --retry 3 "https://hc-ping.com/$HC_UUID" > /dev/nullpg_dump -Fc erzeugt ein komprimiertes Archiv im eigenen Format, gzip ist unnötig. pg_restore --list prüft direkt nach dem Backup, dass das Archiv lesbar ist. Die Option -T schaltet das Pseudoterminal ab, das docker compose exec standardmäßig zuteilt; bei binärer Ausgabe, die in eine Datei umgeleitet wird, muss es aus sein.
Hochgeladene Dateien werden mit aws s3 sync ohne --delete gesichert, eine auf dem Server gelöschte Datei löscht also nicht ihre Kopie.
Schutz vor Löschung: Der Bucket hat Versionierung und eine Lebenszyklusregel, die alte Versionen nach 35 Tagen entfernt. Der Schlüssel auf dem Server darf nur hochladen (s3:PutObject) und für den Abgleich den Inhalt auflisten (s3:ListBucket). Er hat weder s3:DeleteObject noch s3:DeleteObjectVersion noch das Recht, Versionierung oder Regeln zu ändern.
Die Erfolgsmeldung geht an Healthchecks.io. Kommt sie nicht rechtzeitig, schickt der Dienst eine E-Mail. Die Open-Source-Version auf eigenem Server tut es genauso.
Ein wöchentlicher Wiederherstellungstest: Ein zweites Skript lädt das letzte Backup, spielt es in einen temporären PostgreSQL-Container ein und prüft, dass die neueste Buchung weniger als einen Tag alt ist. Sein Ergebnis meldet es auf dieselbe Weise.
06Warum die Lösung wirkt
set -e beendet das Skript beim ersten Fehler, pipefail lässt auch einen Fehler mitten in einer Pipeline zählen, nicht nur im letzten Befehl, und set -u stoppt bei einem Tippfehler in einem Variablennamen. Die ERR-Falle löscht die halb geschriebene Datei, damit sie nicht als Backup durchgeht.
Der Dienstname db ändert sich nicht, wenn sich der Containername ändert, und genau das war die ursprüngliche Ursache.
Überwacht wird der Erfolg, nicht der Fehler. Der Dienst meldet sich, wenn die Meldung ausbleibt, und deckt damit auch ab, dass das Skript gar nicht läuft, etwa nach dem Löschen des cron-Eintrags.
Mit Versionierung erzeugt das Überschreiben eines Objekts eine neue Version, und die ursprüngliche bleibt. Ein Schlüssel ohne Löschrechte kann weder das Objekt noch seine älteren Versionen entfernen. Ein Angreifer auf dem Server kann die Backups also nicht vernichten.
Was das nicht löst: Ein Backup ist keine Hochverfügbarkeit. Fällt der Server aus, bleibt die Website weg, bis sie anderswo wiederhergestellt ist.
07Überprüfung
- Eine Testkopie des Skripts mit absichtlich falschem Dienstnamen schlug fehl, löschte ihre halb geschriebene Datei, und Healthchecks schickte eine Warnung.
- Eine vollständige Wiederherstellung auf einen sauberen Server nach dem schriftlichen Ablauf, Datenbank und Dateien, mit gemessener Zeit. Diese steht jetzt in der Dokumentation als erwartete Ausfallzeit einer Wiederherstellung.
- Der Versuch, mit dem Serverschlüssel ein Objekt und eine seiner Versionen zu löschen, endete mit
AccessDenied.
08Ergebnisse
Behoben
- Datenbank-Backups haben Inhalt, und ein Fehler fällt innerhalb eines Tages auf.
- Hochgeladene Dateien werden gesichert.
Risiko gesenkt
- Ein kompromittierter Server bedeutet nicht mehr den Verlust der Backups.
- Die Wiederherstellung ist erprobt und dokumentiert, nicht nur angenommen.
Weiter offen
- Mit einem nächtlichen Backup kann die Schule bis zu einen Tag Daten verlieren.
- Es gibt weiterhin nur einen Server. Fällt er aus, ist die Website bis zur Wiederherstellung weg.
09Grenzen
pg_restore --listprüft nur den Kopf und die Objektliste des Archivs, nicht alle Daten. Beschädigte Daten findet erst der wöchentliche Wiederherstellungstest.- Der automatische Test deckt die Datenbank ab. Die Wiederherstellung der Dateien wird einmal im Quartal von Hand geprobt.
- Die Backups enthalten personenbezogene Daten. Zugriff auf den Bucket und Aufbewahrungsdauer müssen dem entsprechen, was die Schule in ihrer Datenschutzerklärung angibt.
- Alte Versionen werden nach 35 Tagen entfernt. Einen Datenfehler, den länger niemand bemerkt, holen die Backups nicht zurück.
10Erkenntnisse
- Ein Backup, das nie eingespielt wurde, ist nur eine Annahme.
- Ein Shell-Skript ohne
set -euo pipefailkann bei einem Fehler Erfolg melden. - Überwachen Sie den Erfolg, nicht nur Fehler. Stille ist keine gute Nachricht.
- Ein Server darf seine eigenen Backups nicht löschen können.
- Sichern Sie alles, was sich nicht neu erzeugen lässt, nicht nur die Datenbank.
11Nächste Schritte für eine kleine Firma
- 1.Den Wiederherstellungsablauf so aufschreiben, dass ihn auch jemand schafft, der den Server nicht kennt.
- 2.Ein Datenbank-Backup auch tagsüber ergänzen, wenn der Verlust eines Tages nicht tragbar ist.
- 3.Zwei-Faktor-Authentifizierung für die AWS-Konsole einschalten und die Backups aus einem anderen Konto als die Produktion verwalten.
- 4.Bei wachsender Zahl von Anwendungen eine verwaltete Datenbank mit automatischen Backups erwägen.
Erkennen Sie Ihre eigene Firma darin?
Schreiben Sie uns, worum es geht. Wir antworten innerhalb eines Werktags und sagen Ihnen, ob es Arbeit für uns ist, auch wenn die Antwort nein lautet.