Zum Inhalt

Runbook: Backup-Umbau (E1)

Schritt-für-Schritt-Anleitung für Robin. Sie setzt die Aufgaben des Umsetzungsplans um, die Robins Hand brauchen. Claude schreibt jeden Abschnitt, bevor er dran ist, und prüft danach lesend. Claude führt hier nichts selbst aus.

Jeder Schritt hat einen Befehl, eine Erwartung und ein Abbruch-Kriterium. Nach jedem Block (◆) meldest du Claude die Ausgabe, bevor es weitergeht. Bei „Abbruch“: nicht weitermachen, Ausgabe melden.

Vorgaben für alle Abschnitte

  • Zeitfenster: Kein Eingriff zwischen 01:45 und 03:15 (Nachtläufe NUC und VPS) und zwischen 19:45 und 20:45 (aiserver). Vor jedem Eingriff:
systemctl is-active borgmatic.service   # erwartet: inactive
  • Rollen: ssh nuc = robin (lesen), ssh nuc-root = root. Auf dem aiserver ssh aiserver und dann sudo. Den VPS erreichst du mit ssh -i ~/.ssh/homelab-external root@<vps>.
  • Secrets:
  • nie in den Chat und nie anzeigen lassen
  • Passphrasen und Tokens als Datei unter /etc/borgmatic.d/credentials/ (Datei 0600, Verzeichnis 0700, root)
  • Quelle der Wahrheit: Infisical /borgmatic (EU), Notfallkopie in Proton Pass
  • geprüft wird nur per Hash-Präfix:
sha256sum < /etc/borgmatic.d/credentials/<datei> | cut -c1-12
  • Configs kommen aus dem Git-Checkout /opt/rpi-docker-compose/backup/borgmatic/:
install -o root -g root -m 0600 /opt/rpi-docker-compose/backup/borgmatic/<datei>.yaml /etc/borgmatic.d/
borgmatic config validate

Task 1 · NUC-Configs aus dem Git (ohne Verhaltensänderung)

Was passiert: - Die beiden Live-Configs werden durch die Fassung aus homeserver/backup/borgmatic/ ersetzt. - Die Secrets ziehen in Dateien unter credentials/. - Der Uptime-Kuma-Push fällt weg. - Sonst ändert sich nichts. Die Gegenprobe vom 30.10. zeigt uptime_kuma als einzigen Unterschied.

Voraussetzungen: - Der homeserver-PR mit backup/borgmatic/ ist gemergt. - Es läuft kein Backup, und das Zeitfenster passt. - Dauer: 10 Minuten.

1.1 · Checkout aktualisieren und Ist-Stand sichern ◆

git -C /opt/rpi-docker-compose pull
ls /opt/rpi-docker-compose/backup/borgmatic/
cd /etc/borgmatic.d && cp -a local.yaml local.yaml.bak-20261030 && cp -a hetzner.yaml hetzner.yaml.bak-20261030
  • Erwartung: README.md hetzner.yaml local.yaml, die Kopien liegen in /etc/borgmatic.d/.
  • Abbruch: Das Verzeichnis fehlt, dann ist der PR noch nicht auf main.

1.2 · Secrets aus den Live-Configs in Dateien ziehen ◆

Das Skript liest die Werte per YAML-Parser und schreibt sie ohne Anzeige. Ausgegeben werden nur Dateinamen und Größen.

( umask 077; /root/.local/pipx/venvs/borgmatic/bin/python - <<'PY'
from pathlib import Path
from ruamel.yaml import YAML
d = Path("/etc/borgmatic.d")
c = d / "credentials"
y = YAML()
local = YAML(typ="safe").load(open(d / "local.yaml"))
hetzner = YAML(typ="safe").load(open(d / "hetzner.yaml"))
assert local["ntfy"]["access_token"] == hetzner["ntfy"]["access_token"], "ntfy tokens differ"
(c / "ntfy.token").write_text(local["ntfy"]["access_token"])
for name, conf in (("local", local), ("hetzner", hetzner)):
    with open(c / f"healthchecks-nuc-{name}.yaml", "w") as f:
        y.dump(dict(conf["healthchecks"]), f)
with open(c / "repositories-hetzner.yaml", "w") as f:
    y.dump([dict(r) for r in hetzner["repositories"]], f)
for p in sorted(c.iterdir()):
    print(p.name, p.stat().st_size)
PY
)
ls -l /etc/borgmatic.d/credentials
  • Erwartung: sechs Dateien, alle -rw------- root root. Neu sind ntfy.token, healthchecks-nuc-local.yaml, healthchecks-nuc-hetzner.yaml und repositories-hetzner.yaml, von früher stammen hetzner.pass und local.pass.
  • Abbruch: ntfy tokens differ oder eine Python-Fehlermeldung. Dann ist nichts überschrieben, die Configs gelten unverändert.

1.3 · Neue Configs installieren ◆

install -o root -g root -m 0600 /opt/rpi-docker-compose/backup/borgmatic/local.yaml /etc/borgmatic.d/
install -o root -g root -m 0600 /opt/rpi-docker-compose/backup/borgmatic/hetzner.yaml /etc/borgmatic.d/
borgmatic config validate
grep -c "uptime_kuma\|https://hc\." /etc/borgmatic.d/local.yaml /etc/borgmatic.d/hetzner.yaml
  • Erwartung: All configuration files are valid, beide grep-Zähler 0.
  • Abbruch: validate meldet einen Fehler. Dann die alten Configs zurückholen und melden:
cp -a local.yaml.bak-20261030 local.yaml; cp -a hetzner.yaml.bak-20261030 hetzner.yaml

1.4 · Probe ◆

borgmatic --config /etc/borgmatic.d/local.yaml repo-list | tail -n 2
borgmatic --config /etc/borgmatic.d/hetzner.yaml repo-list | tail -n 2
  • Erwartung: Beide listen Archive bis zur letzten Nacht. repo-list löst keine Monitoring-Hooks aus.
  • Abbruch: Fehler bei Passphrase oder Verbindung. Zurückholen wie in 1.3 und melden.

1.5 · Uptime Kuma ◆

In Uptime Kuma die beiden Push-Monitore für das NUC-Backup pausieren. Ab heute Nacht kommen dort keine Pushes mehr an, und ohne Pause schlagen sie Alarm. Gelöscht werden sie erst nach dem ersten sauberen Morgen (1.6).

1.6 · Am nächsten Morgen (Claude prüft lesend)

Claude prüft: - beide Configs Successfully ran - kein Pushing Uptime Kuma im Journal - beide Healthchecks-Checks up

Danach löschst du die Kopien, weil sie Klartext-Tokens enthalten, und anschließend die pausierten Kuma-Monitore:

shred -u /etc/borgmatic.d/local.yaml.bak-20261030 /etc/borgmatic.d/hetzner.yaml.bak-20261030

Task 2 · Storage Box: Sub-Accounts, Snapshots, Zugänge

Was passiert: - Jeder Host bekommt einen eigenen Sub-Account mit eigenem Key und sieht nur noch sein Verzeichnis. - Die bestehenden Repos ziehen eine Ebene tiefer nach backups/<host>/repo, denn im Home eines Sub-Accounts liegt dessen .ssh/. - Die Box legt täglich Snapshots an, die kein Host löschen kann. - Die alten Keys mit Vollzugriff verschwinden, und das Hauptpasswort ist danach neu.

Voraussetzungen: - Task 1 ist durch und hatte einen sauberen Morgen. - homeserver-PR „storage box sub-account“ ist gemergt. - Alles in einer Sitzung zwischen 10:00 und 18:00. Nach dem Umzug in 2.4 laufen die Backups ins Leere, bis 2.7 bzw. 2.8 erledigt sind. - Dauer: ca. 1 h.

Platzhalter:

Platzhalter Bedeutung Beispiel
<MAIN> Hauptaccount u123456
<SUB_NUC>, <SUB_AI>, <SUB_VPS>, <SUB_RESTORE> Benutzernamen der Sub-Accounts, die die Console beim Anlegen vergibt u123456-sub1

Der Host ist immer <benutzer>.your-storagebox.de. Die Namen sind keine Secrets, kommen aber nicht ins Git. Halte sie in Infisical /borgmatic fest, als STORAGEBOX_MAIN_USER und STORAGEBOX_SUB_<HOST>_USER.

Teil A · Hetzner Console und Laptop

2.1 · Sicherheitsnetz ◆

Hetzner Console → Storage Box → Snapshots → Create snapshot, Beschreibung vor-e1-subaccounts.

Erwartung: Der Snapshot steht in der Liste.

2.2 · Snapshot-Plan ◆

Snapshots → Automatic snapshots aktivieren: täglich, 03:30 UTC, maximal 10.

Erwartung: Der Plan ist aktiv. Der erste automatische Snapshot erscheint morgen früh.

2.3 · Hauptpasswort neu, Admin-Key auf dem Laptop ◆

  1. Console → Reset password. Das Passwort kommt nach Infisical /borgmatic als STORAGEBOX_MAIN_PASSWORD und nach Proton Pass. Es kommt auf keinen Host.
  2. Auf dem Laptop:
MAIN=<MAIN>
ssh-keygen -t ed25519 -N '' -f ~/.ssh/storagebox-admin -C storagebox-admin
cat ~/.ssh/storagebox-admin.pub | ssh -p23 $MAIN@$MAIN.your-storagebox.de install-ssh-key
ssh -p23 -i ~/.ssh/storagebox-admin -o IdentitiesOnly=yes $MAIN@$MAIN.your-storagebox.de ls backups

install-ssh-key fragt einmal nach dem neuen Passwort.

Erwartung: aiserver nuc.

2.4 · Repos eine Ebene tiefer legen ◆

Ab hier laufen die Backups ins Leere, bis die Configs umgestellt sind. Die Box-Shell kennt keine Ketten mit ; oder &&, deshalb ist jeder Befehl ein eigener Aufruf.

B="ssh -p23 -i $HOME/.ssh/storagebox-admin -o IdentitiesOnly=yes $MAIN@$MAIN.your-storagebox.de"
$B mv backups/nuc backups/nuc-repo
$B mkdir backups/nuc
$B mv backups/nuc-repo backups/nuc/repo
$B mv backups/aiserver backups/aiserver-repo
$B mkdir backups/aiserver
$B mv backups/aiserver-repo backups/aiserver/repo
$B mkdir backups/vps
$B tree -L 2 backups

Erwartung: backups/nuc/repo, backups/aiserver/repo, backups/vps. Unter repo liegen jeweils config, data/, index.* usw.

Abbruch: Ein mv meldet einen Fehler. Dann nicht weitermachen und melden. Rückweg ist dieselbe Kette rückwärts.

2.5 · Sub-Accounts anlegen ◆

Console → Storage Box → Sub-accounts → Create, viermal:

Home-Verzeichnis SSH External reachability Samba WebDAV Read-only
backups/nuc an an aus aus aus
backups/aiserver an an aus aus aus
backups/vps an an aus aus aus
backups an an aus aus erst aus (wird in 2.6 umgestellt)

Jedes Passwort kommt nach Infisical als STORAGEBOX_SUB_<HOST>_PASSWORD. Für den Sub-Account restore heißt es STORAGEBOX_RESTORE_PASSWORD und kommt zusätzlich in Proton Pass, später auch in den Umschlag. Die Passwörter braucht man nur einmal für install-ssh-key. Abschalten lässt sich der Passwort-Login nicht.

Erwartung: Vier Sub-Accounts, die Benutzernamen stehen in Infisical.

2.6 · Restore-Key auf dem Laptop ◆

RST=<SUB_RESTORE>
ssh-keygen -t ed25519 -N '' -f ~/.ssh/storagebox-restore -C storagebox-restore
cat ~/.ssh/storagebox-restore.pub | ssh -p23 $RST@$RST.your-storagebox.de install-ssh-key
ssh -p23 -i ~/.ssh/storagebox-restore -o IdentitiesOnly=yes $RST@$RST.your-storagebox.de ls

Danach in der Console den Sub-Account backups auf Read-only stellen. Den privaten Key ~/.ssh/storagebox-restore legst du in Proton Pass ab.

