Неизменяемый бэкап своими силами: защита копий от того, кто уже внутри
Классический сценарий взлома заканчивается не кражей данных, а их шифрованием — включая бэкапы, если у скомпрометированного root на продакшене есть куда дотянуться руками или сетевым доступом. Проблема не в том, что бэкапы не делались, а в том, что они были устроены так же уязвимо, как и защищаемые ими данные. Ниже — рабочие способы сделать так, чтобы даже полный root-доступ к продакшену не позволял злоумышленнику стереть или изменить уже созданные копии, без покупки дорогих enterprise-решений для неизменяемого хранения.
Содержание
- Модель угроз: почему root на проде — ещё не гарантия защиты бэкапа
- Архитектура: отдельный сервер и роль, которая умеет только писать
- Ограниченный SSH: forced command и пользователь без права удаления
- Файловые флаги и снапшоты: chattr +i и ZFS в режиме readonly
- Облачные варианты: S3 Object Lock и self-hosted MinIO
- Где иммутабельность не спасает: честные ограничения
Модель угроз: почему root на проде — ещё не гарантия защиты бэкапа
Когда вы настраиваете бэкап скриптом с продакшн-сервера — rsync, pg_dump с последующей отправкой, cron-джобом на выгрузку в облако — вы неявно даёте этому серверу учётные данные для записи в хранилище копий. Учётные данные, которые с этого же сервера крадёт злоумышленник в первую же минуту после эскалации привилегий: ключ SSH из /root/.ssh/, токен облачного провайдера из переменных окружения, пароль к rsync-модулю из конфига. Если у него достаточно прав, чтобы отправить бэкап, у него достаточно прав, чтобы его удалить или перезаписать — а шифровальщики и целенаправленные атаки именно так и работают: сначала находят и портят резервные копии, чтобы жертва не могла восстановиться без выкупа.
Реальная защита строится на асимметрии: продакшн должен уметь только добавлять новые данные в хранилище бэкапов, но не должен физически или логически иметь возможность удалить или изменить то, что там уже лежит — даже обладая полным root-доступом на себе самом. Это не паранойя ради галочки, а прямое следствие того, что бэкап, доступный на запись и удаление с того же сервера, который он защищает, — по сути не бэкап, а просто ещё одна копия данных, которая падёт вместе с оригиналом. Ниже — конкретные механизмы, которые превращают эту идею в работающую конфигурацию, без покупки дорогих специализированных appliance-решений для immutable-хранения.
Архитектура: отдельный сервер и роль, которая умеет только писать
Базовая схема, которая закрывает 90% риска без экзотики: отдельная физическая или виртуальная машина под бэкапы, не входящая в тот же домен управления, что и продакшен. Идеально — на отдельном провайдере или хотя бы в отдельном сетевом сегменте без общих учётных записей и общего root-доступа. Если у вас есть доступ по SSH-ключу от продакшена на бэкап-сервер и наоборот — это уже не изоляция, а общая зона поражения.
Дальше на этом сервере заводится не общий root-аккаунт для приёма бэкапов, а отдельный непривилегированный пользователь с урезанным набором возможностей:
useradd -m -s /usr/sbin/nologin -d /home/backupwrite backupwrite
mkdir -p /home/backupwrite/.ssh /mnt/backups/prod-db
chown backupwrite:backupwrite /home/backupwrite/.ssh
chmod 700 /home/backupwrite/.ssh
Ключевой момент — /usr/sbin/nologin в качестве шелла. Даже если ключ этого пользователя утечёт вместе с root-доступом к продакшену, интерактивный вход по SSH невозможен: единственное, что может выполнить клиент, — то, что явно разрешено принудительной командой в authorized_keys (следующий раздел). Это и есть асимметрия ролей на уровне ОС, а не на уровне доброй воли скрипта.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверОграниченный SSH: forced command и пользователь без права удаления
Ядро схемы — строка command= в authorized_keys бэкап-пользователя, которая переопределяет любую команду, присланную клиентом, и подставляет вместо неё жёстко заданную:
# /home/backupwrite/.ssh/authorized_keys
command="rrsync -wo /mnt/backups/prod-db/",no-pty,no-agent-forwarding,no-X11-forwarding,no-port-forwarding ssh-ed25519 AAAA...prod-db-source
rrsync — обёртка над rsync, идущая в комплекте с пакетом (apt install rsync, скрипт лежит в /usr/share/doc/rsync/support/rrsync или ставится отдельным пакетом rrsync в зависимости от дистрибутива), которая ограничивает клиента одним каталогом и фильтрует опасные флаги вроде --delete. Флаг -wo включает write-only режим: клиенту разрешено только отправлять файлы, но не запрашивать листинг или чтение уже лежащих в каталоге. Важный нюанс: список разрешённых и запрещённых опций у rrsync менялся между версиями rsync, поэтому перед продакшн-использованием стоит явно проверить актуальное поведение на вашей версии — rrsync --help и код скрипта читаются за пять минут.
Но полагаться только на фильтрацию флагов клиента — хрупко: сегодня rrsync режет --delete, а завтра появится новый деструктивный флаг, о котором вы не подумали. Поэтому вторая, более надёжная линия защиты — файловые имена и права каталога, которые делают перезапись физически невозможной независимо от того, что просит клиент:
- Каждый бэкап пишется под уникальным именем с меткой времени (
prod-db-2026-08-30T02-00.sql.gz), а не поверх одного и того жеlatest.sql.gz— тогда «запись» никогда не совпадает с «перезаписью» существующего файла. - Каталог
/mnt/backups/prod-dbпринадлежитroot, а неbackupwrite, и имеет права733—backupwriteможет создавать в нём новые файлы (нужны права на запись и исполнение каталога), но не может удалить чужие файлы или переименовать каталог целиком, потому что не владеет им.
Это не идеальная защита от удаления собственных только что созданных файлов (пока владелец файла — сам backupwrite, он всё ещё может стереть свой файл в пределах текущей сессии rsync), но комбинация с иммутабельностью на уровне файловой системы (следующий раздел) закрывает и этот пробел.
Файловые флаги и снапшоты: chattr +i и ZFS в режиме readonly
На ext4 и XFS (расширенные атрибуты поддерживают оба) есть флаг неизменяемости на уровне VFS — chattr +i. Файл с этим флагом нельзя изменить, удалить, переименовать или дописать в него — вообще никак, пока флаг не снят явно:
chattr +i /mnt/backups/prod-db/prod-db-2026-08-30T02-00.sql.gz
lsattr /mnt/backups/prod-db/prod-db-2026-08-30T02-00.sql.gz
# ----i---------e------- prod-db-2026-08-30T02-00.sql.gz
Практическая схема: backupwrite пишет новый файл через forced command, а отдельный локальный скрипт на самом бэкап-сервере (запускается по cron или через inotifywait на завершение записи) сразу после приёма ставит +i на файл. У пользователя backupwrite при этом нет прав на выполнение chattr — он ограничен принудительной командой rsync и физически не может снять флаг с уже помеченного файла, даже если бы захотел.
Честная оговорка: chattr +i снимается процессом с capability CAP_LINUX_IMMUTABLE, а у root-процесса на той же машине эта capability есть по умолчанию. То есть флаг защищает от скомпрометированного продакшена, у которого нет шелл-доступа к бэкап-серверу вообще — но не от полноценного root-доступа непосредственно на самом бэкап-сервере. Если атакующий смог перепрыгнуть с прода на бэкап-хост, chattr +i его не остановит. Отсюда правило: сетевая изоляция бэкап-сервера — не опциональное дополнение к иммутабельности, а её обязательное условие.
Для ZFS-хранилищ работает похожий, но чуть иной механизм. Снапшот ZFS в принципе не редактируется — copy-on-write гарантирует, что блоки, зафиксированные в снапшоте, не меняются, и каталог .zfs/snapshot/имя всегда доступен только на чтение на уровне ядра, независимо от прав пользователя. Но снапшот можно удалить целиком командой zfs destroy, если у процесса есть на это права:
zfs snapshot backups/prod-db@2026-08-30
zfs hold keep backups/prod-db@2026-08-30
zfs destroy backups/prod-db@2026-08-30
# cannot destroy 'backups/prod-db@2026-08-30': dataset is busy
zfs hold ставит защиту от удаления, которую не снимает даже владелец датасета без явного zfs release. В сочетании с zfs allow, ограничивающим права реплицирующего пользователя только до receive,create,mount (без destroy и без hold,release), получается связка, которая логически повторяет схему с chattr +i: продакшн может добавлять новые снапшоты через zfs send | ssh backup zfs receive, но не может удалить старые — потому что у ограниченного аккаунта на приёмной стороне физически нет на это прав. Подробнее о том, где заканчиваются гарантии самих снапшотов и почему это не замена полноценному бэкапу, — в статье про снапшоты ZFS против бэкапа.
Облачные варианты: S3 Object Lock и self-hosted MinIO
Если храните копии в объектном хранилище S3-совместимого типа, есть готовый механизм именно для этого сценария — Object Lock. У него два режима, и разница между ними принципиальна:
| Режим | Кто может удалить до истечения срока | Когда уместен |
|---|---|---|
| Governance | Пользователь со спецправом s3:BypassGovernanceRetention | Защита от типовой ошибки и скомпрометированного рабочего аккаунта без спецправа |
| Compliance | Никто, включая владельца бакета, до истечения retention | Защита от полной компрометации, включая административные учётки |
На AWS S3 включение выглядит так (Object Lock нельзя добавить задним числом на уже существующий бакет — только при создании, либо через обращение в поддержку для миграции):
aws s3api create-bucket --bucket my-backups-lock --create-bucket-configuration LocationConstraint=eu-west-2
aws s3api put-bucket-versioning --bucket my-backups-lock --versioning-configuration Status=Enabled
aws s3api put-object-lock-configuration --bucket my-backups-lock \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":30}}}'
Если хотите держать object lock у себя, а не у стороннего облачного провайдера — тот же функционал есть в self-hosted MinIO, который вы разворачиваете на арендованном сервере под своим контролем. Здесь тоже действует то же ограничение: локировку нужно включить в момент создания бакета:
mc mb --with-lock local/prod-db-backups
mc retention set --default COMPLIANCE 30d local/prod-db-backups
Синтаксис команд mc менялся между версиями клиента, так что перед боевым использованием стоит свериться с документацией конкретно вашей сборки. Идея self-hosted варианта в том, что вы получаете гарантию неизменяемости без зависимости от чужого облака и без ежемесячных счетов за GB-хранение у провайдера — платите только за сервер, на котором крутится MinIO. Как поднять такое хранилище с нуля на своей машине, разобрано в статье про S3-совместимое хранилище у себя. Для этой роли логично брать сервер в отдельной юрисдикции и у отдельного провайдера от продакшена — тогда компрометация одной инфраструктуры не автоматически означает доступ ко второй.
Где иммутабельность не спасает: честные ограничения
Ни один из перечисленных механизмов не заменяет остальные принципы резервного копирования, и здесь стоит быть прямым:
- Иммутабельность защищает от удаления и изменения, но не от отсутствия бэкапа. Если джоб сломался и данные вообще не доехали до хранилища, никакой
chattr +iне поможет — нужен отдельный мониторинг факта успешной доставки, а не только факта запуска скрипта. - Immutable-копия повреждённых данных остаётся повреждённой копией. Если шифровальщик уже испортил файлы на проде до момента бэкапа, вы просто получите неизменяемую, но нерабочую копию. Ретеншен нужно планировать так, чтобы держать несколько поколений копий за разные даты — тогда есть шанс откатиться к точке до заражения. Разбор именно такого сценария — в статье про бэкапы, зашифрованные вместе с боевыми данными.
- Compliance-режим необратим и в вашу сторону тоже. Если вы установили retention на 30 дней в режиме Compliance, вы сами не сможете удалить эти объекты раньше срока — ни вручную, ни по ошибке. Это осознанный компромисс между защитой и гибкостью, и его стоит принимать заранее, а не постфактум.
- Растущее хранилище стоит денег. Append-only схема без ротации накапливает данные бесконечно. Нужна отдельная, доверенная процедура очистки по-настоящему устаревших копий — с явным снятием
chattr -iи явнымzfs releaseпередdestroy, выполняемая вручную или доверенным процессом, а не тем же аккаунтом, что принимает новые бэкапы. - Иммутабельность на бэкап-сервере не защищает, если скомпрометирован сам бэкап-сервер. Все описанные механизмы предполагают, что атакующий достиг только продакшена. Если есть общий канал управления (общий пароль, общий VPN, общая учётка администратора) между продом и бэкап-хостом — модель угроз рушится целиком, и никакие флаги файловой системы это не исправят.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Достаточно ли только chattr +i, без отдельного сервера?
Нет. Если бэкап и его +i-флаг живут на том же хосте, что и продакшн, root на этом хосте снимет флаг за одну команду. Иммутабельность работает только в паре с изоляцией — отдельная машина, отдельные учётные данные, отсутствие общего root-доступа.
Чем принудительная SSH-команда лучше обычного пароля к rsync-модулю?
Пароль или ключ можно использовать для любой команды, которую поддерживает протокол. command= в authorized_keys жёстко подменяет любую присланную клиентом команду заданной — даже если у атакующего есть ключ, он не может заставить сервер выполнить что-то, кроме разрешённого write-only rsync.
Что произойдёт, если я случайно поставлю chattr +i не на тот файл?
Ничего катастрофического — флаг снимается командой chattr -i тем, у кого есть на это права (обычно root на бэкап-сервере). Проблема возникает только тогда, когда снять флаг некому, потому что вы намеренно ограничили себе доступ, — держите отдельный административный канал с более широкими правами, недоступный из forced-command-аккаунта.
ZFS snapshot readonly и zfs set readonly=on — это одно и то же?
Нет. Свойство readonly=on запрещает запись в живой датасет (полезно для принимающей стороны репликации, чтобы её никто не трогал вручную). Сам факт, что снапшот нельзя отредактировать, — отдельное встроенное свойство снапшотов, действующее всегда, независимо от этого параметра.
Стоит ли переходить сразу на облачный Object Lock, минуя self-hosted схему?
Если для вас приемлема зависимость от стороннего провайдера и его биллинга — да, это быстрее в настройке. Self-hosted MinIO на своём сервере даёт тот же механизм без ежемесячных счетов за хранение и с полным контролем над юрисдикцией данных, но требует администрирования.
Нужен ли отдельный мониторинг для immutable-хранилища?
Да, обязательно. Иммутабельность защищает уже записанные данные, но не сигнализирует, если новые бэкапы перестали доезжать. Алерт на отсутствие свежих файлов ожидаемого размера нужен точно так же, как и при обычной схеме.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →