Зачем вообще hardening сервисов на VDS
Даже идеально настроенный файрвол и обновлённые пакеты не спасут, если скомпрометирован один конкретный сервис. В таком случае нападающему важно, какие действия сможет выполнять процесс: писать в конфиги, подменять бинарники, сканировать файловую систему, выходить в сеть на привилегированные порты, подгружать модули ядра и т.д. Чем меньше прав у сервиса, тем меньше последствий от уязвимости. Этот подход и называется hardening на уровне сервиса.
В системах с systemd мы можем изолировать процессы и урезать их права буквально несколькими строками в юните. На VDS это особенно критично: на одном узле часто живёт несколько приложений, и изоляция снижает риск каскадных инцидентов.
Как работает sandbox в systemd
Опции sandbox в systemd создают для процесса ограниченное окружение: монтируют каталоги в read-only, прячут устройства, отделяют /tmp, отрезают опасные системные вызовы и Linux capabilities. Это работает на уровне пространств имён ядра (namespaces), cgroups и политики монтирования. В отличие от контейнеров, мы не меняем модель деплоя: сервис запускается как обычно, но в более строгих рамках.
Идея проста: пусть сервис видит и трогает только то, что ему действительно нужно. Всё остальное — read-only или недоступно.

Ключевые опции, которые стоит включать первыми
ProtectSystem
ProtectSystem переводит системные каталоги в режим только для чтения. Это рвёт целую категорию атак: подмена бинарников, модификация конфигов базовой системы, неожиданные записи в системные директории. На практике используют значения full или strict. При full системные области вроде /usr и других критичных путей становятся read-only. Режим strict делает ещё жёстче: почти вся файловая система становится только для чтения, а исключения на запись задаются явными списками через ReadWritePaths, StateDirectory, CacheDirectory, LogsDirectory и т.п. Для большинства сервисов full — безопасная отправная точка, а strict — цель для зрелой конфигурации.
PrivateTmp
PrivateTmp=yes выдаёт сервису собственный изолированный /tmp и /var/tmp. Один сервис больше не видит временные файлы другого. Это снижает вероятность как преднамеренных атак через подмену файлов в /tmp, так и случайных коллизий. Из важных нюансов: если два сервиса ожидали «встречаться» через общий /tmp, они перестанут видеть файлы друг друга. Лучше заменить такие практики на выделенные каталоги через RuntimeDirectory или обмен через сокеты/IPC.
CapabilityBoundingSet
Linux capabilities разрезают «root» на набор мелких привилегий. CapabilityBoundingSet позволяет жёстко задать список capabilities, которые разрешены процессу. Например, веб‑серверу часто нужен только CAP_NET_BIND_SERVICE для портов ниже 1024. Остальные (особенно опасные вроде CAP_SYS_ADMIN, CAP_SYS_MODULE, CAP_SYS_PTRACE, CAP_NET_ADMIN) лучше вырезать. Чем меньше список, тем безопаснее. Для сервисов без нужды слушать привилегированные порты смело делайте пустой набор.
NoNewPrivileges
NoNewPrivileges=yes запрещает процессу повышать свои права после запуска. Любые setuid-бинарники и дополнительные capabilities из файловых атрибутов не дадут прироста. Это дешёвая и очень эффективная защита, которую стоит включать почти везде.
ProtectSystem: от «full» к «strict» без боли
Чаще всего переходят так: сначала ProtectSystem=full, сервис работает — затем двигаются к strict. Главная сложность — правильно перечислить каталоги, куда процесс всё-таки должен писать. Вместо открытия целых деревьев рекомендуются декларативные директории StateDirectory, CacheDirectory, LogsDirectory, RuntimeDirectory. Они автоматически создаются с подходящими правами, попадают в нужные tmpfs или постоянные места, и вам не нужно держать список путей вручную.
Если же нужно явно разрешить запись в конкретный путь, используйте ReadWritePaths. Для доступа только на чтение — ReadOnlyPaths, а для запрета даже чтения — InaccessiblePaths. Так вы соберёте минимум поверхностей записи, который необходим приложению, а остальное останется read-only или недоступным.
PrivateTmp: типичные сценарии и отладка
Изоляция временных файлов чаще помогает, чем мешает. Однако проблемы всплывают, когда логика приложения опиралась на общий /tmp. Примеры:
- Веб‑сервер писал временные загруженные файлы в
/tmp, а backend брал их оттуда по пути. После включенияPrivateTmpbackend больше их не видит. - Два сервиса обменивались сокетом в
/tmp.
Правильные решения: настройте оба сервиса на использование общего RuntimeDirectory одного из них (или вынесите сокет в выделенный каталог под /run), либо явно укажите путь вне /tmp, например в /var/lib/yourapp/tmp, открыв к нему доступ через StateDirectory или ReadWritePaths. Проверка проста: смотрите реальный путь к /tmp внутри неймспейса процесса, а также логи отказов на запись.
CapabilityBoundingSet: как подбирать минимальный набор
Подход снизу вверх: сначала запрещаем всё (пустой список), затем добавляем ровно те capabilities, без которых сервис не стартует или теряет требуемую функциональность. Несколько ориентиров:
- HTTP‑сервер на 80/443 портах обычно требует только
CAP_NET_BIND_SERVICE. Для HTTPS не забудьте про актуальные SSL-сертификаты. - Сервис без низкопортового bind и без управления сетевыми интерфейсами не нуждается в сетевых привилегиях вовсе.
- Базы данных не нуждаются в
CAP_SYS_ADMIN,CAP_SYS_PTRACE,CAP_SYS_MODULE. - Процессы, запускаемые от непривилегированных пользователей, часто обходятся без любых capabilities.
Помните про AmbientCapabilities: если процесс реально должен наследовать capability в дочерних процессах, добавьте её туда. Но в большинстве случаев обходитесь только CapabilityBoundingSet.

NoNewPrivileges: включайте по умолчанию
Опция редко ломает легитимные сценарии и значительно сокращает класс атак через исполняемые файлы с setuid и файловые capabilities. Если сервис не зависит от запуска setuid‑бинарников (а это редкость для сетевых демонов), просто включайте NoNewPrivileges=yes.
Практический шаблон: безопасный override для типового сервиса
Используйте systemctl edit your.service и добавляйте минимальный профиль, постепенно ужесточая настройки. Пример стартового варианта для сетевого демона, слушающего 80/443:
[Service]
User=www-data
Group=www-data
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=true
PrivateTmp=yes
PrivateDevices=yes
ProtectControlGroups=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectHostname=yes
LockPersonality=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
MemoryDenyWriteExecute=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
# Явные каталоги для записи
StateDirectory=yourapp
CacheDirectory=yourapp
LogsDirectory=yourapp
RuntimeDirectory=yourapp
После успешного прогона тестов можно перейти к ProtectSystem=strict и, при необходимости, добавить ReadWritePaths для оставшихся путей.
Примеры по конкретным сервисам
Nginx
Часто достаточно: NoNewPrivileges=yes, ProtectSystem=full, PrivateTmp=yes, CapabilityBoundingSet=CAP_NET_BIND_SERVICE, каталоги на запись — /var/log/nginx, /var/cache/nginx. Если пойдёте в strict, добавьте нужные ReadWritePaths или используйте LogsDirectory/CacheDirectory. Для HTTPS пригодятся актуальные SSL-сертификаты.
PHP-FPM
Включайте PrivateTmp=yes без страха: обмен с Nginx идёт через сокеты/порт, а не через общий /tmp. Ограничьте записи только пулами логов и сессий. Сами сессии удобно хранить в каталоге, который создаёт StateDirectory. NoNewPrivileges и строгий набор capabilities (вплоть до пустого) обычно работают без проблем.
PostgreSQL/MySQL
У базы данных нет причин трогать системные каталоги или ядро. Рекомендуются: NoNewPrivileges=yes, ProtectSystem=full (в перспективе — strict), PrivateTmp=yes. Обязательно откройте каталог данных и логов через StateDirectory/LogsDirectory или ReadWritePaths. Набор capabilities — пустой.
Redis
Redis хорошо изолируется: пустой CapabilityBoundingSet, NoNewPrivileges=yes, ProtectSystem=strict с разрешением записи в каталог данных и логов, PrivateTmp=yes. Если используется RDB/AOF, не забудьте разрешить запись туда, где лежат файлы персистентности.
Отладка и верификация
Несколько приёмов, чтобы проверять и улучшать профиль безопасности:
systemd-analyze security your.service— оценка безопасности юнита и подсказки по улучшениям.journalctl -u your.service -b— ищите Permission denied и отказ в монтировании или открытии файлов.systemctl show -p FragmentPath -p DropInPaths your.service— убедитесь, что override подхватился.systemctl status your.service— быстрый взгляд на ошибки запуска.- Тестовый запуск с параметрами:
systemd-run --unit=sandbox-test -p PrivateTmp=yes -p NoNewPrivileges=yes /usr/bin/env.
Про оркестрацию задач см. также материал про таймеры systemd: планирование без crontab, а для фона и очередей — systemd‑воркеры.
Совместимость с SELinux/AppArmor и ядром
Опции systemd и MAC‑политики (SELinux/AppArmor) комплементарны. Если у вас активен SELinux в enforcing‑режиме, некоторые операции могут блокироваться до попадания в логику sandbox systemd. Это неплохо: многослойная защита — цель. Просто учитывайте оба источника ограничений при отладке. Сторонние модули ядра или нестандартные драйверы потребуют аккуратнее подходить к ProtectKernelModules и CapabilityBoundingSet.
План безопасного внедрения на проде
- Снимите «снимок» состояния:
systemd-analyze securityдля ключевых юнитов, зафиксируйте метрики. - Начните с безобидных опций:
NoNewPrivileges=yes,PrivateTmp=yes, пустой или минимальныйCapabilityBoundingSet. - Перейдите к
ProtectSystem=full; протестируйте сценарии записи (логи, кэш, загрузки). - Выделите каталоги через
StateDirectory/CacheDirectory/LogsDirectory/RuntimeDirectory. - Доводите до
ProtectSystem=strict, добавляя точечныеReadWritePathsпри необходимости. - Включайте дополнительные ограничения:
RestrictSUIDSGID,RestrictNamespaces,MemoryDenyWriteExecute,ProtectKernel*. - Катите поэтапно, начиная со стейджа и непиковых часов. Держите под рукой быстрый откат:
systemctl revert your.service.
Частые ошибки и как их диагностировать
- Сервис не может открыть порт 80/443 после ужесточения. Проверьте, что в
CapabilityBoundingSetиAmbientCapabilitiesестьCAP_NET_BIND_SERVICE. - Permission denied при записи в лог. Либо используйте
LogsDirectory, либо дайте доступ черезReadWritePathsк каталогу логов. - Пропали временные файлы между сервисами. Это
PrivateTmp. Перенесите обмен в общий каталог под/runили используйте сокеты/IPC. - Конфиг изменяется приложением и перестал сохраняться.
ProtectSystemсделал/etcread‑only. Перенесите динамичную конфигурацию в/var/libили отдельный путь сReadWritePaths. - Непредсказуемые сбои при загрузке модулей или управлении сетью. Вероятно, вырезаны критичные capabilities. Временно расширьте набор, найдите минимально достаточный список и зафиксируйте его.
Итог
Sandbox‑опции systemd позволяют быстро и без миграции в контейнеры резко сократить последствия возможной компрометации сервиса. Начните с NoNewPrivileges и PrivateTmp, закрепите результат ProtectSystem=full, затем переходите к strict и минимальному CapabilityBoundingSet. Дальше наращивайте дополнительные ограничения. Такой поэтапный подход безопасен для продакшена и ощутимо повышает уровень защиты на VDS без заметных затрат.