Erwartung: ls zeigt aiserver nuc vps.

Teil B · NUC (ssh nuc-root)

2.7 · borg 1.4.5, Key, Config ◆

systemctl is-active borgmatic.service      # erwartet: inactive
curl -fL -o /tmp/borg-1.4.5 https://github.com/borgbackup/borg/releases/download/1.4.5/borg-linux-glibc231-x86_64
echo "c8457f70660064d0f45b38283ab4cc65b342970012013201d3aec713a75898fb  /tmp/borg-1.4.5" | sha256sum -c
cp -a /usr/local/bin/borg /usr/local/bin/borg-1.4.0
install -m 0755 /tmp/borg-1.4.5 /usr/local/bin/borg && rm /tmp/borg-1.4.5
borg --version

Erwartung: /tmp/borg-1.4.5: OK, dann borg 1.4.5. Abbruch: Die Prüfsumme stimmt nicht. Dann die Datei löschen und melden.

SUB=<SUB_NUC>
ssh-keygen -t ed25519 -N '' -f /root/.ssh/storagebox-nuc -C nuc-storagebox
ssh-keyscan -p 23 -t ed25519 $SUB.your-storagebox.de 2>/dev/null | ssh-keygen -lf -
ssh-keygen -lF "[$(grep -oE 'ssh://[^@]+@[^:]+' /etc/borgmatic.d/credentials/repositories-hetzner.yaml | cut -d@ -f2)]:23" -f /root/.ssh/known_hosts

Erwartung: Beide Fingerprints sind gleich. Alle Hetzner-Storage-Boxen nutzen denselben Host-Key. Abbruch: Die Fingerprints sind verschieden.

cat /root/.ssh/storagebox-nuc.pub | ssh -p23 -o StrictHostKeyChecking=accept-new $SUB@$SUB.your-storagebox.de install-ssh-key
ssh -p23 -i /root/.ssh/storagebox-nuc -o IdentitiesOnly=yes $SUB@$SUB.your-storagebox.de ls

install-ssh-key fragt nach STORAGEBOX_SUB_NUC_PASSWORD.

Erwartung: repo.

Repo-Pfad in der Include-Datei umstellen und die Config aus dem Git installieren:

( umask 077; /root/.local/pipx/venvs/borgmatic/bin/python - "$SUB" <<'PY'
import sys
from ruamel.yaml import YAML
sub = sys.argv[1]
path = "/etc/borgmatic.d/credentials/repositories-hetzner.yaml"
with open(path, "w") as f:
    YAML().dump([{"path": f"ssh://{sub}@{sub}.your-storagebox.de:23/./repo", "label": "hetzner-offsite"}], f)
PY
)
git -C /opt/rpi-docker-compose pull
install -o root -g root -m 0600 /opt/rpi-docker-compose/backup/borgmatic/hetzner.yaml /etc/borgmatic.d/
borgmatic config validate
borgmatic --config /etc/borgmatic.d/hetzner.yaml repo-list | tail -n 2

Erwartung: All configuration files are valid, dann die Archive bis letzte Nacht. Es kommt keine Rückfrage, ob der Zugriff auf das verschobene Repo in Ordnung ist. Abbruch: Repository does not exist oder eine Passphrase-Frage. Dann melden, die Pfade aus 2.4 prüfen.

Erster Lauf von Hand. Er zählt wie ein normaler Nachtlauf, mit Dumps und Healthchecks-Ping:

borgmatic --config /etc/borgmatic.d/hetzner.yaml create --stats 2>&1 | tail -n 15

Erwartung: Archivstatistik, kein Fehler, Dauer wenige Minuten.

Teil C · aiserver (ssh aiserver, dann sudo -i)

2.8 · Key und Config ◆

Der aiserver nutzt ein globales ssh_command mit dem Key, den auch das NUC-Repo braucht. Darum bekommt der neue Box-Key einen eigenen Host-Block, und ssh_command bleibt, wie es ist.

systemctl is-active borgmatic.service      # erwartet: inactive
SUB=<SUB_AI>
ssh-keygen -t ed25519 -N '' -f /root/.ssh/storagebox-aiserver -C aiserver-storagebox
cat >> /root/.ssh/config <<EOC

Host $SUB.your-storagebox.de
    Port 23
    IdentityFile /root/.ssh/storagebox-aiserver
EOC
chmod 600 /root/.ssh/config
cat /root/.ssh/storagebox-aiserver.pub | ssh -o StrictHostKeyChecking=accept-new $SUB@$SUB.your-storagebox.de install-ssh-key
ssh -i /root/.ssh/storagebox-aiserver -o IdentitiesOnly=yes $SUB@$SUB.your-storagebox.de ls

Erwartung: repo. Vorher den Fingerprint vergleichen: ssh-keyscan -p 23 -t ed25519 $SUB.your-storagebox.de 2>/dev/null | ssh-keygen -lf - gegen ssh-keygen -lF "[<MAIN>.your-storagebox.de]:23" -f /root/.ssh/known_hosts. Beide müssen gleich sein.

Config anpassen. Das Skript ändert nur den Pfad des Repos mit dem Label storagebox und setzt remote_path und relocated_repo_access_is_ok. Kommentare bleiben erhalten. remote_path gilt auch für das NUC-Repo, schadet dort aber nicht, weil der NUC per Forced Command immer borg serve startet.

cp -a /etc/borgmatic.d/aiserver.yaml /etc/borgmatic.d/aiserver.yaml.bak-e1
/usr/bin/python3 - "$SUB" <<'PY'
import sys
from ruamel.yaml import YAML
sub = sys.argv[1]
path = "/etc/borgmatic.d/aiserver.yaml"
y = YAML()
y.preserve_quotes = True
c = y.load(open(path))
hits = [r for r in c["repositories"] if r.get("label") == "storagebox"]
assert len(hits) == 1, f"expected one storagebox repo, found {len(hits)}"
hits[0]["path"] = f"ssh://{sub}@{sub}.your-storagebox.de:23/./repo"
c["remote_path"] = "borg-1.4"
c["relocated_repo_access_is_ok"] = True
y.dump(c, open(path, "w"))
print("ok")
PY
borgmatic config validate
borgmatic --config /etc/borgmatic.d/aiserver.yaml repo-list --repository storagebox | tail -n 2

Erwartung: ok, valid, die Archive. Abbruch: - ModuleNotFoundError: ruamel: Dann apt install python3-ruamel.yaml oder den Python-Interpreter aus borgmatic nehmen (head -1 $(command -v borgmatic)). - expected one storagebox repo: Die Config ist unverändert. Melden.

Teil D · Alte Zugänge schließen, Lese-Token

2.9 · Hauptaccount nur noch mit Admin-Key ◆

Erst wenn 2.7 und 2.8 grün sind. Vom Laptop aus:

scp -P 23 -i ~/.ssh/storagebox-admin -o IdentitiesOnly=yes ~/.ssh/storagebox-admin.pub $MAIN@$MAIN.your-storagebox.de:.ssh/authorized_keys
$B cat .ssh/authorized_keys

Erwartung: genau eine Zeile ssh-ed25519 … storagebox-admin. Damit sind die drei alten Keys entfernt, auch die Fassungen für Port 22.

Gegenprobe auf dem NUC und dem aiserver mit den alten Keys:

ssh -p23 -i /root/.ssh/hetzner-borg -o IdentitiesOnly=yes -o BatchMode=yes <MAIN>@<MAIN>.your-storagebox.de ls     # NUC
ssh -p23 -i /root/.ssh/borg_aiserver -o IdentitiesOnly=yes -o BatchMode=yes <MAIN>@<MAIN>.your-storagebox.de ls    # aiserver

Erwartung: beide Permission denied.

2.10 · Lese-Token für den Füllstand ◆

Console → Projekt der Storage Box → Security → API tokens → Generate, Name nuc-backup-status, Berechtigung Read. Das Token kommt nach Infisical als HETZNER_API_TOKEN_RO. Auf dem NUC:

( umask 077; read -rs -p "Token: " T; echo; printf '%s' "$T" > /etc/borgmatic.d/credentials/hetzner-api-ro.token; unset T )
curl -sf -H "Authorization: Bearer $(cat /etc/borgmatic.d/credentials/hetzner-api-ro.token)" https://api.hetzner.com/v1/storage_boxes | jq '.storage_boxes | length'

Erwartung: 1.

2.11 · Am nächsten Morgen (Claude prüft lesend)

  • Nachtläufe: NUC beide Configs und aiserver Successfully, keine Rückfrage zum Ortswechsel, kein Lock-Fehler.
  • Snapshot: Der automatische Snapshot von 03:30 UTC existiert, Prüfung über das Lese-Token.
  • Abgrenzung (Ergebnis ins Log):
  • vom aiserver der NUC-Sub-Account: ssh -i /root/.ssh/storagebox-aiserver -o IdentitiesOnly=yes -o BatchMode=yes <SUB_NUC>@<SUB_NUC>.your-storagebox.de ls → Permission denied
  • vom Laptop mit restore schreiben: ssh -p23 -i ~/.ssh/storagebox-restore -o IdentitiesOnly=yes <SUB_RESTORE>@<SUB_RESTORE>.your-storagebox.de touch probe → abgelehnt (read-only)
  • Aufräumen: Danach auf dem NUC rm /usr/local/bin/borg-1.4.0 /root/.ssh/hetzner-borg /root/.ssh/hetzner-borg.pub, auf dem aiserver rm /etc/borgmatic.d/aiserver.yaml.bak-e1. borg_aiserver auf dem aiserver bleibt, das NUC-Repo braucht ihn bis Task 4c.

Task 3 · rclone-Mount /mnt/storagebox entfernen

Was passiert: - Der rclone-Mount fällt weg und damit der Schreibzugriff von robin auf die ganze Box. - Docker hängt danach nicht mehr am Mount. - Den Füllstand liefert ab Task 7 das Lese-Token. - Docker wird nicht neu gestartet.

Voraussetzungen: - Task 2 ist durch. - Am 02.11. lesend geprüft: Den Mount nutzt nur der Kernel (fuser -vm), es gibt keinen Cron-Eintrag, und kein Git-Repo zeigt auf /mnt/storagebox, abgesehen von Doku. - Dauer: 5 Minuten.

3.1 · Mount abschalten, Docker-Drop-in anpassen ◆

fuser -vm /mnt/storagebox                      # erwartet: nur "kernel mount"
systemctl disable --now mnt-storagebox.mount
sed -i 's# /mnt/storagebox##' /etc/systemd/system/docker.service.d/10-wait-for-mounts.conf
cat /etc/systemd/system/docker.service.d/10-wait-for-mounts.conf
systemctl daemon-reload
systemctl show docker -p RequiresMountsFor
findmnt /mnt/storagebox; echo "findmnt exit: $?"
docker ps -q | wc -l
  • Erwartung:
  • Die Datei enthält [Unit] und RequiresMountsFor=/mnt/ssd /mnt/unas /mnt/pictures /mnt/monitoring /mnt/paperless.
  • systemctl show zeigt dieselben Pfade ohne /mnt/storagebox.
  • findmnt exit: 1.
  • Die Container-Zahl ist unverändert.
  • Abbruch: fuser zeigt einen Prozess, dann melden. Fehlt [Unit] in der Datei, die Zeile wieder einfügen. Ohne Sektionskopf verwirft systemd den Drop-in still.

3.2 · Aufräumen ◆

rm /etc/systemd/system/mnt-storagebox.mount
systemctl daemon-reload
cp -a /home/robin/.config/rclone/rclone.conf /root/rclone.conf.bak-e1

Dann in /home/robin/.config/rclone/rclone.conf den Abschnitt [storage-box] löschen, mit einem Editor deiner Wahl. Die Datei enthält das obfuskierte Box-Passwort, das seit 2.3 ohnehin ungültig ist. Danach:

grep -c "storage-box" /home/robin/.config/rclone/rclone.conf
shred -u /root/rclone.conf.bak-e1
rmdir /mnt/storagebox

Erwartung: 0, und das leere Verzeichnis ist entfernt.