Backups richtig testen: die Wiederherstellung zählt
Ob ein Backup im Ernstfall hilft, zeigt nur eine Wiederherstellung: Sie spielen die Sicherung in eine getrennte Umgebung zurück und prüfen, ob Daten, Dateien und Anwendung vollständig wieder da sind. Wie lange das dauert, messen Sie dabei mit.
Ein Backup-Protokoll mit grünen Haken belegt, dass ein Programm Dateien geschrieben hat. Ob sich aus diesen Dateien eine laufende Anwendung zurückholen lässt, steht darin nicht. Das IT-Grundschutz-Kompendium des BSI führt deshalb fehlende Wiederherstellungstests als eigene Gefährdung: Wer regelmäßig sichert, aber nie zurückspielt, weiß im Ernstfall nicht, ob die Daten zu retten sind.
Im Notfallplan für die erste Stunde ist das Zurückspielen eines Backups der letzte Ausweg nach einem Ausfall. Hier geht es um die Vorbereitung darauf: was gesichert sein muss, wo die Kopien liegen und wie ein Test abläuft, der mehr beweist als ein Dateidatum.
Ausgangslage
Warum ein Backup ohne Test nichts beweist
Eine Sicherung kann auf viele Arten unbrauchbar sein, ohne dass ein Werkzeug Alarm schlägt. Der Datenbank-Dump bricht nach der Hälfte ab, weil die Platte voll war, und die Datei ist trotzdem da. Die Sicherung läuft weiter gegen eine alte Datenbank, nachdem die Anwendung umgezogen ist. Hochgeladene Dokumente liegen in einem Verzeichnis, das nie in den Sicherungsplan aufgenommen wurde. Oder der Schlüssel für die verschlüsselte Sicherung liegt nur auf dem Server, der gerade ausgefallen ist.
Das BSI verlangt im Baustein CON.3 Datensicherungskonzept als Basis-Anforderung A15, regelmäßig zu testen, ob gesicherte Daten einwandfrei und in angemessener Zeit zurückgespielt werden können. Beide Teile zählen: Eine Sicherung, deren Wiederherstellung zwei Tage dauert, hilft einem Onlineshop am Montagmorgen wenig, auch wenn am Ende alle Daten da sind.
Umfang
Was gesichert werden muss: Datenbank, Dateien, Konfiguration
Eine Webanwendung besteht für die Wiederherstellung aus drei Teilen, und jeder braucht seinen eigenen Weg in die Sicherung:
Datenbank: Kundendaten, Bestellungen, Inhalte, Benutzerkonten. Ein Dump oder eine datenbankeigene Sicherung, erstellt in einem konsistenten Zustand, nicht durch Kopieren der laufenden Datenbankdateien.
Dateien: Uploads, erzeugte PDFs, Rechnungen, Bilder. Sie liegen oft außerhalb des Code-Repositorys und manchmal auf einem eigenen Speicher, der in keiner Server-Sicherung enthalten ist.
Konfiguration: Umgebungsvariablen, Zugangsdaten zu Zahlungsanbieter und Mailversand, Webserver- und Cron-Einstellungen, Zertifikate. Ohne sie startet die wiederhergestellte Anwendung nicht, auch wenn Daten und Dateien vollständig sind.
Der Quellcode selbst liegt meist in einem Repository und lässt sich daraus neu ausliefern. Zur Wiederherstellung gehört dann der Stand, der zum Zeitpunkt der Sicherung live war, damit Datenbankschema und Code zusammenpassen.
Ablage
Die 3-2-1-Regel praktisch umgesetzt
Die US-Behörde CISA fasst die Regel so zusammen: drei Kopien jeder wichtigen Datei, eine davon das Original und zwei Sicherungen, auf zwei verschiedenen Arten von Speicher, eine Kopie außer Haus. Das BSI nennt die Regel in CON.3 nicht beim Namen, verlangt aber in A12, Speichermedien für die Datensicherung räumlich getrennt von den gesicherten Systemen aufzubewahren, und in A14, dass Sicherungen nicht absichtlich oder versehentlich überschrieben werden können.
Für eine Webanwendung bei einem Hoster lässt sich das so übersetzen:
Original: die Daten auf dem Live-Server.
Erste Sicherung: die Sicherung beim Hoster, schnell verfügbar für den Alltag.
Zweite Sicherung: bei einem anderen Anbieter oder an einem anderen Standort, mit eigenen Zugangsdaten, die auf dem Live-Server nicht liegen.
Die zweite Sicherung braucht eigene Zugangsdaten, weil Ransomware nach Laufwerken mit Schreibzugriff sucht, wie das BSI in CON.3 beschreibt. Kann der Server seine eigene Fernsicherung löschen, kann es auch ein Angreifer, der den Server übernommen hat. Die Zugangsdaten für die zweite Sicherung erlauben deshalb nur das Schreiben neuer Dateien, kein Löschen und kein Überschreiben.
Ablauf
Ein Restore-Test in fünf Schritten
Der Test läuft immer in einer getrennten Umgebung, nie auf der Produktion.
Ziel festlegen. Notieren Sie, welche Sicherung Sie testen (Datum und Uhrzeit), wie lange die Wiederherstellung höchstens dauern darf und welcher Datenstand am Ende da sein muss.
Getrennte Umgebung vorbereiten. Ein eigener Server oder eine eigene Datenbank mit anderem Namen. Schalten Sie dort Mailversand, Zahlungsanbindung und geplante Aufgaben ab, bevor Daten einlaufen. Sonst verschickt die wiederhergestellte Kopie Erinnerungsmails an echte Kunden oder bucht Abos ein zweites Mal ab.
Mit Stoppuhr zurückspielen. Datenbank, Dateien und Konfiguration kommen aus der Sicherung, nicht aus der laufenden Anwendung. Gemessen wird bis zu dem Zeitpunkt, an dem sich die Anwendung aufrufen lässt.
Technisch und fachlich prüfen. Technisch: Lässt sich die Datenbank fehlerfrei lesen, stimmen Tabellen, Zeilenzahlen und Inhalte? Fachlich: Melden Sie sich an, öffnen Sie die letzte Bestellung vor dem Sicherungszeitpunkt, laden Sie ein hochgeladenes Dokument herunter und erzeugen Sie eine Rechnung.
Protokollieren und aufräumen. Datum, getestete Sicherung, Dauer, Abweichungen und Maßnahmen gehören ins Protokoll. Danach wird die Testumgebung gelöscht, denn sie enthält dieselben personenbezogenen Daten wie die Produktion.
Für den technischen Teil von Schritt 4 eignet sich ein Skript, das die Wiederherstellung selbst ausführt und mit dem Stand vergleicht, aus dem die Sicherung stammt. Das Beispiel nutzt SQLite, damit es auf jedem Rechner läuft; für MySQL oder PostgreSQL folgt es demselben Ablauf mit deren Kommandozeilenwerkzeugen.
Das Skript bricht mit eigenem Exit-Code ab: 2 Datei fehlt, ist leer oder kein gzip, 3 Dump abgeschnitten oder Restore-Fehler, 4 Integritätsprüfung, 5 Tabellen fehlen, 6 Inhalte weichen ab. Unerwartete Fehler von sqlite3 enden mit 1. Für einen geplanten Lauf mit Alarm zählt deshalb jeder Code außer 0 als Fehlschlag.
#!/usr/bin/env bash
# Restores an SQLite dump into a new file and compares it with the source.
# Usage: verify-restore.sh <dump.sql.gz> <source.sqlite>
set -euo pipefail
dump="${1:?usage: verify-restore.sh <dump.sql.gz> <source.sqlite>}"
source_db="${2:?usage: verify-restore.sh <dump.sql.gz> <source.sqlite>}"
[ -s "$dump" ] || { echo "FAIL: dump missing or empty: $dump" >&2; exit 2; }
[ -s "$source_db" ] || { echo "FAIL: source missing or empty: $source_db" >&2; exit 2; }
gzip -t "$dump" || { echo "FAIL: dump is not a valid gzip file" >&2; exit 2; }
# A dump written by sqlite3 .dump ends with COMMIT; anything else is truncated.
last_line="$(gunzip -c "$dump" | tail -n 1)"
[ "$last_line" = "COMMIT;" ] || { echo "FAIL: dump is truncated (no final COMMIT)" >&2; exit 3; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
restored="$work/restored.sqlite"
start=$(date +%s)
gunzip -c "$dump" | sqlite3 -bail "$restored" \
|| { echo "FAIL: restore aborted with an error" >&2; exit 3; }
echo "restore took $(( $(date +%s) - start )) s"
check="$(sqlite3 "$restored" 'PRAGMA integrity_check;')"
[ "$check" = "ok" ] || { echo "FAIL: integrity_check: $check" >&2; exit 4; }
tables_sql="SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;"
src_tables="$(sqlite3 "$source_db" "$tables_sql")"
dst_tables="$(sqlite3 "$restored" "$tables_sql")"
[ -n "$src_tables" ] || { echo "FAIL: source has no tables" >&2; exit 5; }
[ "$src_tables" = "$dst_tables" ] || { echo "FAIL: table lists differ" >&2; exit 5; }
failed=0
while IFS= read -r table; do
# Sort by every column (1, 2, ... n) so that tables WITHOUT ROWID work too.
ncols="$(sqlite3 "$source_db" "SELECT count(*) FROM pragma_table_info('$table');")"
order="$(seq 1 "$ncols" | paste -sd, -)"
q="SELECT count(*) FROM \"$table\";"
src_count="$(sqlite3 "$source_db" "$q")"
dst_count="$(sqlite3 "$restored" "$q")"
dump_rows="SELECT * FROM \"$table\" ORDER BY $order;"
src_sum="$(sqlite3 -csv "$source_db" "$dump_rows" | shasum -a 256 | cut -d' ' -f1)"
dst_sum="$(sqlite3 -csv "$restored" "$dump_rows" | shasum -a 256 | cut -d' ' -f1)"
if [ "$src_count" = "$dst_count" ] && [ "$src_sum" = "$dst_sum" ]; then
echo "ok $table rows=$dst_count"
else
echo "DIFF $table source=$src_count restored=$dst_count" >&2
failed=1
fi
done <<< "$src_tables"
[ "$failed" -eq 0 ] || { echo "FAIL: restored copy differs from source" >&2; exit 6; }
echo "PASS: restored copy matches source"
Was das Skript prüft und was nicht
Das Skript prüft zuerst, ob die Sicherung existiert, nicht leer ist und als gzip-Datei lesbar ist. Ein Dump von sqlite3 endet mit der Zeile „COMMIT;“. Fehlt sie, ist die Datei abgeschnitten, und das Skript bricht ab, bevor es etwas einspielt. Ohne diese Prüfung verwirft sqlite3 die offene Transaktion still, und der Fehler fiele erst beim Tabellenvergleich auf. Danach spielt das Skript die Sicherung in eine neue Datei ein und bricht beim ersten SQL-Fehler ab. Es folgen die Integritätsprüfung von SQLite, der Vergleich der Tabellenliste und je Tabelle Zeilenzahl und Prüfsumme über alle Inhalte. Eine einzige geänderte Zahl in einer Bestellung reicht für eine Abweichung, auch wenn die Zeilenzahl stimmt.
Verglichen wird mit dem Stand, aus dem die Sicherung erzeugt wurde, etwa einer Kopie, die im selben Lauf entsteht. Gegen die laufende Datenbank meldet das Skript Abweichungen, sobald neue Bestellungen hinzugekommen sind; das ist dann kein Fehler der Sicherung. Sortiert wird nach allen Spalten, nicht nach der internen rowid, damit auch Tabellen mit WITHOUT ROWID geprüft werden. Ob Anmeldung, Dateien und Rechnungen funktionieren, prüft kein Skript; dafür ist der fachliche Teil von Schritt 4 da.
Rhythmus
Wie oft testen und wer dokumentiert
Das BSI schreibt „regelmäßig“ und nennt keine feste Zahl. Wir empfehlen einen vollständigen Test mindestens einmal im Quartal und zusätzlich nach jeder Änderung an Server, Datenbank oder Sicherungsplan, weil gerade solche Änderungen eine Sicherung unbemerkt ins Leere laufen lassen. Das Prüfskript kann öfter laufen, etwa jede Nacht nach der Sicherung.
Das Protokoll führt die Person, die den Test durchführt, und eine zweite Person liest es gegen. Neben Datum und Ergebnis gehört die gemessene Dauer hinein, denn sie zeigt, ob die Wiederherstellung in der Zeit bleibt, die Sie in Schritt 1 festgelegt haben. Wiederherstellungen testen wir in der Wartung von Webanwendungen regelmäßig, dokumentieren die benötigte Zeit je System und prüfen die Datenkonsistenz nach dem Restore. Wer seine Updates schon diszipliniert einspielt, wie in Patch-Disziplin: Warum kleine Updates großen Schaden verhindern beschrieben, hat mit dem Restore-Test den zweiten Teil desselben Betriebs.
Rückfragen zur Sicherung
Häufige Fragen
-
So lange, dass sich auch ein spät bemerkter Fehler zurückholen lässt, und so kurz, wie Löschpflichten es verlangen. Ein Beispiel ist eine Staffel aus Generationen: sieben tägliche, vier wöchentliche und zwölf monatliche Stände; die genaue Staffel hängt davon ab, wie lange Fehler in Ihren Daten unentdeckt bleiben können. Arbeitet ein Dienstleister mit den Sicherungen, gehört die Löschfrist auch in den Auftragsverarbeitungsvertrag.
-
Als einzige Sicherung nicht. Bei Hetzner etwa sind die automatischen Backups eines Cloud-Servers an diesen Server gebunden und werden mit ihm gelöscht, sie liegen am selben Standort und schließen angehängte Volumes nicht ein. Für eine konsistente Datenbank empfiehlt Hetzner, den Server vor der Sicherung auszuschalten; deshalb gehört zusätzlich ein Dump der Datenbank in die Sicherung.
-
Mindestens eine Kopie liegt räumlich getrennt vom Live-System, bei einem anderen Anbieter oder in einem anderen Rechenzentrum, mit eigenen Zugangsdaten. Verschlüsselte Sicherungen brauchen einen Schlüssel, der getrennt von der Sicherung und vom Server aufbewahrt wird. Sonst ist eine vollständige Sicherung im Ernstfall nicht lesbar.
Wann wurde Ihre Sicherung zuletzt zurückgespielt?
Wenn die Antwort ein Datum braucht, das niemand kennt, gehört der Restore-Test in den Wartungsplan. In unserer Wartung sind Backups ein nachvollziehbarer Prozess inklusive Test-Restore.
Nachweise
Quellen und Stand
Stand: 30.07.2026. Das Skript haben wir unter macOS mit bash 3.2 und SQLite 3.51 gegen eine Beispieldatenbank mit vier Tabellen und 6.203 Zeilen ausgeführt, darunter eine Tabelle mit WITHOUT ROWID und eine leere Tabelle. Geprüft wurden außerdem eine leere Datei, eine Datei ohne gzip, ein mitten im Text und ein kurz vor COMMIT abgeschnittener Dump, ein Dump mit SQL-Fehler, eine fehlende Tabelle und je eine geänderte Zeile in einer normalen und in der WITHOUT-ROWID-Tabelle; jeder Fall endete mit dem Code aus der Liste unter dem Skript.
Artikel teilen