Files

Бэкап серверов на rpi (fr1, ru1)

Внешняя площадка для VPS — домашняя малина. Схема «малина забирает, сервер шифрует»:

sequenceDiagram
    participant R as rpi (таймер 02:15)
    participant S as сервер (fr1 / ru1)
    R->>S: ssh -i /etc/server-backup/id_ed25519 <профиль>
    Note over S: forced command backup-export<br/>tar | zstd | age -r <ОТКРЫТЫЙ ключ>
    S-->>R: stdout: шифротекст
    S-->>R: stderr: "SHA256 <hex>" шифротекста
    Note over R: сверяет sha256, кладёт<br/>/mnt/backup/servers/<host>/<профиль>/
  • Малина не может расшифровать то, что хранит. Шифрует сервер открытым ключом age, закрытый ключ только у владельца: Mac ~/.config/age/servers-backup.key (+ резервная копия вне малины). Взлом малины или промпт-инъекция агента Astra = утечка только шифротекста.
  • Сервер не получает доступа к малине (в отличие от схемы «сервер пушит через обратный туннель»): ключ малины на сервере умеет только запустить выгрузку (restrict,command=…).
  • Целостность: sha256 шифротекста считает сервер, малина сверяет при приёме и раз в неделю (servers-backup verify). Подмену/порчу внутри поймает и age при расшифровке (AEAD).

Что входит

Хост/профиль Когда Содержимое Размер
ru1 daily ежедневно /etc целиком (OpenVPN, AWG, mtproxy, tproxy, iptables, юниты), /opt (MTProxy, xray), /srv, /usr/local, ~mik (easy-rsa — CA OpenVPN, клиентские .ovpn, wireguard, gost+LE, smart-routing), /root, crontab ~17 МБ
fr1 daily ежедневно /etc целиком (AWG-ключи, nginx, letsencrypt, tg-notify…), /srv/gitea + согласованный снимок gitea.db (snap/gitea.db, через sqlite .backup), /usr/local, ~rus (monitoring с секретами, bin, MyVault, .ssh), /root, crontab ~2.1 ГБ
fr1 weekly вс docker-тома claude-channels_claude-home (⚠️ ключи Astra к хостам), grafana, alertmanager, pushgateway; ~tgclaude (imgbot-data, imgbot, боты) ~10 ГБ

Не входит сознательно: том Prometheus (метрики, 7.9 ГБ, потеря не критична), ~rus/backups и ~tgclaude/backups (разовые архивы — скопировать один раз отдельно), спящий openclaw, кэши .claude/.cache, сборки (badvpn, telemt-build).

Хранение: daily — 14, weekly — 8, monthly — 12 (хардлинк первого daily месяца).

Файлы

  • fr1-backup-export → fr1 ~rus/bin/backup-export (читает root-файлы через docker run -v /:/host:ro: sudo на fr1 по паролю, а rus в группе docker). age — статический бинарник ~rus/bin/age.
  • ru1-backup-export → ru1 /usr/local/sbin/backup-export (root, 750), forced command sudo -n … у mik (у него sudo NOPASSWD). age — /usr/local/bin/age.
  • rpi/servers-backup, rpi/servers-backup.{service,timer} → rpi /usr/local/bin, /etc/systemd/system. Ключ /etc/server-backup/id_ed25519 (root 600), host keys серверов закреплены в /etc/server-backup/known_hosts. Итог прогона → метрики backup_*{job="backup_servers"}, алерт BackupStale (>26 ч) и BackupFailed.

Восстановление

Нужен закрытый ключ (на Mac). Архив можно расшифровывать потоком, не храня открытым на диске:

# список файлов
ssh rpi 'sudo sh -c "cat /mnt/backup/servers/ru1/daily/<файл>.age"' \
  | age -d -i ~/.config/age/servers-backup.key | zstd -dq | tar -tvf -

# достать один файл/каталог (например CA OpenVPN)
ssh rpi 'sudo sh -c "cat /mnt/backup/servers/ru1/daily/<файл>.age"' \
  | age -d -i ~/.config/age/servers-backup.key | zstd -dq | tar -xvf - home/mik/easy-rsa

# fr1: пути в архиве с префиксом host/ (tar шёл из контейнера с / на /host):
#   репозитории — host/srv/gitea/data/git/repositories/…, БД gitea — snap/gitea.db
#   (живой host/srv/gitea/data/gitea.db исключён — он был бы рваной копией)

⚠️ Потеря закрытого ключа = все бэкапы бесполезны. Минимум две копии в разных местах, и пробное восстановление раз в квартал.