Бэкап серверов 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
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# Бэкап серверов на rpi (вариант «малина забирает»): ssh к серверу → forced command отдаёт
# tar|zstd|age, зашифрованный ОТКРЫТЫМ ключом. Закрытого ключа на малине нет — расшифровать
# здесь ничего нельзя (ключ у владельца: Mac ~/.config/age/servers-backup.key + резервная копия).
#
# servers-backup all — ежедневный прогон (таймер): fr1 daily, ru1 daily, по воскресеньям fr1 weekly
# servers-backup verify — сверка sha256 всех хранимых архивов (порча на диске)
#
# Хранение: <host>/daily — 14 последних, <host>/weekly — 8, <host>/monthly — 12 (хардлинк
# первого daily за месяц, места не занимает).
set -uo pipefail
ROOT=/mnt/backup/servers
KEY=/etc/server-backup/id_ed25519
KH=/etc/server-backup/known_hosts
declare -A TARGET=([fr1]=rus@161.97.93.252 [ru1]=mik@85.198.109.86)
declare -A KEEP=([daily]=14 [weekly]=8 [monthly]=12)
log() { echo "$*"; }
pull() { # pull <host> <profile>
local host=$1 profile=$2 dir="$ROOT/$1/$2" ts f rc want got
mkdir -p "$dir"
ts=$(date +%F_%H%M); f="$dir/$host-$profile-$ts.tar.zst.age"
ssh -i "$KEY" -o UserKnownHostsFile="$KH" -o StrictHostKeyChecking=yes -o BatchMode=yes \
-o ConnectTimeout=30 -o ServerAliveInterval=30 -o ServerAliveCountMax=6 \
"${TARGET[$host]}" "$profile" > "$f.part" 2> "$f.stderr"
rc=$?
want=$(grep '^SHA256 ' "$f.stderr" | tail -1 | cut -d' ' -f2)
got=$(sha256sum "$f.part" | cut -d' ' -f1)
if [ $rc -ne 0 ] || [ -z "$want" ] || [ "$want" != "$got" ] || [ ! -s "$f.part" ]; then
log "FAIL $host/$profile: ssh rc=$rc, sha сервера=${want:-нет}, sha принятого=$got"
sed 's/^/ stderr: /' "$f.stderr" | tail -20
rm -f "$f.part" "$f.stderr"
return 1
fi
mv "$f.part" "$f"
echo "$got $(basename "$f")" > "$f.sha256"
grep -v '^SHA256 ' "$f.stderr" | tail -50 > "$f.log"; [ -s "$f.log" ] || rm -f "$f.log"
rm -f "$f.stderr"
log "OK $host/$profile: $(du -h "$f" | cut -f1) $(basename "$f")"
# первый daily за месяц → monthly (хардлинк)
if [ "$profile" = daily ] && ! ls "$ROOT/$host/monthly/$host-daily-$(date +%Y-%m)"-* >/dev/null 2>&1; then
mkdir -p "$ROOT/$host/monthly"; ln "$f" "$f.sha256" "$ROOT/$host/monthly/"
fi
}
prune() {
local d keep
for d in "$ROOT"/*/{daily,weekly,monthly}; do
[ -d "$d" ] || continue
keep=${KEEP[$(basename "$d")]}
ls -1t "$d"/*.age 2>/dev/null | tail -n +$((keep + 1)) | while read -r old; do
rm -f "$old" "$old.sha256" "$old.log"; log "prune $(basename "$old")"
done
done
}
verify() {
local bad=0 n=0 s
while read -r s; do
n=$((n + 1))
(cd "$(dirname "$s")" && sha256sum --quiet -c "$(basename "$s")") || { log "BAD $s"; bad=1; }
done < <(find "$ROOT" -name '*.sha256')
log "verify: проверено $n архивов, $([ $bad = 0 ] && echo 'всё цело' || echo 'ЕСТЬ ПОВРЕЖДЁННЫЕ')"
return $bad
}
mountpoint -q /mnt/backup || { echo "/mnt/backup не смонтирован — пропуск"; exit 1; }
install -d -m 700 "$ROOT"
case "${1:-all}" in
all)
rc=0
pull fr1 daily || rc=1
pull ru1 daily || rc=1
[ "$(date +%u)" = 7 ] && { pull fr1 weekly || rc=1; }
prune
[ "$(date +%u)" = 7 ] && { verify || rc=1; }
exit $rc ;;
verify) verify ;;
pull) pull "$2" "$3" ;;
*) echo "usage: servers-backup all|verify|pull <host> <profile>" >&2; exit 2 ;;
esac