Уязвимость в веб-приложении не обязана превращаться в полный контроль над сервером. Даже если атакующий добился выполнения кода внутри процесса, этому процессу не обязательно видеть домашние каталоги, изменять /etc, читать устройства, загружать модули ядра или запускать произвольные системные вызовы. Systemd позволяет сузить такие возможности прямо в unit-файле, без упаковки сервиса в контейнер.
Однако набор директив, скопированный из чужого примера, легко останавливает production: приложение теряет доступ к сокету базы, каталогу загрузок, JIT-компилятору или служебной утилите. Поэтому systemd sandboxing следует внедрять как управляемое изменение: сначала описать необходимые права, затем добавлять ограничения слоями, наблюдать за отказами и сохранять быстрый путь отката.
Оглавление
- Что такое systemd sandboxing и от чего он защищает
- Модель угроз и карта доступа перед настройкой
- Проверка версии systemd и текущего unit-файла
- Drop-in вместо правки пакетного unit-файла
- Отдельный пользователь и управляемые каталоги
- Защита файловой системы
- Capabilities и запрет новых привилегий
- Изоляция ядра, устройств и namespaces
- Сеть и фильтрация системных вызовов
- Пример защищённого сервиса
- Поэтапное внедрение без неожиданного простоя
- Диагностика отказов после ужесточения
- Как читать systemd-analyze security
- Типичные ошибки при настройке
- Чек-лист для production
- Практический итог
Что такое systemd sandboxing и от чего он защищает
Systemd запускает сервис и формирует его окружение до передачи управления приложению. На этом этапе менеджер может изменить набор Linux capabilities, включить no_new_privs, создать отдельные mount-, network- или IPC-namespaces, сделать части файловой системы недоступными или только для чтения, закрыть устройства и установить seccomp-фильтр системных вызовов. Основной справочник по этим механизмам — systemd.exec.
Это не виртуальная машина и не абсолютная граница безопасности. Процесс остаётся на том же ядре, а эффективность конкретной директивы зависит от версии systemd, возможностей ядра и сохранённых привилегий. Например, ограничение mount namespace мало помогает процессу, который всё ещё может вернуть себе опасную CAP_SYS_ADMIN. Механизмы нужно комбинировать.
Практический смысл sandboxing — уменьшить последствия компрометации. Если уязвимый API должен читать конфигурацию, записывать данные только в /var/lib/demo-api и обращаться к PostgreSQL по TCP, ему не нужны домашние каталоги пользователей, блочные устройства, управление временем, модулями ядра и произвольными mount-операциями. Запрет лишнего превращает часть возможных действий атакующего в ошибки доступа.
Готовы перейти на современную серверную инфраструктуру?
В King Servers мы предлагаем серверы как на AMD EPYC, так и на Intel Xeon, с гибкими конфигурациями под любые задачи — от виртуализации и веб-хостинга до S3-хранилищ и кластеров хранения данных.
- S3-совместимое хранилище для резервных копий
- Панель управления, API, масштабируемость
- Поддержку 24/7 и помощь в выборе конфигурации
Результат регистрации
...
Создайте аккаунт
Быстрая регистрация для доступа к инфраструктуре
Модель угроз и карта доступа перед настройкой

