Выберите продукт

systemd hardening: DynamicUser, ProtectSystem и практичный sandboxing для сервисов

Пошагово усиливаем безопасность systemd-сервисов без контейнеров: включаем DynamicUser, ограничиваем файловую систему через ProtectSystem/ProtectHome, изолируем /tmp, режем привилегии NoNewPrivileges и CapabilityBoundingSet, добавляем SystemCallFilter и проверяем изменения в проде.
systemd hardening: DynamicUser, ProtectSystem и практичный sandboxing для сервисов

Когда сервис на Linux запускается «как есть», он обычно получает больше прав и доступа к файловой системе, чем ему нужно. В итоге любая RCE-уязвимость превращается в проблему уровня сервера: чтение конфигов и ключей, запись в «системные» каталоги, запуск дочерних процессов и попытки закрепиться.

У systemd есть мощный набор директив для sandboxing и hardening unit без контейнеров. Ниже — практичный набор: DynamicUser, ProtectSystem, ProtectHome, PrivateTmp, NoNewPrivileges, CapabilityBoundingSet, SystemCallFilter, а также подготовка каталогов через системные директории и tmpfiles.d.

Важная мысль: hardening почти всегда делается итеративно. Добавили ограничение, проверили функциональность, посмотрели логи, поправили. Это нормальный цикл, и его стоит заложить в план внедрения.

Базовая идея hardening в systemd

Удобная модель: сначала определяем, что сервису точно нужно, затем разрешаем это явно, а остальное запрещаем. Обычно всё сводится к трём зонам:

  • Файловая система: куда можно читать и куда можно писать.
  • Привилегии: какие capabilities и повышение прав допустимы.
  • Поведение процесса: какие системные вызовы и пространства имён разрешены.

Практическое правило: чем меньше «область записи» и чем меньше привилегий у процесса, тем сложнее атакующему превратить баг в компрометацию системы.

Где размещать настройки

Unit-файлы из пакетов лучше не править напрямую. Используйте drop-in:

systemctl edit yourservice.service

Откроется файл вида /etc/systemd/system/yourservice.service.d/override.conf. После изменений:

systemctl daemon-reload
systemctl restart yourservice.service
systemctl status yourservice.service

Проверить итоговую конфигурацию (с учётом всех drop-in и наследований):

systemctl cat yourservice.service
systemd-analyze security yourservice.service

DynamicUser: сервис без постоянного пользователя

DynamicUser=yes говорит systemd: «создай временного пользователя/группу на время запуска сервиса и удали после остановки». Это снижает риск, что процесс будет писать в «чужие» каталоги, и упрощает эксплуатацию: не нужно заводить системного пользователя вручную.

Нюанс: динамический пользователь не должен писать куда попало. Вам нужно явно подготовить каталоги под запись.

StateDirectory/CacheDirectory/LogsDirectory/RuntimeDirectory вместо ручных mkdir

Самый правильный путь — дать systemd создать директории с корректными правами:

[Service]
DynamicUser=yes
StateDirectory=yourservice
CacheDirectory=yourservice
LogsDirectory=yourservice
RuntimeDirectory=yourservice

Что это даёт на практике:

  • StateDirectory создаёт /var/lib/yourservice (данные состояния).
  • CacheDirectory создаёт /var/cache/yourservice (кэш).
  • LogsDirectory создаёт /var/log/yourservice (если нужны файлы логов; часто хватает journald).
  • RuntimeDirectory создаёт /run/yourservice (сокеты, pid-файлы, runtime-данные).

Ключевое: systemd выставит владельца/права так, чтобы процесс мог писать туда даже без постоянного UID.

Пример drop-in override.conf для systemd-сервиса в терминальном редакторе

Когда всё-таки нужен tmpfiles.d

tmpfiles.d полезен, когда нужен нестандартный путь, симлинк, файл-заглушка, FIFO, или вы хотите управлять очисткой (retention). Также он выручает, если приложение «жёстко» ожидает каталог не в стандартном месте.

Пример: создаём каталог и очищаем старые файлы. Файл /etc/tmpfiles.d/yourservice.conf:

d /var/lib/yourservice 0750 root root -
d /var/lib/yourservice/uploads 0750 root root -
q /var/lib/yourservice/tmp 0750 root root 7d

Важно: если у вас DynamicUser=yes, то владение каталогом «динамическим» UID вне запуска сервиса проблематично. Поэтому чаще используйте StateDirectory/RuntimeDirectory, а в tmpfiles.d оставляйте только то, что действительно нельзя выразить штатными директивами.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

ProtectSystem и ProtectHome: ограничиваем файловую систему

Большинство успешных атак после RCE — это чтение секретов и запись/подмена файлов. Поэтому защита ФС — одна из самых эффективных мер.

ProtectSystem

ProtectSystem монтирует части файловой системы в режим «только чтение» для процесса сервиса.

  • ProtectSystem=true обычно переводит /usr, /boot, /etc в read-only.
  • ProtectSystem=full делает read-only почти всё, кроме стандартных путей для записи (поведение зависит от версии systemd).
  • ProtectSystem=strict максимально «закрывает» ФС, а вы затем точечно открываете нужные пути через ReadWritePaths.

Практический рецепт: для новых сервисов начинайте с ProtectSystem=strict и добавляйте ReadWritePaths только туда, где действительно нужна запись.

ProtectHome

ProtectHome ограничивает доступ к домашним каталогам пользователей:

  • ProtectHome=true скрывает /home, /root, /run/user.
  • ProtectHome=read-only позволяет читать, но не писать.
  • ProtectHome=tmpfs подменяет на пустую tmpfs (жёстче, иногда ломает софт).

Для серверных демонов почти всегда уместно ProtectHome=true.

ReadWritePaths/ReadOnlyPaths/InaccessiblePaths

Эти директивы — «скальпель», когда нужно разрешить минимум:

[Service]
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/yourservice
ReadWritePaths=/run/yourservice
ReadOnlyPaths=/etc/ssl
InaccessiblePaths=/srv

Совет: начинайте с минимального набора путей на запись. Если сервис падает, смотрите ошибки доступа в journald и добавляйте только конкретный путь, который действительно нужен.

PrivateTmp: отдельный /tmp для сервиса

PrivateTmp=yes даёт процессу отдельные /tmp и /var/tmp. Это снижает риск атак через «общий» tmp (подмена файлов, гонки, утечки данных между процессами) и часто убирает конфликты имён.

Типичная проблема: сервис ожидает обмениваться файлами через /tmp с другим процессом (например, внешним конвертером или агентом). Тогда нужно либо отключить PrivateTmp, либо перенести обмен в RuntimeDirectory (например, /run/yourservice) и дать обоим сервисам доступ через группы/права или сокеты.

NoNewPrivileges: запрет на повышение привилегий

NoNewPrivileges=yes запрещает процессу и дочерним процессам получать новые привилегии через setuid/setgid-бинарники и некоторые механизмы exec. Это почти всегда «бесплатная» защита для веб-воркеров, воркеров очередей и небольших API-демонов.

Где ломается: если приложение по дизайну запускает setuid-хелперы или требует привилегированного поведения при exec. Для типовых серверных демонов это скорее исключение.

CapabilityBoundingSet: оставляем только нужные capabilities

Linux capabilities позволяют разделить «root-права» на части. systemd даёт удобный способ «срезать» их у сервиса через CapabilityBoundingSet. Идея: даже если сервис запущен от root (или получил root из-за ошибки), ядро не даст ему выполнить часть привилегированных операций.

Типовой безопасный подход: обнулить набор и затем добавить только нужное. Пример для сервиса, которому не нужны привилегии:

[Service]
CapabilityBoundingSet=
NoNewPrivileges=yes

Если сервису нужно слушать порт ниже 1024, добавляют CAP_NET_BIND_SERVICE:

[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes

Пояснение: CapabilityBoundingSet задаёт «потолок», а AmbientCapabilities помогает передать capability в процесс без полного root.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

SystemCallFilter: режем поверхность атаки на уровне syscalls

SystemCallFilter — сильный инструмент, но внедрять его нужно аккуратно: неправильный фильтр ломает сервис «странно» и иногда только под нагрузкой.

Практичный путь внедрения:

  1. Сначала включите «мягкие» ограничения (ФС, tmp, привилегии).
  2. Затем добавляйте SystemCallFilter на основе реальных потребностей сервиса.
  3. Тестируйте функциональность, перезапуски, обработку сигналов и работу под нагрузкой.

Пример стартового фильтра

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

[Service]
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

Дальше можно точечно запрещать опасные классы, если уверены, что они не нужны:

[Service]
SystemCallFilter=@system-service
SystemCallFilter=~@mount
SystemCallFilter=~@swap
SystemCallFilter=~@reboot
SystemCallErrorNumber=EPERM

Если после включения фильтра сервис падает, смотрите journald: часто там видно, какой syscall был заблокирован. Альтернатива — временно ослабить фильтр, стабилизировать, затем снова «закручивать».

Практический шаблон: «закрытый» unit для простого демона

Представим сервис yourservice, который слушает TCP-порт, пишет состояние в /var/lib/yourservice и использует runtime-сокет в /run/yourservice. Пример drop-in (адаптируйте под ваш софт):

[Service]
DynamicUser=yes

StateDirectory=yourservice
RuntimeDirectory=yourservice

ProtectSystem=strict
ProtectHome=true
PrivateTmp=yes
NoNewPrivileges=yes

ReadWritePaths=/var/lib/yourservice
ReadWritePaths=/run/yourservice

CapabilityBoundingSet=
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictSUIDSGID=yes
RestrictNamespaces=yes
RestrictRealtime=yes

SystemCallFilter=@system-service
SystemCallFilter=~@mount
SystemCallFilter=~@reboot
SystemCallErrorNumber=EPERM

Почему здесь нет User=: при DynamicUser=yes systemd сам назначит пользователя.

Вывод systemd-analyze security и журнал journalctl для диагностики hardening

Диагностика и внедрение без сюрпризов

Начинайте с «малых шагов»

Если включить всё сразу, будет трудно понять, что именно сломало сервис. Рабочая последовательность (почти всегда):

  1. NoNewPrivileges, PrivateTmp, ProtectHome.
  2. DynamicUser плюс StateDirectory/RuntimeDirectory.
  3. ProtectSystem=strict и точечные ReadWritePaths.
  4. CapabilityBoundingSet (обнуление или минимум).
  5. SystemCallFilter (последним).

Проверяйте оценку security и фактические ошибки

systemd-analyze security не гарантирует безопасность, но хорошо подсвечивает очевидные «дыры» и помогает видеть прогресс:

systemd-analyze security yourservice.service

Фактические проблемы будут в логах:

journalctl -u yourservice.service -b --no-pager

Как понять, куда сервису нужна запись

Если вы включили ProtectSystem=strict и сервис перестал стартовать, ищите в журнале “Permission denied” и путь. Дальше обычно есть два безопасных варианта:

  • Перенастроить приложение на запись в StateDirectory/RuntimeDirectory.
  • Если приложение не настраивается — добавить конкретный путь в ReadWritePaths.

Старайтесь не «открывать» крупные деревья. Hardening выигрывает именно от минимизации.

Частые ошибки и типовые «ломается потому что…»

DynamicUser и запись в /etc

Некоторые приложения пытаются писать runtime-файлы в /etc (кэш, pid, «сгенерённый конфиг»). С ProtectSystem и DynamicUser это почти всегда плохо. Переносите такие файлы в /var/lib/… или /run/…, а /etc оставляйте только для статических конфигов.

PrivateTmp и интеграции

Если сервис взаимодействует с внешними процессами через файлы в /tmp, после включения PrivateTmp они окажутся в разных пространствах. Решение: общий каталог в /run или сокетный IPC.

CapabilityBoundingSet: «вроде всё сломалось»

При обнулении capabilities ломаются операции, которые раньше «случайно работали», потому что сервис запускался как root. Признаки:

  • не открывается привилегированный порт (ниже 1024);
  • не удаётся менять владельца/права файлов;
  • ошибки работы с raw-socket/ICMP, если сервис — мониторинг/пингер.

Решение: возвращайте только точечно необходимое (например, CAP_NET_BIND_SERVICE), либо выносите привилегированную часть в отдельный маленький сервис.

SystemCallFilter: «падает только под нагрузкой»

Часть syscalls используется только на отдельных путях кода: при ротации логов, при форке воркеров, при reload-конфигурации. Поэтому фильтры проверяйте не только стартом, но и типовыми админ-действиями: reload, graceful restart, пик нагрузок.

Мини-чеклист для production hardening

  • Сервис пишет только в StateDirectory/RuntimeDirectory и нигде больше.
  • ProtectSystem=strict включён; пути на запись минимальны и понятны.
  • ProtectHome=true включён, если сервису не нужен доступ к /home.
  • PrivateTmp=yes включён, если нет обмена через /tmp с другими процессами.
  • NoNewPrivileges=yes включён.
  • CapabilityBoundingSet либо пустой, либо содержит 1–2 необходимых capability.
  • SystemCallFilter добавлен и протестирован (или отложен, если риск простоя важнее).
  • Проверены systemd-analyze security и журналы journalctl.

Почему это особенно полезно на VDS

На виртуальном сервере часто живёт несколько сервисов рядом: веб, воркеры, агенты мониторинга, планировщики. systemd-hardening даёт «мягкую изоляцию» между ними: даже если один процесс скомпрометирован, ему сложнее прочитать чужие секреты и повлиять на систему.

Если вы поднимаете инфраструктуру на VDS, то hardening сервисов через systemd — один из самых быстрых способов снизить риск бокового перемещения без внедрения контейнеризации.

Контейнеры и MAC-системы (SELinux/AppArmor) тоже важны, но hardening на уровне systemd часто даёт заметный выигрыш за часы, а не за недели внедрения.

Что почитать дальше

Итог

DynamicUser помогает отказаться от постоянных системных аккаунтов для сервисов. В связке с ProtectSystem, ProtectHome и PrivateTmp вы сокращаете доступ к файловой системе и риск утечек. NoNewPrivileges и CapabilityBoundingSet режут эскалацию привилегий, а SystemCallFilter снижает поверхность атаки на уровне ядра. Если нужны нестандартные пути или правила очистки — подключайте tmpfiles.d, но сначала используйте встроенные директории systemd.

Поделиться статьей

Вам будет интересно

JWT security: JWKS, key rotation, clock skew и защита от alg=none OpenAI Статья написана AI (GPT 5)

JWT security: JWKS, key rotation, clock skew и защита от alg=none

JWT удобны в микросервисах, но ошибки валидации быстро превращают их в дыру. Разберём JWKS и kid, ротацию ключей без даунтайма, уч ...
Segfault в production на Linux: coredumpctl, gdb и debuginfo — разбор падений без паники OpenAI Статья написана AI (GPT 5)

Segfault в production на Linux: coredumpctl, gdb и debuginfo — разбор падений без паники

Segfault в проде — это не «рандом», а нехватка артефактов. Показываю пошагово: включить core dump в systemd, проверить core_patter ...
Git dubious ownership и safe.directory в Linux: как чинить в CI/CD и на VDS без дыр в безопасности OpenAI Статья написана AI (GPT 5)

Git dubious ownership и safe.directory в Linux: как чинить в CI/CD и на VDS без дыр в безопасности

Git dubious ownership появляется в CI/CD и на серверах деплоя после смены пользователя, запуска через sudo, Docker volume или shar ...