From 59b7d288dc2c710d7130b0244fbf136d692b7c10 Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Fri, 21 Aug 2026 12:00:00 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM --- apps/production/kustomization.yaml | 1 + apps/production/restore-drill-media.yaml | 144 +++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 apps/production/restore-drill-media.yaml diff --git a/apps/production/kustomization.yaml b/apps/production/kustomization.yaml index 364d9e3..7a69624 100644 --- a/apps/production/kustomization.yaml +++ b/apps/production/kustomization.yaml @@ -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 diff --git a/apps/production/restore-drill-media.yaml b/apps/production/restore-drill-media.yaml new file mode 100644 index 0000000..66f1e04 --- /dev/null +++ b/apps/production/restore-drill-media.yaml @@ -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