Бэкап, из которого можно восстановиться, запускается по расписанию без вашего участия, хранит одну копию вне площадки, где взломанный сервер не сможет ее удалить, снимает базы данных в согласованном состоянии и недавно проходил проверку восстановлением с замером времени. На restic это означает зашифрованный репозиторий в S3-совместимом хранилище под защитой object lock или append-only сервера, дампы баз перед каждым запуском, таймер systemd с оповещениями о сбоях и регулярные учения по восстановлению. В этом руководстве такая схема собирается для Linux-сервера, а те же элементы подходят и для машины разработчика.
Определите RPO и RTO до выбора инструментов
Recovery point objective (RPO) показывает, сколько данных вы готовы потерять, в единицах времени. Recovery time objective (RTO) показывает, сколько сервис может простаивать во время восстановления. Владелец сервиса отвечает на оба вопроса простыми словами: «мы можем потерять не больше одного дня заказов» и «магазин должен заработать в течение четырех часов».
Худшая потеря данных при бэкапе по расписанию примерно равна интервалу между запусками плюс времени, за которое запуск доходит до внешнего хранилища. Ночное задание, которое стартует в 02:00 и заканчивает выгрузку в 02:40, дает худший RPO около 24 часов 40 минут. Если для базы данных это слишком много, делайте ее бэкап чаще или добавьте архивирование WAL.
RTO в основном определяется временем передачи и пересборки. Время передачи в секундах равно объему данных в мегабитах, деленному на скорость канала в Мбит/с. Восстановление 200 ГБ составляет 1 600 000 мегабит, поэтому на 100 Мбит/с оно займет 16 000 секунд, около 4,4 часа, еще до восстановления базы. Если это уже больше RTO, один удаленный бакет цель не обеспечит, и нужна локальная копия или резервная система.
Правило 3-2-1 и его расширение 3-2-1-1-0
Правило 3-2-1 означает три копии данных на двух разных типах хранилищ, из которых одна копия находится вне площадки. На VPS первая копия находится на рабочем диске, вторая представляет собой локальный дамп или репозиторий на отдельном хранилище, а третья лежит в бакете у другого провайдера или в другом аккаунте. Второй раздел на том же диске умирает вместе с первым, поэтому держите локальную копию на другом устройстве (выбрать его поможет руководство по выбору NVMe SSD) или на другом хосте.
Производители систем резервного копирования популяризировали расширение 3-2-1-1-0. Дополнительная единица означает копию, которая находится офлайн или неизменяема, чтобы злоумышленник с root на сервере и его облачными учетными данными все равно не смог ее удалить. Ноль означает отсутствие ошибок в проверках целостности и тестах восстановления. Большинство схем пропускает именно последнюю цифру, хотя только она показывает, реальны ли остальные четыре.
Что включать в бэкап и что исключать
Сохраняйте то, что нельзя быстро воссоздать из публичного источника: /etc, код приложения и загрузки в /srv или /var/www, домашние каталоги с ключами SSH и GPG, crontab, TLS-материалы, дампы баз данных и список установленных пакетов из apt-mark showmanual. На машине разработчика ценны незапушенная работа, stash, dotfiles, ключи и документы.
Исключайте большие или воспроизводимые данные: кэши, node_modules, виртуальные окружения, результаты сборки, образы контейнеров, веса моделей и swap. Исключайте живые каталоги баз данных, например /var/lib/postgresql, потому что копия, снятая с работающего сервера, не является пригодным бэкапом. В файлах исключений restic шаблон, начинающийся с /, совпадает с абсолютным путем, а голое имя вроде node_modules совпадает на любой глубине:
/var/lib/postgresql
/var/lib/docker
/var/cache
/var/tmp
/swapfile
/home/*/.cache
node_modules
.venv
__pycache__
*.tmp
--exclude-caches дополнительно пропускает каталоги со стандартным файлом CACHEDIR.TAG, а --one-file-system не дает restic переходить в другие смонтированные файловые системы.
Основы restic: репозиторий, шифрование и пароль
restic хранит дедуплицированные снапшоты в репозитории на локальном диске, по SFTP, на собственном REST-сервере или в объектном хранилище и выгружает только новые фрагменты. Шифрование нельзя отключить: по документации дизайна restic все данные шифруются AES-256 в режиме счетчика и аутентифицируются Poly1305-AES, а ключ защищен паролем, усиленным через scrypt.
Держите настройки в файле окружения, доступном только root, чтобы интерактивные команды и задания по расписанию использовали одни и те же значения. В документации по репозиториям перечислены форматы URL, например s3:https://server:port/bucket для S3-совместимых сервисов.
# /etc/restic/restic.env, mode 600
RESTIC_REPOSITORY=s3:https://s3.example.com/web1-backups
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=replace-me
AWS_SECRET_ACCESS_KEY=replace-me
HEARTBEAT_URL=https://monitor.example.net/ping/web1-backup
ALERT_WEBHOOK_URL=https://chat.example.net/hooks/backups
install -d -m 700 /etc/restic
(umask 077; openssl rand -base64 32 > /etc/restic/password)
set -a; . /etc/restic/restic.env; set +a
restic init
restic key add # второй пароль для офлайн-копии на случай восстановления
restic backup --one-file-system --exclude-caches \
--exclude-file=/etc/restic/excludes.txt /etc /root /home /srv /var/www
restic snapshots
Документация говорит прямо: потеря пароля означает безвозвратную потерю данных. Храните пароль, URL репозитория и учетные данные бакета в менеджере паролей и в запечатанной офлайн-копии, но никогда только на защищаемом сервере. При работе с менеджером секретов RESTIC_PASSWORD_COMMAND может выводить пароль вместо чтения файла.
Хранение через forget и prune, целостность через check
restic forget применяет политику хранения к каждой группе снапшотов (по умолчанию группировка идет по хосту и путям), а prune удаляет данные, на которые больше не ссылается ни один снапшот. Флаг --prune выполняет оба шага:
restic forget --prune --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3
restic check --read-data-subset=10%
Такая политика сохраняет самый новый снапшот за каждый из последних 14 дней, в которые он был, затем по одному в неделю за 8 недель, в месяц за 12 месяцев и в год за 3 года. Изменения политики проверяйте, добавив --dry-run. В документации по forget сказано, что во время prune репозиторий заблокирован, поэтому разносите его и бэкапы по времени и давайте команде бэкапа --retry-lock.
restic check проверяет структуру и метаданные. --read-data скачивает каждый pack, что стоит времени и исходящего трафика, а --read-data-subset читает часть: 10% выбирает случайное подмножество при каждом запуске, а 3/12 читает фиксированную двенадцатую часть, как описано в руководстве по обслуживанию репозитория. Успешная проверка доказывает согласованность, но не то, что вы сможете восстановить сервис.
Внешний репозиторий, который не сотрет шифровальщик
Злоумышленник с root может прочитать /etc/restic/restic.env. Если эти учетные данные позволяют удалять объекты, внешнюю копию тоже можно удалить. Ограничьте учетные данные одним бакетом, держите административные ключи вне сервера и применяйте принцип минимальных привилегий из руководства по защите Linux VPS. Затем выберите одну из двух защит.
REST-сервер в режиме append-only
У rest-server от restic есть режим --append-only, который принимает новые бэкапы, но запрещает удалять или изменять существующие. Документация restic рекомендует запускать forget и prune для таких репозиториев с отдельной, хорошо защищенной машины и использовать --keep-within вместо правил вроде --keep-weekly, потому что взломанный клиент может добавить фальшивые снапшоты с подобранными метками времени, которые займут места настоящих.
Object lock в S3-совместимом хранилище
S3 Object Lock хранит версии объектов по модели write-once-read-many. Он требует версионирования, а в режиме compliance никто, включая root-пользователя аккаунта, не может удалить заблокированную версию до даты окончания хранения. Начиная с версии 0.13 restic отправляет контрольные суммы загрузки, которых требуют такие бакеты, поэтому он работает с политикой хранения бакета по умолчанию (для других провайдеров добавьте --endpoint-url):
aws s3api create-bucket --bucket web1-backups --object-lock-enabled-for-bucket
aws s3api put-object-lock-configuration --bucket web1-backups \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":30}}}'
Когда restic удаляет файл в таком бакете, S3 добавляет маркер удаления, а заблокированная версия остается. После атаки скопируйте бакет в том состоянии, в каком он был, например командой rclone copy --s3-version-at "2026-09-20" offsite:web1-backups /srv/recovered-repo, и восстанавливайтесь из этой копии. Данные после prune продолжают занимать место до конца срока хранения, поэтому удаляйте неактуальные версии правилом жизненного цикла не раньше этого срока. Проверьте весь цикл у своего провайдера, поскольку реализации S3-совместимых сервисов различаются.
Согласованные бэкапы PostgreSQL
Документация PostgreSQL о бэкапах на уровне файловой системы говорит прямо: для пригодной копии каталога данных сервер должен быть остановлен, и запрета подключений недостаточно, потому что утилиты вроде tar не делают атомарный снимок, а сервер буферизует данные в памяти. Атомарный снимок тома со всеми файлами данных и WAL работает, поскольку PostgreSQL воспроизводит WAL, как после сбоя.
Для большинства серверов логические дампы остаются самым простым корректным вариантом. pg_dump делает согласованную выгрузку, пока база используется, и не блокирует других пользователей. Формат directory позволяет параллельные дампы и восстановление. Роли в дамп не входят, поэтому глобальные объекты выгружаются отдельно. Этот скрипт запускается перед каждым бэкапом и хранит последние дампы на локальном диске:
#!/usr/bin/env bash
# /usr/local/sbin/pg-dump-for-backup
set -euo pipefail
out=/var/backups/postgresql
install -d -m 700 -o postgres -g postgres "$out"
runuser -u postgres -- pg_dumpall --globals-only --file="$out/globals.sql"
runuser -u postgres -- psql -Atc \
"SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate" |
while read -r db; do
rm -rf "$out/$db.new"
runuser -u postgres -- pg_dump --format=directory --jobs=2 --file="$out/$db.new" "$db"
rm -rf "$out/$db"
mv "$out/$db.new" "$out/$db"
done
Используйте pg_basebackup, когда логическое восстановление большого кластера не укладывается в RTO или нужен point-in-time recovery. Он копирует весь кластер в пустой каталог по протоколу репликации, требует роль с атрибутом REPLICATION и запись в pg_hba.conf, а также передает WAL, нужный для запуска копии. pg_verifybackup проверяет результат по манифесту:
sudo -u postgres pg_basebackup --pgdata=/var/backups/pg-base --wal-method=stream --checkpoint=fast
sudo -u postgres pg_verifybackup /var/backups/pg-base
pg_restore пересобирает индексы по их определениям. Если в базе есть большие векторные индексы, например HNSW из руководства по гибридному поиску на pgvector, эта пересборка может занять большую часть времени восстановления.
Расписание через сервис и таймер systemd
Сервис типа oneshot запускает сначала дамп, затем бэкап. Если дамп не удался, бэкап не стартует, и юнит переходит в состояние failed. Сохраните юниты в /etc/systemd/system/:
# restic-backup.service
[Unit]
Description=restic backup to the off-site repository
Wants=network-online.target
After=network-online.target postgresql.service
OnFailure=backup-alert@%n.service
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
CacheDirectory=restic
Nice=10
IOSchedulingClass=idle
ExecStartPre=/usr/local/sbin/pg-dump-for-backup
ExecStart=/usr/local/bin/restic backup --retry-lock 30m \
--one-file-system --exclude-caches \
--exclude-file=/etc/restic/excludes.txt --tag scheduled \
/etc /root /home /srv /var/www /var/backups/postgresql
ExecStartPost=-/usr/bin/curl -fsS --max-time 10 --retry 3 ${HEARTBEAT_URL}
# restic-backup.timer
[Unit]
Description=Nightly restic backup
[Timer]
OnCalendar=*-*-* 02:15:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
Включите таймер командой systemctl enable --now restic-backup.timer и проверьте его через systemctl list-timers. Согласно systemd.timer, Persistent=true догоняет запуск, пропущенный, пока машина была выключена, а RandomizedDelaySec= разносит по времени хосты, которые пишут в один бакет. У сервисов oneshot по умолчанию нет таймаута запуска. На Linux-ноутбуке та же пара работает как пользовательские юниты для домашнего каталога.
Еженедельный restic-maintenance.service с тем же заголовком выполняет очистку и проверки:
ExecStart=/usr/local/bin/restic forget --prune --retry-lock 2h \
--keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3
ExecStart=/usr/local/bin/restic check --read-data-subset=10%%
Пишите %% для буквального знака процента, потому что в юнитах % начинает спецификатор.
Мониторинг и оповещения о сбоях
Используйте два независимых сигнала, иначе тихий сбой обнаружится во время инцидента. Первый сигнал дает OnFailure=. Коды выхода restic конкретны: 1 означает ошибку команды, 3 означает, что часть исходных файлов не удалось прочитать, 10 сообщает об отсутствии репозитория, 11 о неудачной блокировке, а 12 о неверном пароле. systemd считает сбоем любой ненулевой код, и для 3 это правильно: снапшот есть, но в нем пробелы.
# [email protected]
[Unit]
Description=Alert for failed %i
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
ExecStart=/usr/bin/curl -fsS --max-time 10 --retry 3 \
--data-urlencode "text=%H: %i failed, see journalctl -u %i" \
${ALERT_WEBHOOK_URL}
Один шаблон обслуживает все задания, и systemd передает ему переменные вроде MONITOR_EXIT_STATUS. Проверьте цепочку командой systemctl start [email protected].
Второй сигнал работает как dead man's switch: ExecStartPost= отправляет запрос на heartbeat URL только после успеха, а система мониторинга поднимает тревогу, если запрос не пришел, скажем, за 26 часов. Так ловятся отключенный таймер или выключенный хост. Для взгляда снаружи ежедневно запускайте restic snapshots --latest 1 --json --no-lock с другой машины и оповещайте, если самый новый снапшот старше RPO.
Проведите учения по восстановлению с замером времени
Учения превращают предположения в цифры. Проводите их после первоначальной настройки, после крупных изменений и не реже раза в квартал на чистой виртуальной машине, а не на рабочем хосте, используя только то, что будет у инженера в реальном инциденте: запись в менеджере паролей и runbook.
- Запустите секундомер в момент начала условного инцидента.
- Получите учетные данные, установите restic и выведите список снапшотов. Возраст самого нового снапшота и есть ваш фактический RPO.
- Восстановите файлы и дампы, затем базу данных, замеряя каждый шаг.
- Запустите staging-копию приложения и пройдите один реальный сценарий.
- Запишите длительность этапов и проблемы, затем обновите runbook.
set -a; . ./restic.env; set +a
restic snapshots --latest 3
time restic restore latest --target /restore --verify
sudo -u postgres psql -f /restore/var/backups/postgresql/globals.sql
sudo -u postgres createdb app
time sudo -u postgres pg_restore --jobs=4 --dbname=app /restore/var/backups/postgresql/app
sudo -u postgres psql -d app -c "SELECT max(created_at) FROM orders;"
--verify повторно читает восстановленные файлы и сравнивает их со снапшотом. Последний запрос показывает, сколько данных восстановление фактически потеряло. Ошибки о том, что роли уже существуют, ожидаемы. Отработайте и восстановление одного файла с --include /etc/nginx/nginx.conf, самый частый реальный запрос.
Сравните общее время с RTO. Типичные сюрпризы: отсутствующие учетные данные, пароль, который жил только на погибшем сервере, медленная пересборка индексов и забытые файлы вроде TLS-ключей или .env за пределами сохраняемых путей.
restic, BorgBackup или снимки файловой системы
Многие схемы сочетают эти подходы: снимок ZFS, Btrfs или LVM для быстрого локального отката плюс файловые бэкапы вне площадки. BorgBackup 1.4 является текущей стабильной веткой.
| Свойство | restic | BorgBackup 1.4 | Снимки файловой системы |
|---|---|---|---|
| Куда хранить | Локально, SFTP, REST-сервер, S3-совместимые и другие облачные хранилища | Локально или по SSH, лучше с Borg на сервере | Тот же пул; для внешней копии нужна репликация, например zfs send |
| Шифрование | Всегда включено | Выбирается при создании репозитория, необязательно | Зависит от хранилища |
| Взломанный клиент | rest-server --append-only, object lock бакета |
borg serve --append-only |
Защищены только при репликации в отдельно администрируемую систему |
| Согласованность БД | Нужны дампы | Нужны дампы | Crash-consistent, если данные и WAL в одном атомарном снимке |
| Восстановление | От одного файла до всего дерева, монтирование через FUSE | От одного файла до всего дерева, монтирование через FUSE | Откат всего тома или копирование из смонтированного снимка |
| Клиенты | Linux, macOS, Windows, BSD | Linux, macOS, BSD; Windows только через экспериментальные WSL или Cygwin | Зависит от файловой системы |
Снимок в том же пуле защищает от неудачного деплоя, но не от отказа пула или взлома хоста, поэтому сам по себе он не является копией в смысле 3-2-1.
Чек-лист
- RPO и RTO записаны и согласованы с владельцем сервиса.
- Есть три копии, одна вне площадки и одна неизменяемая или append-only.
- Пароль, URL репозитория и учетные данные лежат в менеджере паролей и офлайн.
- Живые каталоги баз исключены, а дампы включены.
- PostgreSQL покрыт
pg_dumpс глобальными объектами или провереннымpg_basebackup. - Таймер использует
Persistent=true, а обработчикOnFailure=хотя бы раз сработал на тесте. - Heartbeat поднимает тревогу при отсутствии успешного запуска, а внешняя проверка при устаревших снапшотах.
forget --pruneиcheck --read-data-subsetвыполняются еженедельно.- Учетные данные сервера не могут удалить заблокированные версии.
- Учения по восстановлению с замером времени прошли в этом квартале, и runbook отражает их выводы.