Бэкап серверов fr1/ru1 на rpi: сервер шифрует age открытым ключом, малина только хранит

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gilfanovr
2026-10-11 14:44:05 +03:00
co-authored by Claude Opus 5.5
parent e645377916
commit 7d0a891793
11 changed files with 334 additions and 8 deletions
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# Выгрузка бэкапа fr1 для rpi. Forced command в ~rus/.ssh/authorized_keys:
# restrict,command="/home/rus/bin/backup-export" <ключ server-backup@rpi>
# Клиент передаёт профиль: daily | weekly. В stdout — tar | zstd | age (шифрует ОТКРЫТЫМ
# ключом, закрытый только у владельца вне малины — rpi хранит то, что сама прочитать не может).
# В stderr последней строкой — "SHA256 <hex>" шифротекста: rpi сверяет, что дошло целиком.
#
# Читает root-файлы через docker (rus в группе docker = root-эквивалент), а не через sudo:
# sudo на fr1 по паролю. Монтирование / только read-only.
set -euo pipefail
PUB=age1qfsgzyglv0vmq4kskg888hzgq032mry5kxz9kptkvzsrz0fz0v7q6kxwk2
AGE=/home/rus/bin/age
IMG=debian:stable-slim # GNU tar: --exclude/--warning, в alpine — busybox
profile="${SSH_ORIGINAL_COMMAND:-daily}"
tmp=$(mktemp -d /tmp/backup-export.XXXXXX)
cleanup() { rm -rf "$tmp"; docker exec gitea rm -f /tmp/gitea-snap.db >/dev/null 2>&1 || true; }
trap cleanup EXIT
case "$profile" in
daily)
# Gitea на SQLite: копировать живой gitea.db нельзя (рваная копия) — .backup даёт согласованный снимок
docker exec -u 1000 gitea sqlite3 /data/gitea.db ".backup '/tmp/gitea-snap.db'"
docker cp gitea:/tmp/gitea-snap.db "$tmp/gitea.db" >/dev/null
paths=(host/etc host/srv/gitea host/usr/local host/home/rus host/var/spool/cron host/root snap)
excl=(--exclude=host/srv/gitea/data/gitea.db --exclude='host/srv/gitea/data/gitea.db-*'
--exclude=host/home/rus/backups --exclude=host/home/rus/.openclaw
--exclude=host/home/rus/openclaw --exclude=host/home/rus/openclaw-work
--exclude=host/home/rus/.cache --exclude=host/home/rus/.claude --exclude='*.bak-*' --exclude='*.tar.gz')
;;
weekly)
paths=(host/var/lib/docker/volumes/claude-channels_claude-home
host/var/lib/docker/volumes/monitoring_grafana_data
host/var/lib/docker/volumes/monitoring_alertmanager_data
host/var/lib/docker/volumes/monitoring_pushgateway_data
host/home/tgclaude)
excl=(--exclude=host/home/tgclaude/backups --exclude=host/home/tgclaude/openclaw-deploy
--exclude=host/home/tgclaude/.local --exclude=host/home/tgclaude/.cache --exclude=host/home/tgclaude/chrome-libs)
;;
*) echo "профиль: daily|weekly" >&2; exit 2 ;;
esac
# tar exit 1 = «файл изменился при чтении» (живые логи/репо) — не ошибка для бэкапа
docker run --rm --network none -v /:/host:ro -v "$tmp":/snap:ro "$IMG" \
sh -c 'tar --numeric-owner --ignore-failed-read --warning=no-file-changed -C / -cf - "$@"; rc=$?; [ $rc -le 1 ] || exit $rc' tar "${excl[@]}" "${paths[@]}" \
| zstd -3 -T0 -q | "$AGE" -r "$PUB" | tee >(sha256sum > "$tmp/sum")
# процесс-подстановка живёт отдельно от конвейера — ждём, пока sha256sum допишет итог
for _ in $(seq 300); do [ -s "$tmp/sum" ] && break; sleep 0.1; done
echo "SHA256 $(cut -d' ' -f1 "$tmp/sum")" >&2