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:
- Rollen:
ssh nuc=robin(lesen),ssh nuc-root= root. Auf dem aiserverssh aiserverund dannsudo. Den VPS erreichst du mitssh -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:
- 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 sindntfy.token,healthchecks-nuc-local.yaml,healthchecks-nuc-hetzner.yamlundrepositories-hetzner.yaml, von früher stammenhetzner.passundlocal.pass. - Abbruch:
ntfy tokens differoder 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, beidegrep-Zähler0. - Abbruch:
validatemeldet einen Fehler. Dann die alten Configs zurückholen und melden:
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-listlö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:
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 ◆¶
- Console → Reset password. Das Passwort kommt nach Infisical
/borgmaticalsSTORAGEBOX_MAIN_PASSWORDund nach Proton Pass. Es kommt auf keinen Host. - 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:
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
restoreschreiben: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 aiserverrm /etc/borgmatic.d/aiserver.yaml.bak-e1.borg_aiserverauf 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]undRequiresMountsFor=/mnt/ssd /mnt/unas /mnt/pictures /mnt/monitoring /mnt/paperless. systemctl showzeigt dieselben Pfade ohne/mnt/storagebox.findmnt exit: 1.- Die Container-Zahl ist unverändert.
- Abbruch:
fuserzeigt 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.