drill: monthly media restore probe, proven against production (#0030)

Counterpart to restore-drill.yaml: that one covers the databases, this the
Synapse media store. Separate job on purpose - the database probe needs a
throwaway Postgres, this one the production PVC read-only, and folding two
different permission and failure pictures into one job makes an emergency
harder to diagnose, not easier.

The name is deliberate. BackupJobFailed already matches restore-drill.*,
so a failure is covered without a new rule and reaches the maintenance
room through the single alertmanager route.

It re-proves its own comparison every run. After the check passes, one
shared file is altered by a byte and the comparison must report it -
otherwise the job fails with "this probe proves nothing". A comparison
that has only ever said "equal" is a guess, and that stays true when it
runs monthly rather than once.

The intersection carries the proof, not the totals: the backup is a
snapshot while production keeps running, and Synapse prunes its own
preview caches. One-sided files are therefore tolerated in url_cache and
url_cache_thumbnails and are an error anywhere else.

The production PVC is mounted read-only and only emptyDir is written. On
today's single node a second read-only mount of the ReadWriteOnce volume
is unproblematic; the comment says what would have to change if a second
node ever appeared, because that failure would alert without anything
being wrong with the data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM
This commit is contained in:
Thore Cimbal
2026-08-21 12:00:00 +00:00
co-authored by Claude Opus 5
parent 73f56c3414
commit 59b7d288dc
2 changed files with 145 additions and 0 deletions
+1
View File
@@ -42,6 +42,7 @@ resources:
- synapse-backup.yaml
# Monatliche Restore-Probe: spielt die Sicherungen isoliert zurueck (#0030)
- restore-drill.yaml
- restore-drill-media.yaml
# Automatisierte TURN-Secret-Rotation (Issue #38)
- turn-secret-rotation-secret.yaml
- turn-secret-rotation.yaml
+144
View File
@@ -0,0 +1,144 @@
# Monatliche Medien-Restore-Probe (#0030). Gegenstueck zu restore-drill.yaml: dort
# Datenbanken, hier der Medienspeicher von Synapse.
#
# Warum eigener Job und nicht angehaengt: Die Datenbank-Probe braucht eine Wegwerf-Postgres,
# diese hier das Produktions-PVC (lesend). Zwei verschiedene Rechte- und Fehlerbilder in
# einem Job zu buendeln macht die Fehlersuche im Ernstfall schwerer, nicht leichter.
#
# Der NAME ist bewusst `restore-drill-media`: Die bestehende Regel BackupJobFailed matcht
# `restore-drill.*`, also ist der Fehlschlag ohne neue Alarmregel abgedeckt und landet
# ueber die einzige Alertmanager-Route im wartung-Raum.
#
# ⚠️ Das Produktions-PVC wird `readOnly: true` gemountet. Geschrieben wird ausschliesslich
# nach emptyDir. Das PVC ist ReadWriteOnce; im heutigen Ein-Knoten-Cluster ist ein zweiter
# lesender Mount unproblematisch. Kaeme je ein zweiter Knoten dazu, koennte der Pod dort
# landen und der Mount scheitern - der Job schluege fehl und alarmierte, ohne dass an den
# Daten etwas waere. Dann gehoert hier eine Node-Affinitaet zu Synapse hin.
apiVersion: batch/v1
kind: CronJob
metadata:
name: restore-drill-media
namespace: matrix
spec:
# 4. des Monats, 05:20 - eine Stunde nach der Datenbank-Probe, damit beide denselben
# frischen Sicherungsstand pruefen, sich aber nicht um Storage Box und CPU streiten.
schedule: "20 5 4 * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
template:
metadata:
labels:
app.kubernetes.io/name: synapse-backup # NetworkPolicy/Egress wie die Backups
app.kubernetes.io/component: restore-drill-media
spec:
restartPolicy: OnFailure
containers:
- name: drill
image: rohana.axion1337.de/sorb/axion-backup:v2
env:
- name: BORG_PASSPHRASE
valueFrom:
secretKeyRef:
name: synapse-backup-credentials
key: borg-passphrase
- name: SSH_PRIVATE_KEY_FILE
value: /secrets/ssh/ssh-private-key
- name: SSH_KNOWN_HOSTS_FILE
value: /secrets/known-hosts/known_hosts
command: ["sh", "-c"]
args:
- |
set -eu
export BORG_RSH="ssh -i $SSH_PRIVATE_KEY_FILE -o UserKnownHostsFile=$SSH_KNOWN_HOSTS_FILE -o StrictHostKeyChecking=yes"
export BORG_REPO='ssh://u641795@u641795.your-storagebox.de:23/./synapse-backup'
cd /scratch
latest=$(borg list --last 1 --format '{archive}' "$BORG_REPO")
echo "[drill] Archiv: $latest"
borg extract "$BORG_REPO::$latest" media/media_store
anz=$(find media/media_store -type f | wc -l)
echo "[drill] zurueckgespielt: ${anz} Dateien, $(du -sk media/media_store | cut -f1) KB"
echo "[drill] Produktion jetzt: $(find /live/media_store -type f | wc -l) Dateien, $(du -sk /live/media_store | cut -f1) KB"
if [ "$anz" -le 0 ]; then
echo "[drill] FEHLER: nichts extrahiert - Sicherung unbrauchbar!"
exit 1
fi
( cd /scratch/media/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/restore.sha
( cd /live/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/live.sha
awk '{print $2}' /scratch/restore.sha | sort > /scratch/r.names
awk '{print $2}' /scratch/live.sha | sort > /scratch/l.names
comm -12 /scratch/r.names /scratch/l.names > /scratch/beide.names
# Die Sicherung ist eine Momentaufnahme, die Produktion laeuft weiter.
# Beweiskraeftig ist deshalb die Schnittmenge - die Gesamtzahlen weichen
# regulaer ab, weil Synapse seine Vorschau-Caches selbst aufraeumt.
abw() { awk 'NR==FNR{h[$2]=$1;next} ($2 in h) && (h[$2]!=$1){print $2}' /scratch/live.sha "$1"; }
n_abw=$(abw /scratch/restore.sha | wc -l)
echo "[drill] gemeinsam: $(wc -l < /scratch/beide.names), davon abweichend: ${n_abw}"
if [ "$n_abw" -ne 0 ]; then
echo "[drill] FEHLER: gemeinsame Dateien sind NICHT byte-gleich:"
abw /scratch/restore.sha | head -10
exit 1
fi
# Nur url_cache und url_cache_thumbnails duerfen einseitig sein - alles
# andere waere ein echter Fehlbetrag und kein Cache-Effekt.
fremd=$(comm -23 /scratch/r.names /scratch/l.names | sed 's|^\./||' | cut -d/ -f1 \
| grep -vE '^url_cache(_thumbnails)?$' | sort -u || true)
if [ -n "$fremd" ]; then
echo "[drill] FEHLER: einseitige Dateien ausserhalb der Vorschau-Caches:"
echo "$fremd"
exit 1
fi
# Positivkontrolle bei JEDEM Lauf: eine gemeinsame Datei um ein Byte
# veraendern und pruefen, dass der Vergleich es merkt. Ein Abgleich, der
# nur je "gleich" gesagt hat, ist eine Vermutung - auch als CronJob.
opfer=$(head -1 /scratch/beide.names)
printf 'x' >> "/scratch/media/media_store/${opfer}"
( cd /scratch/media/media_store && find . -type f -print0 | xargs -0 sha256sum ) | sort -k2 > /scratch/kaputt.sha
if [ "$(abw /scratch/kaputt.sha | wc -l)" -eq 0 ]; then
echo "[drill] FEHLER: der Vergleich erkennt eine absichtliche Aenderung NICHT - diese Probe beweist nichts!"
exit 1
fi
echo "[drill] OK - ${anz} Dateien zurueckgespielt, alle gemeinsamen byte-gleich, Vergleich gegengeprueft"
volumeMounts:
- name: ssh-key
mountPath: /secrets/ssh
readOnly: true
- name: known-hosts
mountPath: /secrets/known-hosts
readOnly: true
- name: scratch
mountPath: /scratch
- name: live
mountPath: /live
readOnly: true
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
volumes:
- name: ssh-key
secret:
secretName: synapse-backup-credentials
defaultMode: 0400
items:
- key: ssh-private-key
path: ssh-private-key
- name: known-hosts
configMap:
name: synapse-backup-known-hosts
- name: scratch
emptyDir:
sizeLimit: 4Gi
- name: live
persistentVolumeClaim:
claimName: matrix-stack-synapse-media
readOnly: true