Начинать с готового «максимально безопасного» блока рискованно. Сначала ответьте, что должен уметь штатный процесс и что не должен получить атакующий после выполнения кода. Для обычного сетевого сервиса полезно зафиксировать пять групп зависимостей.
- Файлы: какие пути читаются, куда пишутся данные, кэш, логи, PID-файлы и Unix-сокеты.
- Сеть: какие входящие сокеты слушает сервис, к каким адресам и протоколам обращается сам.
- Процессы: запускает ли дочерние бинарники, использует ли shell, JIT, отладчик или helper с setuid.
- Ядро и устройства: нужны ли FUSE, GPU, raw sockets, TUN/TAP, perf, eBPF, mount или изменение sysctl.
- IPC: используются ли D-Bus, shared memory, очереди сообщений и Unix-сокеты других служб.
Источником карты должны быть конфигурация приложения, документация, открытые файлы и сокеты процесса, журналы и тестовый трафик. Один спокойный час не доказывает полноту профиля: редкая ротация сертификата, экспорт отчёта или задача обслуживания может срабатывать раз в неделю. Включайте владельцев приложения в проверку нетипичных сценариев.
Минимальная карточка сервиса
Запишите владельца, unit, пользователя, listen-сокеты, исходящие зависимости, читаемые и записываемые пути, необходимые capabilities, внешние команды, health check, допустимое окно рестарта и команду отката. Такая карточка полезнее случайного security score: она связывает ограничение с реальной функцией.
Sandboxing дополняет, но не заменяет обновления, аутентификацию, контроль секретов, firewall и журналирование. Для общего уровня защиты пригодится security baseline нового VPS, а внешнюю доступность портов стоит сверить с материалом о минимальных правилах firewall.
Проверка версии systemd и текущего unit-файла
Набор директив развивается. Не предполагайте, что опция из свежей документации существует в дистрибутиве с длительной поддержкой. Перед изменением зафиксируйте версию и эффективную конфигурацию:
systemd --version
systemctl status demo-api.service
systemctl cat demo-api.service
systemctl show demo-api.service \
-p User -p Group -p MainPID -p FragmentPath -p DropInPaths
systemd-analyze security --no-pager demo-api.servicesystemctl cat показывает основной файл и подключённые drop-ins. Это важно: ограничение могло быть задано не там, где вы ожидаете, а пустое присваивание в другом drop-in могло сбросить список. Команда systemctl show выводит нормализованное состояние менеджера; её назначение и отличие от человекочитаемого status описаны в документации systemctl.
Сохраните вывод до изменения в системе управления конфигурацией или в тикете. Не включайте в него секреты из Environment и credentials. Для unit-файла полезен отдельный diff, а для сервиса — контрольный запрос, который проверяет не только открытый порт, но и реальную зависимость: базу, очередь или файловое хранилище.
Drop-in вместо правки пакетного unit-файла
Файл из /usr/lib/systemd/system или /lib/systemd/system принадлежит пакету и может измениться при обновлении. Локальные ограничения размещают в каталоге /etc/systemd/system/.d/. Удобный способ — systemctl edit demo-api.service: команда создаёт или редактирует drop-in и не требует копировать весь unit.
sudo systemctl edit demo-api.service
sudo systemctl daemon-reload
sudo systemd-analyze verify demo-api.service
sudo systemctl restart demo-api.serviceПеред редактированием проверьте, нет ли уже override.conf с чужими настройками. В инфраструктуре как код лучше дать своему файлу отдельное имя, например 50-sandbox.conf, и управлять именно им. Тогда откат не уничтожит настройки владельца приложения.
После удаления или перемещения своего drop-in выполните daemon-reload и перезапустите службу. Просто изменить файл недостаточно: работающий процесс уже получил namespaces, capabilities и seccomp-фильтр при запуске, поэтому новые ограничения применятся только к новому процессу.
Отдельный пользователь и управляемые каталоги
Самая важная граница — не запускать прикладной процесс от root без необходимости. Статический User=demo-api подходит сервису, который владеет долговечными данными. DynamicUser=yes полезен для службы без заранее созданного аккаунта, но динамические UID переиспользуются, поэтому произвольные оставленные файлы создают риск доступа другого процесса в будущем.
Systemd умеет создавать каталоги с корректным владельцем и жизненным циклом:
[Service]
User=demo-api
Group=demo-api
UMask=0077
RuntimeDirectory=demo-api
StateDirectory=demo-api
CacheDirectory=demo-api
LogsDirectory=demo-apiЭти директивы соответствуют путям под /run, /var/lib, /var/cache и /var/log. Они сокращают ручные операции с владельцами и делают список разрешённых мест записи явным. Для stateless-сервиса можно выбрать DynamicUser=yes вместе с управляемыми каталогами: документация systemd отмечает, что этот режим неявно включает несколько защитных параметров, включая NoNewPrivileges и строгую защиту файловой системы.
Не меняйте пользователя вслепую у службы, которая стартует от root, а затем сама сбрасывает права. Проверьте, выполняет ли она до этого привилегированные действия: привязывается к порту, читает закрытый ключ, открывает устройство, создаёт namespace или меняет владельца файлов. Иногда правильнее оставить небольшой привилегированный launcher либо использовать socket activation, а основной процесс запускать без root.
Защита файловой системы
ProtectSystem=strict делает файловую систему в mount namespace сервиса преимущественно доступной только для чтения. Запись затем разрешают точечно через управляемые каталоги или ReadWritePaths=. ProtectHome=yes закрывает /home, /root и /run/user, если приложению они не нужны.
[Service]
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/srv/demo-api/uploadsPrivateTmp=yes даёт службе отдельные /tmp и /var/tmp. Это уменьшает риск атак через общие временные файлы, но ломает интеграцию, если другой процесс ожидает увидеть созданный там файл. Для обмена лучше использовать явно выделенный каталог с понятными владельцем и правами.
| Директива | Что ограничивает | Что проверить |
|---|---|---|
ProtectSystem=strict | Запись в большую часть файловой системы | Загрузки, локальная БД, сертификаты, плагины, generated config |
ProtectHome=yes | Доступ к домашним каталогам | Ключи и конфиги, ошибочно лежащие в home пользователя |
PrivateTmp=yes | Общий namespace временных файлов | Обмен файлами с cron, конвертером или другим сервисом |
ReadOnlyPaths= | Запись в выбранные пути | Ротация и автоматическое обновление содержимого |
InaccessiblePaths= | Любой доступ к выбранным путям | Символические ссылки и косвенные зависимости библиотек |
Не разрешайте запись в широкий родительский каталог ради одной ошибки. Если приложению нужен /srv/demo-api/uploads, открывайте именно его, а не весь /srv. И помните: уже открытый файловый дескриптор или переданный через Unix-сокет дескриптор может дать доступ, который не очевиден из списка путей.
Capabilities и запрет новых привилегий
Linux делит полномочия root на capabilities. CapabilityBoundingSet= задаёт верхнюю границу capabilities, которые процесс и его потомки могут получить. Пустое значение удаляет их все; это хороший вариант для обычного API, работающего на непривилегированном порту и не управляющего системой.
[Service]
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=yesNoNewPrivileges=yes устанавливает одноимённый флаг ядра. После этого execve() не должен дать процессу права, которых у него не было: setuid/setgid-биты не повысят UID/GID, а file capabilities не расширят permitted set. Флаг наследуется потомками и не снимается. Эти свойства зафиксированы в документации ядра Linux.
Сам по себе NoNewPrivileges не отбирает уже имеющиеся полномочия и не запрещает все способы изменения идентичности. Поэтому его сочетают с непривилегированным User=, ограниченным bounding set и другими слоями. Подробный смысл отдельных полномочий, включая широкую CAP_SYS_ADMIN, приведён в capabilities(7).
Не выдавайте CAP_SYS_ADMIN «на всякий случай»
Эта capability охватывает множество операций и способна обесценить часть namespace-ограничений. Если сервису нужна одна конкретная функция, ищите более узкую capability или меняйте архитектуру: отдельный helper, socket activation, подготовленный ресурс либо действие вне процесса приложения.
Если приложение действительно слушает привилегированный порт, возможна точечная конфигурация:
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICEНо сначала проверьте, нельзя ли слушать порт выше 1023 за reverse proxy или получить готовый socket от systemd. Чем меньше полномочий у кода приложения, тем проще оценивать последствия его компрометации.
Изоляция ядра, устройств и namespaces

Большинству бизнес-приложений не нужно менять sysctl, загружать модули, читать kernel log, управлять cgroups или видеть все устройства. Для такого профиля уместен следующий слой:
[Service]
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
LockPersonality=yes
RestrictRealtime=yesPrivateDevices=yes создаёт для процесса минимальный /dev и дополнительно влияет на capabilities и device policy. Он несовместим с сервисами, которым нужны GPU, диски, TUN/TAP, FUSE, аппаратные ключи или другие реальные устройства. В таких случаях не отключайте всю защиту автоматически: рассмотрите DevicePolicy=closed и точечный DeviceAllow=, предварительно проверив поддержку на хосте.
ProtectKernelTunables, ProtectKernelModules и ProtectKernelLogs закрывают разные поверхности. Первое не даёт менять параметры ядра, второе блокирует загрузку модулей, третье ограничивает доступ к ring buffer. Агент диагностики или низкоуровневый security sensor может законно нуждаться в части этих возможностей, обычный HTTP API — почти никогда.
PrivateNetwork=yes создаёт отдельный network namespace только с loopback. Это сильная мера для локального worker без сети, но она отключит и входящие соединения, и доступ к внешней БД, DNS, metadata endpoints и API. Для сетевого приложения полезнее ограничить адресные семейства и firewall, чем бездумно включать PrivateNetwork.
Сеть и фильтрация системных вызовов
RestrictAddressFamilies= определяет, какие семейства сокетов может создавать процесс. Для типичного веб-сервиса часто достаточно Unix-сокетов, IPv4 и IPv6:
[Service]
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6Это не ACL по адресам и портам. Директива не заменяет firewall и не описывает, к какому IP разрешено подключаться. Она сокращает набор интерфейсов ядра: например, процессу без необходимости не требуется AF_PACKET для низкоуровневой работы с пакетами или AF_NETLINK для взаимодействия с подсистемами ядра.
Следующий уровень — seccomp через SystemCallFilter=. Allow-list задаёт разрешённые вызовы, deny-list с префиксом ~ запрещает выбранные. В systemd есть группы вроде @system-service, но их фактический состав зависит от версии. Начинать с короткого собственного allow-list опасно: стандартная библиотека, DNS resolver, сборщик мусора или обновление runtime могут использовать неожиданный syscall.
[Service]
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERMФильтр systemd использует seccomp mode 2. Ядро описывает его как механизм сокращения доступной процессу поверхности системных вызовов; при этом фильтры наследуются потомками, если разрешены fork/clone и execve. Подробности и ограничения приведены в документации seccomp.
JIT и MemoryDenyWriteExecute
MemoryDenyWriteExecute=yes полезен против создания исполняемой памяти, но часто несовместим с Java, JavaScript/V8, .NET, eBPF-компиляцией и другими JIT-нагрузками. Проверяйте реальный runtime. Не отключайте JIT или защиту в production только ради красивого score без нагрузочного и функционального теста.
Пример защищённого сервиса
Ниже — отправная точка для условного API, которое работает от отдельного пользователя, слушает порт 8080, читает конфигурацию из /etc/demo-api, хранит состояние в /var/lib/demo-api и использует обычные TCP/Unix-сокеты. Это не универсальный шаблон: каждую строку необходимо сверить с картой доступа.
[Service]
User=demo-api
Group=demo-api
UMask=0077
RuntimeDirectory=demo-api
StateDirectory=demo-api
CacheDirectory=demo-api
LogsDirectory=demo-api
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictNamespaces=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERMRestrictNamespaces=yes подходит приложению, которое само не создаёт namespaces. Контейнерный runtime, sandbox браузера, некоторые build-системы и инструменты изоляции могут на этом сломаться. SystemCallFilter=@system-service также следует включать отдельным этапом, а не одновременно со всеми файловыми ограничениями: иначе источник ошибки будет трудно локализовать.
В примере нет ReadWritePaths, потому что запись идёт через каталоги, созданные StateDirectory, CacheDirectory, LogsDirectory и RuntimeDirectory. Если данные живут в собственном пути, добавьте минимальный разрешённый каталог и заранее создайте его с корректным владельцем.
Поэтапное внедрение без неожиданного простоя
Ограничения лучше раскладывать на небольшие группы. Тогда после отказа понятен кандидат, а rollback затрагивает только один слой.
- Наблюдение: снимите baseline, выполните smoke-тест и сохраните текущий security report.
- Идентичность: проверьте отдельного пользователя, UMask и управляемые каталоги.
- Файловая система: включите
PrivateTmp, затемProtectHomeиProtectSystemс узкими исключениями. - Привилегии: уберите capabilities, включите
NoNewPrivilegesиRestrictSUIDSGID. - Ядро и устройства: добавьте Protect-директивы,
PrivateDevices, ограничения namespaces и personality. - Сеть и syscalls: ограничьте address families и последним этапом примените seccomp-профиль.
На нескольких экземплярах сначала примените drop-in к canary-узлу, убедитесь в прохождении бизнес-операций и только затем расширяйте изменение. На одиночном VPS заранее определите окно, health check и команду возврата. План действий на случай неудачного рестарта стоит добавить в серверный runbook.
Диагностика отказов после ужесточения
Сначала отделите синтаксическую ошибку от запрета во время выполнения:
sudo systemd-analyze verify demo-api.service
sudo systemctl daemon-reload
sudo systemctl restart demo-api.service
systemctl status --no-pager demo-api.service
journalctl -u demo-api.service -b --since "10 minutes ago" --no-pagerСообщения Permission denied, Read-only file system, Operation not permitted и Bad system call указывают на разные группы ограничений. Read-only обычно ведёт к ProtectSystem и отсутствующему writable path. SIGSYS или Bad system call — к seccomp. Недоступное устройство — к PrivateDevices или device policy. Ошибка создания сокета может быть связана с RestrictAddressFamilies или PrivateNetwork.
Для seccomp полезен audit log, если аудит включён в дистрибутиве. Но не стройте диагностику только на нём: формат и доступность сообщений различаются. Воспроизведите конкретную операцию, временно снимите последний добавленный слой, подтвердите причинность и затем сформулируйте минимальное исключение.
Не оставляйте сервис «починенным» путём полного удаления sandbox-блока. Сначала верните доступность, затем в том же тикете уточните карту зависимостей и восстановите остальные рабочие ограничения. Иначе временный rollback незаметно становится постоянным.
Как читать systemd-analyze security

