Зачем вообще 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 брал их оттуда по пути. После включенияPrivateTmp
backend больше их не видит. - Два сервиса обменивались сокетом в
/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
сделал/etc
read‑only. Перенесите динамичную конфигурацию в/var/lib
или отдельный путь сReadWritePaths
. - Непредсказуемые сбои при загрузке модулей или управлении сетью. Вероятно, вырезаны критичные capabilities. Временно расширьте набор, найдите минимально достаточный список и зафиксируйте его.
Итог
Sandbox‑опции systemd
позволяют быстро и без миграции в контейнеры резко сократить последствия возможной компрометации сервиса. Начните с NoNewPrivileges
и PrivateTmp
, закрепите результат ProtectSystem=full
, затем переходите к strict
и минимальному CapabilityBoundingSet
. Дальше наращивайте дополнительные ограничения. Такой поэтапный подход безопасен для продакшена и ощутимо повышает уровень защиты на VDS без заметных затрат.