OYSI DR-Cockpit passive only

Szenario: VPS-Totalausfall

Entscheidungsleitfaden + 8 Phasen aus DR-Runbook. Lokaler Browser-State (kein Server).

Passive only: Befehle werden NICHT vom Cockpit ausgeführt. Kopieren, in einer SSH-Session ausführen, Output prüfen.

Happy-Path TL;DR (Normalfall, ~50 min)

Wenn Bitwarden + GitHub + OVH Managed DBs alle leben → das ist Dein Pfad. Details unten in den Phasen.

  1. Keys holen (Bitwarden → SOPS Key + Backup Key + OVH-S3 rclone) — Phase 2
  2. Neuen VPS aufsetzen (OVH Snapshot ODER bare Hetzner) — Phase 1 + 3
  3. Repo klonen (HTTPS + interaktiver PAT) — Phase 4
  4. Restore: ERPNext (4c), Volumes (4d), wg-easy (4e). OVH Managed PG (4a) braucht nichts wenn Cluster lebt.
  5. Services starten (Phase 5) in Reihenfolge Infra → Zammad → EFSD → Rest
  6. Verifizieren (Phase 6): DNS, Smoke, restore-test.sh

Start hier — 4 Entscheidungen vor allem anderen

  1. 1. Kommst Du in den OVH Manager rein? (manager.ovh.com login, 2FA)
    • ✅ JA → Phase 1 (OVH Snapshot) versuchen.
    • ❌ NEIN → Phase 1 überspringen. OVH Account-Recovery dauert 24–72 h, blockiert RTO. Direkt zu Phase 3 (neuer Provider).
  2. 2. Hast Du Bitwarden-Zugang? (vault.bitwarden.com login)
    • ✅ JA → Recovery möglich.
    • ❌ NEIN → Recovery blockiert. Master-Password aus Memorized-Backup wiederherstellen, sonst sind alle .age-Dateien wertlos.
  3. 3. Sind die OVH Managed DBs erreichbar? ([OVH-CLOUDDB-HOST]:[OVH-CLOUDDB-PORT] ping/telnet — Host + Port aus Bitwarden Item "OVH CloudDB")
    • ✅ JA → Phase 4a überspringen. 11 CloudDBs leben extern. Nicht restaurieren — überschreibt aktive Daten.
    • ❌ NEIN → Phase 4a vollständig durchgehen (OVH-Backup-Restore ODER lokaler Test-Restore + neuer Cluster).
  4. 4. Snapshot-Boot-Timeout: 15 Minuten.
    • Wenn nach 15 min OVH-Snapshot nicht bootet → Plan B starten (Phase 3 bare Hetzner). Nicht warten.
    • Plan A ist schnell aber NIE end-to-end getestet — OVH-Snapshots werden nicht regelmäßig auf Boot validiert.
1 Plan A — OVH VPS-Snapshot zurückspielen (UNGETESTET, 15-min Timeout) ~15 min
⚠ Plan A: schnell, aber UNGETESTET.

OVH-Snapshots werden täglich um 04:43 erzeugt (Retention 7d), aber NIE regelmäßig auf Boot-Fähigkeit getestet (siehe GAP P0-3). Wenn der Snapshot nach 15 min nicht bootet → direkt Plan B (Phase 3). Nicht endlos warten.

  1. OVH Manager öffnen (siehe Direct-Links).
  2. VPS [OVH-VPS-ID] (Instanz-ID aus Bitwarden Item "OVH VPS") → Backups → letzten Snapshot auswählen.
  3. "Restore" anklicken. Wartezeit: 5–10 min für Restore + 2–5 min für Boot.
  4. Stoppuhr 15 min ab Klick. Bootet VPS → Phase 6 (Verifizierung). Bootet nicht → Phase 3 (Plan B).

PITR-Window und Retention sind in OVH Console unter dem CloudDB-Service (id aus Bitwarden Item "OVH CloudDB") — siehe GAP P0-2.

F1 Drill-Fix — OVH Manager-Login auch tot (Account-Lock / 2FA verloren)?

Wenn Du nicht in OVH Manager kommst, spring direkt zu Phase 3 (Plan B = neuer Provider). OVH Account-Recovery dauert 24–72 h Werktage und blockiert RTO komplett.

Für OVH-Account-Recovery brauchst Du (alles in Bitwarden Item "OVH — Account Recovery"):

  • Customer-ID (nic-handle, Format xx12345-ovh)
  • Security-Code (numerisch, bei Account-Erstellung gesetzt)
  • Hinterlegte Telefonnummer + Backup-E-Mail (für 2FA-Reset)
  • Personalausweis-Scan (OVH verlangt ID bei Recovery)

Recovery-URL: https://www.ovh.com/auth/forgottenPassword/ + Support-Ticket öffnen. Parallel den DR-Pfad fortsetzen — nicht auf Recovery warten.

2 Encryption-Keys zurückholen (Voraussetzung für ALLES) ~10 min

Ohne backup.key sind ALLE .age-Dateien wertlos.

Weg A — Bitwarden (bevorzugt):

  1. vault.bitwarden.com öffnen, Master-Password.
  2. Ordner "OYSI — Backups & DR" → Items "SOPS Key" + "Key-Bundle Passphrase" + "OVH-S3 rclone".
  3. Inhalt auf den neuen VPS speichern (siehe Phase 3).

F2: Item-Namen wurden 2026-04-30 verifiziert. Falls anders benannt: in Vault-Search nach "OYSI" + Tag DR filtern.

F15 — R2-Token IP-Filter (Drill-Erkenntnis):

Production-Push-Tokens (R2_DR_COCKPIT_*, R2_CROWN_JEWELS_*) haben IP-Filter auf die alte Production-VPS-IP ([PROD-IP] — siehe Bitwarden Item "OVH VPS"). Beim Recovery hast Du andere IP → Access Denied.

Decision-Tree:

  • Wenn Du Bitwarden + OVH-S3 hast → Weg A unten reicht (R2 nicht zwingend gebraucht für Restore — alle Crown-Jewels sind auch in OVH-S3-Bundle).
  • Wenn Du R2-Crown-Jewels-Pfad doch brauchst → ein Read-only-Token ohne IP-Filter liegt in Bitwarden Item "R2 Crown-Jewels Read-only" (Projekt "Backups & DR"). Enthält ACCESS_KEY_ID + SECRET_ACCESS_KEY + ENDPOINT + BUCKET — read-tested 2026-04-30, write/delete blockiert. Pattern in Phase 4b unten.

Trade-off-Doku: tasks/dr-drill-2026-04-30/r2-token-architecture-fix.md — Option B 2-Token-Pattern ist Empfehlung (RW mit IP-Filter für Push, RO ohne Filter für Recovery).

Weg B — Bundle aus OVH-S3 (Fallback, Bundle wird quartalsweise refreshed via backup-keys.sh):

# rclone-Creds aus Bitwarden Item "OVH-S3 rclone" einlesen
mkdir -p ~/.config/rclone && nano ~/.config/rclone/rclone.conf
# $OVH_S3_BUCKET = OVH-S3 bucket name aus Bitwarden Item "OVH-S3 rclone"
rclone copy ovh-backup:$OVH_S3_BUCKET/keys/keys-bundle.tar.gz.age .

# Passphrase NICHT in History — interaktiv eingeben
age -d keys-bundle.tar.gz.age > keys-bundle.tar.gz   # Passphrase aus Bitwarden, prompt

tar xzf keys-bundle.tar.gz -C /home/ubuntu/infrastructure/keys/
chmod 600 /home/ubuntu/infrastructure/keys/{sops,backup}.key

# Sofort wieder aufräumen
shred -u keys-bundle.tar.gz keys-bundle.tar.gz.age

Endergebnis: /home/ubuntu/infrastructure/keys/{sops.key,backup.key} existieren, Mode 600.

F16 Drill-Fix — Creds vom Mac zum neuen VPS übergeben (sicher):

ssh user@host VAR=x cmd funktioniert NICHT — die Variable wird als Argument an cmd gehängt, nicht als Env exportiert.

Variante A — Heredoc + read -rs (kein Echo, keine Shell-History):

# Auf dem Mac — Var ohne Echo lesen, niemals als CLI-Argument
read -rs -p "ROOT_PWD: " ROOT_PWD; echo
ssh root@new-vps bash <<EOF
export ROOT_PWD="$ROOT_PWD"
docker exec erp-mariadb mariadb -uroot -p"\$ROOT_PWD" -e "SHOW DATABASES;"
EOF
unset ROOT_PWD

Variante B — temporäre .env-Datei mit chmod 600:

# Auf dem Mac
umask 077   # neue Dateien standardmäßig 600
cat > ~/dr-creds.env <<EOF
ROOT_PWD=$(read -rs -p "ROOT_PWD: " p; echo "$p")
EOF
scp ~/dr-creds.env root@new-vps:/root/.dr-creds.env

# Auf dem neuen VPS — sourcen + verbrauchen + sofort löschen
ssh root@new-vps 'set -a; . /root/.dr-creds.env; set +a; \
  echo "Loaded: $(env | grep ROOT_PWD | cut -d= -f1)"; \
  shred -u /root/.dr-creds.env'

# Lokal beim Mac auch verbrennen
shred -u ~/dr-creds.env

NIE Passwörter als CLI-Argument oder in ~/.bash_history. read -rs versteckt Eingabe, shred -u überschreibt + löscht. Heredoc-Quoting: \\$VAR = remote-expandiert, $VAR = lokal-expandiert.

3 Basis-System aufsetzen (Plan B oder fresh nach Plan A) ~15 min
F11/F12 Drill-Fix 2026-04-30: docker-compose-plugin und sops sind NICHT in Ubuntu-Standard-Repos. Befehl unten installiert Docker-Repo + lädt sops-Binary von GitHub. awscli + mariadb-client sind ebenfalls Pflicht (R2-Crown-Jewels resp. ERPNext).
# 1) Docker offizielle Repo (sops und docker-compose-plugin sind nicht in Standard-Ubuntu-apt)
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list

# 2) Update + alle benötigten Pakete (inklusive awscli, mariadb-client die im Original-Wizard fehlten)
sudo apt update
sudo apt install -y \
    docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
    age postgresql-client mariadb-client \
    rclone git curl wget mailutils awscli

# 3) sops binary (nicht in apt verfügbar)
sudo curl -fsSL -o /usr/local/bin/sops "https://github.com/getsops/sops/releases/download/v3.10.2/sops-v3.10.2.linux.amd64"
sudo chmod +x /usr/local/bin/sops

# 4) Verzeichnis-Skeleton (auf user $HOME, nicht hardcoded /home/ubuntu)
sudo systemctl enable docker
sudo usermod -aG docker $USER
mkdir -p $HOME/infrastructure/{keys,secrets,backups,logs,pg-credentials,state/backup}
mkdir -p $HOME/services

Wichtig nach usermod -aG docker: Neuer SSH-Login (Disconnect/Reconnect) ODER newgrp docker in der laufenden Shell — sonst greifen die Docker-Permissions noch nicht und alle docker-Befehle scheitern mit "permission denied".

F14: Wizard nutzt $HOME statt hardcoded /home/ubuntu. Auf Hetzner-default root-Account funktioniert das (root-HOME=/root), auch wenn ubuntu-User später angelegt wird. Service-Pfade ggf. via Symlink anpassen.

Backup-Disk (war /dev/sdb1 ext4 50 GB) bei OVH-Snapshot-Restore vorhanden; bei nacktem VPS via OVH Console → Additional Disk neu zuweisen + formatieren.

4 Repository klonen + Configs zurückholen ~5 min

Configs liegen primär in Git, das Config-Backup ist die Belt-and-Suspenders-Kopie.

F5 Drill-Fix: Repo heißt vps-infrastructure (nicht infrastructure-repo).

⚠ PAT-Sicherheit: PAT NIEMALS in Clone-URL. Landet sonst in ~/.bash_history, ~/.git-credentials, Process-Listings — und stays auch nach Repo-Delete im Filesystem. Stattdessen interaktiv eingeben (Username + PAT als Password) ODER als ephemeral env-var.

Variante A — HTTPS interaktiv (sicher, bevorzugt):

cd $HOME
git clone https://github.com/oysi2025/vps-infrastructure.git infrastructure
# Username: oysi2025
# Password: <PAT aus Bitwarden Item "GitHub PAT — DR" pasten — kein Echo>

# Optional: Credentials nicht persisten lassen (default ist auch off ohne credential helper)
git -C infrastructure config --unset credential.helper 2>/dev/null || true

Variante B — SSH (wenn key-bundle in Phase 2 wiederhergestellt):

cd $HOME && git clone git@github.com:oysi2025/vps-infrastructure.git infrastructure

Variante C — Nur falls Variante A scheitert (z.B. Headless-Script-Modus):

# PAT in env-var, NICHT in URL
read -rs -p "GitHub PAT: " GH_PAT; echo
cd $HOME
git -c credential.helper='!f() { echo "username=oysi2025"; echo "password=$GH_PAT"; }; f' \
    clone https://github.com/oysi2025/vps-infrastructure.git infrastructure
unset GH_PAT

# Cleanup falls trotzdem in remote-URL gelandet
git -C infrastructure remote set-url origin https://github.com/oysi2025/vps-infrastructure.git

Fallback — Wenn Git komplett unerreichbar (GitHub down):

# Config-Tarball aus S3
# $OVH_S3_BUCKET = OVH-S3 bucket name aus Bitwarden Item "OVH-S3 rclone"
LATEST=$(rclone lsf ovh-backup:$OVH_S3_BUCKET/configs/ | sort | tail -1)
rclone copy "ovh-backup:$OVH_S3_BUCKET/configs/$LATEST" .
age -d -i $HOME/infrastructure/keys/backup.key -o configs.tar.gz "$LATEST"
mkdir -p /tmp/restore && tar xzf configs.tar.gz -C /tmp/restore/
4a DB-Restore: OVH Managed PG18 (extern, überlebt VPS-Tod meist) ~5 min Check, ggf. ~30 min Restore

11 CloudDBs leben extern auf dem OVH-Managed-PG-Cluster [OVH-CLOUDDB-HOST]:[OVH-CLOUDDB-PORT] (Host + Port aus Bitwarden Item "OVH CloudDB"). Nach VPS-Tod meist unverändert da.

  1. OVH Manager → Web Cloud → Databases → CloudDB-Service auswählen (Service-id aus Bitwarden Item "OVH CloudDB") → Status prüfen. (F6: nicht Public Cloud → Databases — falscher Pfad.)
  2. Ggf. PITR auf Stand vor VPS-Crash. (Window UNKLAR — GAP P0-2.)
  3. Wenn DBs OK → Phase 4a überspringen, Services connecten direkt nach Service-Start.

4a-A — Cluster lebt, Daten korrupt: OVH-Backup-Restore

F18 Drill-Fix: Wenn der Production-Cluster noch lebt aber Daten korrupt sind, NICHT lokal pg_restore zum Production-Cluster pushen — das würde aktive Daten überschreiben. Stattdessen OVH-internen Restore nutzen.
  1. OVH Manager → Web Cloud → Databases → CloudDB-Service (Service-id aus Bitwarden Item "OVH CloudDB") → Tab Backups.
  2. Snapshot vor Crash-Zeitpunkt auswählen → "Restore from backup". OVH macht das selbst.
  3. Warning: Aktuelle Daten werden überschrieben. Vorher prüfen, ob OVH-Snapshot dasselbe enthält wie unsere backups/clouddb/*.dump.age — sonst ist unser Dump die jüngere Quelle (siehe 4b Test-Restore).
4b DB-Restore: PG Dump-Restore (Cluster tot oder unsere Dumps jünger) ~20 min
F17 Drill-Fix: Ubuntu 22.04 liefert postgresql-client-14 — der kann KEINE PG18-Dumps lesen (Fehler: unsupported version (1.16) in file header). Workaround: pg_restore aus postgres:18-alpine Container, der die richtige Tool-Version mitbringt.

Erst LOKAL testen, dann erst neuen Cluster bestellen. Niemals direkt zum (toten oder lebenden) Production-Cluster pushen.
# WICHTIG: Ubuntu 22.04 pg_restore (v14) kann KEINE PG18-Dumps lesen.
# Workaround: pg_restore aus postgres:18-alpine Container.

# 1) DB-Password ohne Echo lesen (KEIN Echo in History)
read -rs -p "DRILL_PWD (lokales Test-Passwort): " DRILL_PWD; echo

# 2) Dump entschlüsseln (Beispiel finance)
LATEST=$(ls -t $HOME/backups/clouddb/finance_*.dump.age | head -1)
age -d -i $HOME/infrastructure/keys/backup.key -o /tmp/finance.dump "$LATEST"

# 3) Lokalen Test-Postgres starten (NICHT zum Production-Cluster!)
docker run -d --name pg-restore -p 127.0.0.1:5432:5432 \
    -e POSTGRES_PASSWORD="$DRILL_PWD" \
    -e POSTGRES_USER=finance_user \
    -e POSTGRES_DB=finance \
    postgres:18-alpine

# 4) Wait until ready
until docker exec pg-restore pg_isready -U finance_user; do sleep 1; done

# 5) pg_restore via Container (PG18-tools eingebaut)
docker cp /tmp/finance.dump pg-restore:/tmp/
docker exec -e PGPASSWORD="$DRILL_PWD" pg-restore \
    pg_restore -h 127.0.0.1 -U finance_user -d finance \
    --no-owner --no-acl /tmp/finance.dump

# 6) Verify
docker exec -e PGPASSWORD="$DRILL_PWD" pg-restore psql -U finance_user -d finance \
    -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';"

# 7) Cleanup
shred -u /tmp/finance.dump
unset DRILL_PWD

# 8) Wenn Test-Restore OK → neuen OVH-Cluster im OVH Manager bestellen
#    (Web Cloud → Databases → "+ Create"), dann Connection-String in
#    services/<service>/secrets/.env.enc.yaml updaten + sops re-encrypten

F8: Crown-Jewels (R2 EU) als parallele Restore-Quelle prüfen — Cloudflare R2 hält jüngere/komplettere Daten als OVH-S3 (Crown-Jewels-Bucket EU; Bucket-Name aus Bitwarden Item "R2 Crown-Jewels Read-only"). Achtung: F15 — R2-Token hatte IP-Filter auf alte Production-IP, aktuell ungetestet ob aus DR-VPS erreichbar. OVH-S3 Pfad bleibt primary bis Token-Filter entfernt.

F7: Datums-Strings dynamisch via ls -t ... | head -1 auflösen statt hardcoded _20260430_*.

4c DB-Restore: ERPNext MariaDB (im Volume erp-db-data) ~10 min
F20 Drill-Fix: Der ERPNext-Dump enthält kein CREATE DATABASE / USE — direkt einlesen failt mit ERROR 1046: No database selected. DB-Name muss aus Dump-Header oder Dateiname extrahiert + DB manuell angelegt werden.
# 0) Root-Password ohne Echo lesen
read -rs -p "ERPNext ROOT_PWD: " ROOT_PWD; echo

# 1) Neueste ERPNext-Dump dynamisch finden (statt hardcoded Datums-Strings)
LATEST_ERP=$(ls -t $HOME/backups/erpnext/*.sql.gz.age | head -1)
echo "Using: $LATEST_ERP"

# 2) Entschlüsseln + entpacken
age -d -i $HOME/infrastructure/keys/backup.key -o erpnext.sql.gz "$LATEST_ERP"
gunzip -f erpnext.sql.gz
# erpnext.sql liegt jetzt im CWD

# 3) DB-Name extrahieren — aus Dump-Header-Comment "Database: _8dd95132..."
DB_NAME=$(head -10 erpnext.sql | grep -oP 'Database:\s+\K[^ ]+' || basename "$LATEST_ERP" | grep -oP '_[a-f0-9]{16}' | head -1 | tr -d '\n')
[ -z "$DB_NAME" ] && DB_NAME="_8dd95132afac5d61"   # Bekannter Fallback aus Production
echo "Restoring to DB: $DB_NAME"

# 4) MariaDB starten + DB anlegen + Dump einlesen
docker compose -f services/erp/docker-compose.yml up -d mariadb
until docker exec erp-mariadb mariadb -uroot -p"$ROOT_PWD" -e "SELECT 1;" >/dev/null 2>&1; do sleep 1; done

docker exec erp-mariadb mariadb -uroot -p"$ROOT_PWD" -e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\`;"
docker cp erpnext.sql erp-mariadb:/tmp/erpnext.sql
docker exec erp-mariadb sh -c "mariadb -uroot -p\"$ROOT_PWD\" $DB_NAME < /tmp/erpnext.sql"

# 5) Verify
docker exec erp-mariadb mariadb -uroot -p"$ROOT_PWD" "$DB_NAME" \
  -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME';"

# 6) Cleanup
shred -u erpnext.sql 2>/dev/null || rm -f erpnext.sql
unset ROOT_PWD
4d Docker Volumes restaurieren (Coverage 100% verifiziert) ~20 min
✓ Volume-Coverage 100% (verifiziert täglich 03:42):
  • 9 CRITICAL (mit Container-Stop) — DBs, Stateful-Stores
  • 14 STANDARD (live tar) — Configs, Lead-Daten, Monitoring-States
  • 7 EXCLUDED-by-design (regenerable/derived): monitoring_prometheus/loki/alloy, oysi-seo-isr/nextjs-cache, mailbot_redis-data, mailbot_mailbot-secrets (truth-source = SOPS).
  • 9 ANONYMOUS (hex-IDs, compose-managed) — ERPNext queue logs / beszel / goaccess transient state.
  • 0 unbekannte Volumes.

Verification per verify-volume-coverage.sh (cron 03:42). Status flippt automatisch auf WARN sobald ein neues unklassifiziertes Volume auftaucht (z.B. nach docker volume create oder neuer Service).

# Neuesten Volume-Backup-Ordner dynamisch finden (statt hardcoded 20260430_030155)
# $OVH_S3_BUCKET = OVH-S3 bucket name aus Bitwarden Item "OVH-S3 rclone"
LATEST_VOL_DIR=$(rclone lsf ovh-backup:$OVH_S3_BUCKET/volumes/ -d | sort | tail -1 | sed 's|/$||')
echo "Latest volume snapshot: $LATEST_VOL_DIR"

# Pro Volume (Beispiel zammad_zammad-data):
rclone copy "ovh-backup:$OVH_S3_BUCKET/volumes/$LATEST_VOL_DIR/zammad_zammad-data.tar.gz.age" .
age -d -i $HOME/infrastructure/keys/backup.key -o vol.tar.gz zammad_zammad-data.tar.gz.age
docker volume create zammad_zammad-data
docker run --rm -v zammad_zammad-data:/target -v "$PWD":/backup alpine \
  sh -c 'cd /target && tar xzf /backup/vol.tar.gz'

# Volume-Liste der 22 zu restaurierenden:
grep -E '^\s*"' $HOME/infrastructure/scripts/backup-volumes.sh | grep -v '^\s*#' | head -30

F19: Container-Cache-Strategie — beim ersten Pull (postgres:18-alpine, mariadb:11, alpine:latest) braucht Container-Image ~30 s Download je Image bei guter Verbindung, mehr bei langsamem Uplink. Cache reuse beim 2. Restore — docker pull der drei Base-Images früh in Phase 3 ziehen wenn parallele Bandbreite frei ist.

4e DB-Restore: wg-easy SQLite (VPN) ~3 min
# Neuesten wg-easy Backup dynamisch finden (statt hardcoded -20260430.db.gz)
LATEST_WG=$(ls -t $HOME/backups/vpn/wg-easy-*.db.gz.age | head -1)
age -d -i $HOME/infrastructure/keys/backup.key -o wg.db.gz "$LATEST_WG"
gunzip -f wg.db.gz
mkdir -p $HOME/services/vpn/config
sudo cp wg.db $HOME/services/vpn/config/wg-easy.db
docker compose -f services/vpn/docker-compose.yml up -d wg-easy
5 Service-Start in Reihenfolge ~30 min
  1. Infrastructure (Traefik + Monitoring) — infra-compose.service
  2. Zammad (Support kritisch)
  3. EFSD (Garage S3, DGUV, Multi-Tenant Schule)
  4. Finance, Shipping, ERPNext (Geschäftskritisch)
  5. Restliche Services nach Bedarf
F9 Drill-Fix — systemd-Unit nicht vorhanden auf nacktem VPS:

Bei OVH-Snapshot-Restore ist infra-compose.service bereits installiert. Auf nacktem VPS (Plan B / neuer Provider) muss die Unit aus dem Repo installiert werden, BEVOR systemctl start klappt.

# Falls nackter VPS (kein OVH-Snapshot-Restore): systemd-Units aus Repo installieren
sudo cp $HOME/infrastructure/systemd/infra-compose.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable infra-compose.service
# Pruefen welche weiteren Units aus infrastructure/systemd/ installiert werden muessen:
ls $HOME/infrastructure/systemd/*.service
sudo systemctl start infra-compose.service
cd $HOME/services/zammad && ./compose-sops.sh up -d
cd $HOME/services/efsd && ./compose-sops.sh up -d
cd $HOME/services/finance && ./compose-sops.sh up -d
cd $HOME/services/shipping && ./compose-sops.sh up -d
cd $HOME/services/erp && ./compose-sops.sh up -d
F21 Drill-Fix — Traefik client version 1.24 too old:

Wenn Traefik-Logs zeigen client version 1.24 is too old. Minimum supported API version is 1.40: Image-Tag exakt wie Production pinnen — NICHT nur Major-Version (`v3.5` ist gebrochen, `v3.6.12` läuft auf neuestem Docker-CE).

# Production läuft traefik:v3.6.12 — NICHT v3.5 oder :latest verwenden.
# In infrastructure/docker-compose.yml (NICHT services/traefik/, das gibt's nicht):
#   traefik service block → image: traefik:v3.6.12
# Production-Pfad verifizieren:
grep "image: traefik" /home/ubuntu/infrastructure/docker-compose.yml

# Bei jedem Service-Recreate: Traefik neustarten damit Provider-Discovery greift
docker restart traefik

Symptom: Stack lebt, aber Provider-Discovery findet 0 Router (curl localhost:8080/api/http/routers). Endpoint-Flag-Workarounds (--providers.docker.endpoint=unix://...) helfen NICHT — der Bug ist Traefik-internal-API-Version. Drill 2026-04-30 hat das verifiziert.

6 Verifizierung ~15 min
  • DNS: dig vpn.oysi.tech liefert die richtige IP.
  • systemctl status backup-*.timer — alle aktiv.
  • Smoke: curl -I https://traefik-internal/dashboard/
  • Manueller Restore-Test: /home/ubuntu/infrastructure/scripts/restore-test.sh
F22 Drill-Fix — DNS-Verify nicht vom alten Production-VPS aus:

Production-VPS hat iptables FORWARD DROP Policy (seit 2026-04-18) — dig @1.1.1.1 / @8.8.8.8 timeouten. Stattdessen DNS-Checks vom neuen Test-VPS oder vom Mac aus laufen lassen.

# Vom Mac aus: dig vpn.oysi.tech @1.1.1.1 +short # Oder ueber Host-Header gegen neue VPS-IP testen: curl -k -H "Host: vpn.oysi.tech" -I https://NEUE_VPS_IP/
7 Bitwarden re-sync (NUR wenn DR-VPS == neuer Production)
F10 Drill-Warning — NICHT ausführen wenn Production-VPS noch läuft: Bitwarden-Cloud-State würde mit (potentiell unvollständigen) DR-VPS-Daten überschrieben — Datenverlust im Vault. Erst ausführen, wenn klar ist, dass DR-VPS == neuer Production-VPS und alle Services verifiziert (Phase 6 ✓).
$HOME/infrastructure/scripts/bitwarden-sync.sh --dry-run

Erst Dry-Run-Diff prüfen. Dann ohne --dry-run, damit Bitwarden-Cloud-State == Server-State.