systemd-analyze security demo-api.service оценивает настройки sandboxing, реализованные самим systemd, и выводит общий exposure level от 0.0 до 10.0. Низкое значение означает более строгие ограничения, высокое — малое число применённых защит. Так определяет метрику документация systemd-analyze.
Это инструмент поиска пробелов, а не сканер уязвимостей и не KPI. Он не знает, что приложение само применяет seccomp, работает в контейнере с отдельным профилем, ограничено SELinux/AppArmor или имеет строгую D-Bus policy. Высокий exposure не доказывает уязвимость, а низкий не подтверждает безопасность бизнес-логики.
У score есть ещё одна ловушка: служба резервного копирования, сетевой агент или container runtime по назначению требуют более широких прав, чем API. Нельзя одинаково оценивать их по одному числу. Сравнивайте сервис с собственной моделью угроз и отслеживайте объяснимую динамику после изменений.
systemd-analyze security --no-pager demo-api.service
systemd-analyze security --json=pretty demo-api.serviceОпция JSON зависит от версии, поэтому перед автоматизацией проверьте локальную справку. Для CI можно контролировать обязательные директивы отдельно и использовать exposure как дополнительный сигнал, а не блокировать выпуск из-за произвольного порога.
Типичные ошибки при настройке
Копирование максимального профиля без модели доступа
Список из десятков директив выглядит убедительно, но не показывает, почему разрешён конкретный путь или syscall. После первого сбоя команда снимает весь блок. Профиль, построенный от минимальной карточки сервиса, проще поддерживать и проверять при обновлениях.
Одновременное включение всех ограничений
Если сервис перестал стартовать после 25 новых строк, причина неочевидна. Разделяйте файловую систему, privileges, devices/namespaces и seccomp на этапы. Для каждого этапа нужен отдельный smoke-тест.
Правка vendor unit
Обновление пакета может перезаписать изменение или создать конфликт. Drop-in отделяет локальную политику от поставляемого файла и виден через systemctl cat.
Слишком широкое исключение
Разрешить запись во весь /var ради одного каталога — почти отменить смысл ProtectSystem. Используйте StateDirectory и точные пути, документируйте владельца данных.
Погоня за минимальным exposure
Отключённый JIT, сломанный backup или потерянная телеметрия не делают систему безопаснее. Каждое принятое исключение должно быть связано с функцией, а остаточный риск — понятен владельцу.
Игнорирование обновлений
Новая версия приложения может открыть файл в другом каталоге или использовать новый syscall. Включите sandbox-проверки в процедуру обновления и наблюдайте canary после смены runtime. Это часть управления внешней поверхностью наряду с инвентаризацией, описанной в материале про Attack Surface Management.
Чек-лист для production
Раскрыть проверку перед внедрением
- У unit есть технический владелец, health check и проверенный rollback.
- Зафиксированы версия systemd, основной unit и все действующие drop-ins.
- Описаны пути чтения/записи, сокеты, внешние команды, devices, IPC и capabilities.
- Изменения находятся в отдельном локальном drop-in, а не в vendor unit.
- Синтаксис проверен через
systemd-analyze verify. - Сервис работает не от root либо необходимость root документирована.
- Writable paths минимальны и по возможности созданы через Directory-директивы.
- Capabilities сокращены до фактически необходимых.
- Filesystem, devices, kernel, network и syscalls включаются отдельными этапами.
- Протестированы редкие операции: ротация, backup, обновление сертификата, экспорт, worker-задачи.
- После рестарта проверены статус, journal, метрики, зависимости и бизнес-запрос.
- Отчёт
systemd-analyze securityинтерпретирован вместе с моделью угроз, а не как сертификат безопасности.
Для повторяемой инфраструктуры храните drop-in рядом с ролью Ansible, модулем конфигурации или пакетом приложения. Тест должен проверять не только наличие строк, но и эффективную конфигурацию на целевом дистрибутиве. Если директива не поддерживается, deployment обязан сообщить об этом, а не молча считать защиту включённой.
Практический итог

Systemd sandboxing даёт полезный промежуточный рубеж: приложение остаётся обычной службой Linux, но получает только необходимые файлы, сокеты, устройства, capabilities и системные вызовы. Наибольший эффект дают не редкие сложные параметры, а последовательное сочетание отдельного пользователя, запрета новых привилегий, read-only файловой системы, минимальных writable paths и ограничений ядра.
Начните с одного некритичного сервиса и одного слоя за изменение. Снимите baseline, создайте отдельный drop-in, проверьте canary, задокументируйте исключения и добавьте повторную проверку в процесс обновления. Для рабочей нагрузки на VPS или выделенном сервере KingServers такой подход помогает локализовать последствия ошибки приложения, не подменяя обновления, firewall, резервное копирование и мониторинг.