MAATRIX / Блог / Хранилище бэкапов было доступно проду на запись — и его вычистили

Хранилище бэкапов было доступно проду на запись — и его вычистили

MAATRIX

В четверг утром дежурный полез за бэкапом трёхдневной давности, чтобы поднять тестовое окружение — и не нашёл его. Каталог с архивами последних двух недель был пуст, остались только файлы старше месяца. Паника длилась около часа, пока не выяснилось, что данные никто не крал и не шифровал — их удалил свой же сервер, потому что у него было право на запись в то самое хранилище, которое должно было его защищать. Разберём, как дошло до такой конфигурации, что показали логи, какие версии отбросили и что в итоге поменяли.

Первый сигнал: пустой каталог там, где вчера было тридцать архивов

Инцидент обнаружили не мониторингом, а руками — это первая проблема, к которой мы ещё вернёмся. Инженер зашёл по SFTP на бэкап-сервер за дампом PostgreSQL недельной давности и увидел в /mnt/backups/prod-db/ всего четыре файла вместо ожидаемых пятнадцати-двадцати: записи с 3 по 17 число отсутствовали, а даты оставшихся файлов упирались в конец предыдущего месяца.

Первая реакция — проверить, не сломался ли сам бэкап-джоб:

systemctl status backup-db.timer
journalctl -u backup-db.service --since "-14 days" | grep -i error

Таймер оказался активен, сервис отрабатывал каждую ночь без единой ошибки, exit code 0 во всех запусках. Более того, в логе rsync были строки об успешной передаче свежих файлов каждую ночь — бэкапы создавались исправно. Значит, проблема не в том, что копии не делались, а в том, что они куда-то исчезали уже после того, как были успешно записаны.

Что показали логи и метрики

Дальше смотрели график занятого места на бэкап-томе в Zabbix. Место росло каждую ночь примерно на 2-3 ГБ (новый дамп плюс архив логов приложения), а затем резко падало обратно почти до нуля — но не сразу после записи, а с задержкой в несколько часов, ближе к обеду. Это исключало версию «бэкап не успевает записаться из-за таймаута»: файлы физически существовали часами, прежде чем пропасть.

auditd на бэкап-сервере включён не был (что тоже попало в список правок), поэтому пошли от обратного — смотреть, кто вообще имеет доступ к каталогу. /mnt/backups/prod-db/ оказался NFS-экспортом, смонтированным сразу на двух хостах: на самом бэкап-сервере (куда легитимно писал rsync с продакшена) и — неожиданно — на продакшн-сервере базы данных, в /mnt/remote-backups. Второй монтпоинт завели полгода назад для ручной выгрузки логов и с тех пор о нём забыли.

mount | grep nfs
# 10.20.0.15:/export/backups/prod-db on /mnt/remote-backups type nfs4 (rw,...)

Ключевое слово в выводе — rw. Продакшн-хост мог не только читать это хранилище, но и писать в него, включая удаление.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Гипотезы, которые отбросили

Прежде чем нашли настоящую причину, проверили и закрыли три версии:

  • Шифровальщик или взлом. Файлы не были зашифрованы и не оставили следов записки о выкупе — они просто отсутствовали. Проверили last, auth.log и активные сессии на обоих серверах — посторонних входов не было.
  • Ошибка в скрипте ротации бэкапов. Ротация на бэкап-сервере чистит старьё старше 30 дней, а пропадали файлы недельной давности — логика ротации не совпадала с картиной.
  • Сбой NFS. Логи nfs-kernel-server и dmesg — без ошибок ввода-вывода и без паник. Файлы удалялись штатным unlink, а не терялись из-за сбоя.

Настоящая причина: чужой cron писал в общий каталог по устаревшему шаблону

На продакшн-сервере базы данных существовал ещё один, отдельный от бэкапов, cron-скрипт очистки временных дампов приложения:

# /etc/cron.d/cleanup-exports
0 12 * * * appuser find /mnt/remote-backups -name "*.sql.gz" -mtime +3 -delete

Изначально он чистил /var/tmp/exports/ — временные выгрузки отчётов. При переносе этого инструмента на новый путь полгода назад путь в задаче захардкодили и позже кем-то «исправили руками» на общий монтпоинт /mnt/remote-backups, потому что туда были нужны временные выгрузки для одной разовой задачи. После неё джоб забыли выключить, а маска *.sql.gz -mtime +3 идеально совпадала с именами файлов дампа PostgreSQL, которые каждую ночь клал туда rsync с бэкап-сервера.

Механика была предельно скучной: два независимых процесса на двух разных серверах писали и чистили один и тот же каталог, потому что каталог оказался физически общим по NFS, а разграничения прав между «кто может писать» и «кто может только читать» не было вообще:

# /etc/exports на бэкап-сервере — как было
/export/backups/prod-db 10.20.0.0/24(rw,sync,no_subtree_check)

Вся подсеть продакшена получала право записи в бэкапы одной строкой конфига, написанной, судя по дате в git blame, ещё на этапе первоначальной настройки инфраструктуры — и с тех пор никто не возвращался её сузить.

Что изменили сразу после инцидента

Первым делом закрыли саму дыру — выдачу записи туда, где она не нужна:

# /etc/exports на бэкап-сервере — после правки
/export/backups/prod-db 10.20.0.15(ro,sync,no_subtree_check)
/export/backups/prod-db 10.20.0.20(rw,sync,no_subtree_check,no_root_squash=off)

Читающим клиентам, включая продакшн, — только ro. Право rw осталось исключительно за самим бэкап-сервером, который пишет туда локально, минуя сеть.

Убрали и сам cron-джоб — виновника удаления. Нашли его только через grep -r по всем /etc/cron.d/, /etc/cron.daily/ и crontab appuser на каждом хосте вручную, потому что единого реестра запланированных задач в компании не было.

Отдельно завели алерт на изменение объёма каталога с бэкапами — раньше мониторили только «бэкап-джоб завершился с кодом 0», но не то, что происходит с файлами после. Правило в Zabbix: если размер /mnt/backups/prod-db за сутки уменьшился больше чем на 20% вне окна ротации — алерт дежурному.

Как теперь устроена изоляция бэкапов от прода

После разбора пересобрали всю схему хранения копий по трём принципам, которые до инцидента были только на словах:

ПринципБылоСтало
Кто пишетЛюбой хост подсети прода (rw всем)Только бэкап-сервер, локальная запись
Доступ прода к хранилищуrw по NFSro, либо не смонтировано вовсе
Учётные данныеОбщий доступ по IP-подсетиОтдельный пользователь и ключ на источник
Обнаружение потериТолько руками, при восстановленииАлерт на изменение объёма каталога

Для новых проектов схему хранения теперь делаем через выделенный сервер только под бэкапы — отдельная машина, к которой у продакшн-хостов нет никакого сетевого маршрута на запись, только push по SSH с ограниченным command= в authorized_keys, который умеет принимать файл, но не умеет ничего удалять. Дороже по железу, чем один общий NFS-том на все нужды, зато исключает саму возможность повторить историю: нет прав на удаление — не поможет ни один забытый cron-джоб. Похожий подход к разделению копии и оригинала мы разбирали в статье про антипаттерн, когда бэкап лежит на том же сервере — принцип «копия недостижима из прода» здесь был нарушен не физическим соседством, а правами доступа по сети.

Если используете объектное хранилище S3-совместимого типа вместо NFS, включите версионирование объектов и, если провайдер поддерживает, object lock — тогда даже ошибочный DELETE не уничтожит данные немедленно (см. S3-совместимое хранилище у себя). Проверить схему на соответствие базовому правилу изоляции копий поможет статья про правило 3-2-1 для бэкапов — один экземпляр данных обязан быть недостижим из системы, которую он защищает.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро обнаружить, что бэкапы удаляются, а не просто не создаются?

Мониторьте не факт запуска джоба (exit code), а фактическое изменение объёма и количества файлов в хранилище — резкое падение размера каталога вне штатного окна ротации сразу видно на графике. Подробнее — в статье про мониторинг бэкапов.

Можно ли монтировать хранилище бэкапов на прод только для чтения и не бояться?

В целом да, ro-монтирование NFS исключает удаление и запись через этот канал, но не защищает от ошибок на стороне самого бэкап-сервера.

Почему не использовали S3-совместимое хранилище с самого начала, ведь там проще разграничить права?

Там действительно проще выдать ключ с правом только на PutObject без DeleteObject, но исторически инфраструктура строилась на NFS, и переезд на объектное хранилище сделали уже после этого инцидента.

Как понять, что старый бэкап действительно можно восстановить, а не просто лежит на диске?

Наличие файла на диске ничего не гарантирует — нужно регулярно поднимать копию в тестовом окружении и проверять её целостность, а не полагаться на факт существования архива.

Стоит ли давать продакшн-серверу вообще какой-либо доступ к каталогу с бэкапами?

Только если это действительно нужно бизнес-процессу — и строго ro, через отдельного пользователя, а не ту же учётку и монтпоинт, что использует сам бэкап-джоб.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →