Защита сервисов systemd: песочницы, изоляция и практический hardening Linux

Пошаговое руководство по усилению безопасности сервисов systemd: от аудита с systemd-analyze security до фильтрации системных вызовов, управления секретами через systemd-creds и автоматизации проверок в CI/CD.

Защита сервисов systemd: песочницы, изоляция и практический hardening Linux

Если вы когда-нибудь запускали systemd-analyze security на своём сервере и видели столбец из красных UNSAFE-оценок — вы знаете это чувство. Казалось бы, systemd — просто система инициализации, менеджер сервисов. Но на самом деле это мощная платформа безопасности с более чем 60 директивами для изоляции и защиты процессов. И большинство из них по умолчанию выключены.

В этом руководстве мы разберём все ключевые механизмы усиления безопасности (hardening) сервисов systemd — от аудита текущего состояния до фильтрации системных вызовов и автоматизации проверок в CI/CD. Без сторонних инструментов, только встроенные средства systemd.

Почему усиление безопасности сервисов systemd критически важно

По данным отчётов по кибербезопасности за 2024–2025 годы, более 60% успешных атак на Linux-серверы начинаются с эксплуатации уязвимостей в запущенных сервисах. Веб-серверы, базы данных, очереди сообщений, API-шлюзы — все они работают как systemd-сервисы и, честно говоря, по умолчанию часто имеют избыточные привилегии.

Скомпрометированный процесс nginx или PostgreSQL без надлежащей изоляции получает доступ ко всей файловой системе, сетевому стеку, устройствам и другим процессам. Звучит страшно? Так и есть.

Поверхность атаки типичного сервиса включает:

  • Файловую систему — доступ к конфигурационным файлам, ключам SSH, домашним каталогам пользователей
  • Сетевой стек — возможность открывать произвольные соединения, сканировать внутреннюю сеть
  • Системные вызовы — доступ к вызовам ядра, не нужным для работы сервиса (mount, ptrace, reboot)
  • Привилегии и capabilities — избыточные Linux-capabilities, позволяющие повышение привилегий
  • Пространства имён — отсутствие изоляции процессов, пользователей и IPC
  • Ресурсы — возможность исчерпания CPU, памяти и числа процессов (DoS-атаки)

Хорошая новость — systemd предоставляет более 60 директив безопасности, которые можно применять декларативно прямо в файлах юнитов. Это делает hardening доступным, воспроизводимым и легко поддающимся аудиту. Давайте разберём каждый аспект.

systemd-analyze security — аудит безопасности сервисов

Прежде чем что-то усиливать, нужно понять, с чем мы имеем дело. Утилита systemd-analyze security — ваш главный инструмент аудита.

Получение общей оценки всех сервисов

Запустите команду без аргументов, чтобы увидеть оценки всех активных сервисов:

systemd-analyze security

Вывод — таблица с именем юнита, оценкой экспозиции от 0 до 10 и общим вердиктом. Чем ниже оценка — тем лучше:

UNIT                      EXPOSURE PREDICATE  HAPPY
nginx.service             9.2      UNSAFE     😨
postgresql.service        9.6      UNSAFE     😨
sshd.service              9.6      UNSAFE     😨
systemd-resolved.service  2.1      OK         😀
systemd-journald.service  1.5      OK         😀

Обратите внимание: встроенные сервисы systemd уже неплохо защищены, а вот пользовательские — почти всегда в красной зоне. Вот как интерпретировать шкалу:

  • 0.0–2.0 (OK) — сервис хорошо защищён, минимальная поверхность атаки
  • 2.1–4.9 (MEDIUM) — средний уровень, есть что улучшить
  • 5.0–7.4 (EXPOSED) — значительная поверхность атаки
  • 7.5–10.0 (UNSAFE) — сервис практически не защищён, нужно действовать

Детальный анализ конкретного сервиса

Для подробного отчёта по конкретному сервису:

systemd-analyze security nginx.service

Вывод покажет список всех проверяемых параметров — что включено, что нет:

  NAME                            DESCRIPTION                          EXPOSURE
✓ PrivateNetwork=               Service has access to the host's network    0.5
✗ PrivateTmp=                   Service has access to temporary files       0.1
✗ PrivateDevices=               Service has access to hardware devices      0.2
✗ ProtectHome=                  Service has access to home directories      0.2
✗ ProtectSystem=                Service has full access to the OS tree      0.2
✗ NoNewPrivileges=              Service processes may acquire new priv.     0.2
...

По сути, это готовая дорожная карта: каждый крестик — возможность для улучшения. Начинайте с параметров, которые дают наибольшее снижение оценки экспозиции.

Экспорт в JSON для автоматизации

Для интеграции с мониторингом и CI/CD пригодится JSON-вывод:

systemd-analyze security --json=pretty nginx.service > nginx-security.json

Изоляция файловой системы

Одно из важнейших направлений hardening — ограничить доступ сервиса к файловой системе. По умолчанию любой сервис видит всю ФС. Systemd позволяет создать для него изолированное представление.

ProtectSystem — защита системных каталогов

Директива ProtectSystem делает системные каталоги доступными только для чтения (или полностью недоступными):

[Service]
# Монтирует /usr, /boot, /efi как read-only
ProtectSystem=yes

# Дополнительно делает /etc read-only
ProtectSystem=full

# /usr, /boot, /efi, /etc — read-only, остальное — tmpfs
ProtectSystem=strict

Для большинства сервисов рекомендуется ProtectSystem=strict — он монтирует всю файловую систему как read-only, кроме /dev, /proc и /sys. Если сервису нужно куда-то писать, укажите это явно через ReadWritePaths.

ProtectHome — защита домашних каталогов

[Service]
# Делает /home, /root, /run/user недоступными
ProtectHome=yes

# Монтирует пустые tmpfs вместо домашних каталогов
ProtectHome=tmpfs

# Делает read-only
ProtectHome=read-only

Подавляющее большинство сервисов не нуждаются в доступе к домашним каталогам. Установка ProtectHome=yes — одна из первых вещей, которые стоит сделать.

PrivateTmp — изоляция временных файлов

[Service]
PrivateTmp=yes

Создаёт изолированное пространство для /tmp и /var/tmp, видимое только данному сервису. Предотвращает symlink-атаки и race condition через временные файлы — когда один процесс пытается подменить временный файл другого. Простая директива, но закрывает целый класс уязвимостей.

ReadWritePaths, ReadOnlyPaths, InaccessiblePaths

При использовании ProtectSystem=strict нужно явно указать, куда сервис может писать:

[Service]
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /var/log/myapp /run/myapp
ReadOnlyPaths=/etc/myapp
InaccessiblePaths=/etc/ssh /root/.ssh /var/lib/other-app

InaccessiblePaths делает указанные пути полностью невидимыми для сервиса — любая попытка доступа вернёт EACCES. Удобно для скрытия конфиденциальных файлов вроде SSH-ключей.

TemporaryFileSystem — создание пустых tmpfs

[Service]
TemporaryFileSystem=/var:ro
ReadWritePaths=/var/lib/myapp /var/log/myapp

Монтирует пустую tmpfs поверх указанного каталога. В связке с ReadWritePaths получается белый список для файловой системы: весь /var пуст и read-only, но конкретные каталоги приложения доступны.

BindPaths и BindReadOnlyPaths

[Service]
BindPaths=/srv/data:/app/data
BindReadOnlyPaths=/etc/ssl/certs:/app/certs

Bind-монтирования для случаев, когда приложение ожидает файлы по определённым путям. Ничего сложного, но бывает незаменимо.

Изоляция процессов и пространств имён

Linux namespaces — мощнейший механизм изоляции, и systemd позволяет включать их буквально одной строчкой в конфиге.

PrivateUsers — изоляция пользователей

[Service]
PrivateUsers=yes

Создаёт отдельное пространство имён пользователей (user namespace). Сервис может считать себя root внутри, но реальных привилегий root у него нет. Все UID/GID за пределами namespace маппятся на nobody. Существенно усложняет эскалацию привилегий.

PrivateDevices — изоляция устройств

[Service]
PrivateDevices=yes

Создаёт минимальный /dev с базовыми псевдо-устройствами (/dev/null, /dev/zero, /dev/random, /dev/urandom). Реальные устройства (диски, USB, GPU) — недоступны. Также автоматически устанавливает DevicePolicy=closed.

PrivateIPC — изоляция межпроцессного взаимодействия

[Service]
PrivateIPC=yes

Изолирует разделяемую память, семафоры и очереди сообщений (System V и POSIX). Предотвращает атаки через IPC между разными сервисами.

PrivateNetwork — полная сетевая изоляция

[Service]
PrivateNetwork=yes

Создаёт изолированное сетевое пространство с единственным loopback. Сервис не может ни принимать, ни отправлять сетевые соединения. Идеально для сервисов, работающих только с файлами или через Unix-сокеты. Для веб-серверов и БД, конечно, не подходит.

ProtectProc и ProcSubset — ограничение видимости процессов

[Service]
# Скрывает процессы других пользователей в /proc
ProtectProc=invisible

# Или полный запрет доступа к чужим процессам
ProtectProc=noaccess

# Ограничивает доступные данные в /proc
ProcSubset=pid

ProtectProc=invisible скрывает информацию о чужих процессах — сервис видит только свои. А ProcSubset=pid дополнительно урезает объём информации в /proc.

ProtectKernelTunables, ProtectKernelModules, ProtectKernelLogs

[Service]
# Запрещает изменение параметров ядра через /proc/sys, /sys
ProtectKernelTunables=yes

# Запрещает загрузку/выгрузку модулей ядра
ProtectKernelModules=yes

# Ограничивает доступ к журналу ядра (dmesg)
ProtectKernelLogs=yes

Три директивы, которые стоит включать всегда. Вместе они предотвращают изменение поведения ядра, загрузку вредоносных модулей и утечку информации через dmesg.

ProtectControlGroups и ProtectClock

[Service]
# Запрещает модификацию иерархии cgroups
ProtectControlGroups=yes

# Запрещает изменение системных часов
ProtectClock=yes

Ещё две простые, но полезные директивы. Включайте не задумываясь — подавляющему большинству сервисов они не помешают.

Управление capabilities

Linux capabilities — это способ разделить привилегии root на отдельные «кусочки». Вместо полного root-доступа сервису можно выдать только то, что ему реально нужно.

CapabilityBoundingSet — ограничение набора capabilities

[Service]
# Разрешаем только привязку к привилегированным портам
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# Для сервиса, которому нужно менять владельца файлов
CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER

# Полный запрет всех capabilities
CapabilityBoundingSet=

Хороший подход — начинать с пустого набора и добавлять только необходимое:

[Service]
# Начинаем с нуля, добавляем только нужное
CapabilityBoundingSet=
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

Часто используемые capabilities:

  • CAP_NET_BIND_SERVICE — привязка к портам ниже 1024
  • CAP_DAC_READ_SEARCH — обход проверок прав чтения файлов
  • CAP_SYS_CHROOT — использование chroot
  • CAP_SETUID / CAP_SETGID — смена UID/GID процесса
  • CAP_CHOWN — изменение владельца файлов

AmbientCapabilities — наследуемые capabilities

[Service]
User=www-data
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

Позволяет запускать сервис от непривилегированного пользователя, сохраняя нужные capabilities. Например, nginx от www-data с возможностью слушать порт 80 — без запуска от root.

NoNewPrivileges — запрет получения новых привилегий

[Service]
NoNewPrivileges=yes

Это, пожалуй, одна из самых важных директив. Устанавливает флаг ядра PR_SET_NO_NEW_PRIVS, гарантирующий, что процесс и все его потомки не смогут получить новые привилегии через execve() — включая setuid/setgid-бинарники. Включайте для всех сервисов без исключения.

Фильтрация системных вызовов

Seccomp (Secure Computing Mode) — один из самых мощных инструментов в арсенале Linux-безопасности. И systemd делает его настройку удивительно простой.

SystemCallFilter — белый и чёрный списки вызовов

[Service]
# Белый список: разрешаем только перечисленные группы
SystemCallFilter=@system-service

# Чёрный список: запрещаем опасные группы
SystemCallFilter=~@mount @clock @debug @module @raw-io @reboot @swap @obsolete @cpu-emulation

Systemd предоставляет предопределённые группы системных вызовов — это сильно упрощает настройку:

  • @system-service — минимальный набор для типичного сервиса (хороший базовый белый список)
  • @network-io — сетевые вызовы (socket, connect, accept и др.)
  • @file-system — операции с файловой системой
  • @process — управление процессами (fork, exec и др.)
  • @mount — монтирование файловых систем
  • @clock — изменение системного времени
  • @debug — отладочные вызовы (ptrace и др.)
  • @module — загрузка модулей ядра
  • @raw-io — прямой ввод/вывод
  • @reboot — перезагрузка и выключение
  • @swap — управление swap
  • @privileged — привилегированные операции
  • @obsolete — устаревшие вызовы

Полный список групп и входящих в них вызовов:

systemd-analyze syscall-filter

SystemCallArchitectures — ограничение архитектур

[Service]
SystemCallArchitectures=native

Разрешает только вызовы для нативной архитектуры. На 64-битной системе блокирует 32-битные вызовы, предотвращая атаки через ABI-несоответствия. Простая строчка — серьёзная защита.

SystemCallErrorNumber — реакция на запрещённые вызовы

[Service]
# Вернуть ошибку вместо убийства процесса
SystemCallErrorNumber=EPERM

# Для вызовов, которые точно не нужны — убить процесс
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot

По умолчанию запрещённый вызов убивает процесс сигналом SIGSYS. Установка SystemCallErrorNumber=EPERM вместо этого просто возвращает ошибку, давая приложению шанс обработать ситуацию. Особенно полезно на этапе тестирования, когда белый список ещё не отлажен.

Как это работает внутри

Под капотом SystemCallFilter использует seccomp BPF-фильтры ядра. При каждом системном вызове ядро прогоняет его через BPF-программу и решает — разрешить, вернуть ошибку или завершить процесс. Проверить, активен ли seccomp:

# Проверка наличия seccomp у процесса
grep Seccomp /proc/$(pidof nginx)/status
# Вывод: Seccomp:  2  (означает seccomp BPF активен)

# Трассировка seccomp через strace
strace -f -e trace=seccomp -p $(pidof nginx)

Ограничение ресурсов и контроль cgroups

Помимо изоляции, systemd позволяет лимитировать потребление ресурсов через cgroups v2. Это защита от DoS — ситуаций, когда один сервис съедает все ресурсы системы.

Ограничение памяти

[Service]
# Жёсткий лимит памяти (OOM killer при превышении)
MemoryMax=512M

# Мягкий лимит (приоритет при нехватке)
MemoryHigh=384M

# Минимальная гарантированная память
MemoryMin=64M

# Лимит для swap
MemorySwapMax=0

MemoryMax — жёсткий потолок: превысил — OOM killer прибьёт процесс. MemoryHigh — мягкий лимит: ядро начнёт агрессивно рекламировать страницы, замедляя сервис, но не убивая. MemorySwapMax=0 запрещает swap, что критично для сервисов с требованиями к latency.

Ограничение CPU

[Service]
# Квота CPU: 50% одного ядра
CPUQuota=50%

# Ограничение набора доступных ядер
AllowedCPUs=0-1

# Приоритет CPU (вес от 1 до 10000, по умолчанию 100)
CPUWeight=50

Ограничение числа процессов и задач

[Service]
# Максимальное число задач (процессов и потоков)
TasksMax=64

# Ограничение через RLIMIT
LimitNPROC=32

# Ограничение числа открытых файлов
LimitNOFILE=1024

# Запрет core dump
LimitCORE=0

TasksMax ограничивает общее число процессов и потоков в cgroup. Защита от fork-бомб и утечек потоков. Устанавливайте разумное значение исходя из ожидаемой нагрузки.

Ограничение ввода/вывода

[Service]
# Вес IO (1-10000, по умолчанию 100)
IOWeight=50

# Ограничение скорости чтения
IOReadBandwidthMax=/dev/sda 10M

# Ограничение скорости записи
IOWriteBandwidthMax=/dev/sda 5M

# Максимальное число IOPS
IOReadIOPSMax=/dev/sda 1000
IOWriteIOPSMax=/dev/sda 500

DynamicUser и управление секретами

Systemd предлагает современные механизмы для управления идентичностью и секретами. Это, пожалуй, одни из самых недооценённых возможностей.

DynamicUser — динамические пользователи

[Service]
DynamicUser=yes
StateDirectory=myapp
CacheDirectory=myapp
LogsDirectory=myapp

DynamicUser=yes создаёт временного пользователя и группу при каждом запуске сервиса. Пользователь появляется при старте, исчезает при остановке. Больше не нужно создавать системных пользователей при установке пакетов, и нет риска эксплуатации «забытых» учётных записей.

Бонус: при включении DynamicUser=yes автоматически активируются:

  • ProtectSystem=strict
  • ProtectHome=read-only
  • PrivateTmp=yes

Для хранения данных используйте StateDirectory, CacheDirectory и LogsDirectory — они автоматически создают каталоги с правильными правами:

[Service]
DynamicUser=yes
StateDirectory=myapp
# Создаёт /var/lib/myapp с владельцем — динамическим пользователем
# При перезапуске доступ восстанавливается для нового пользователя

systemd-creds — безопасное управление секретами

Начиная с systemd 250, утилита systemd-creds позволяет безопасно хранить и передавать секреты сервисам. Секреты шифруются и расшифровываются только в момент запуска.

# Шифрование секрета для конкретного сервиса
systemd-creds encrypt --name=db-password - /etc/credstore.encrypted/myapp-db-password
# Введите пароль и нажмите Ctrl+D

# Шифрование из файла
systemd-creds encrypt --name=api-key /path/to/api-key.txt \
    /etc/credstore.encrypted/myapp-api-key

В юните подключаем через LoadCredentialEncrypted:

[Service]
LoadCredentialEncrypted=db-password:/etc/credstore.encrypted/myapp-db-password
LoadCredentialEncrypted=api-key:/etc/credstore.encrypted/myapp-api-key

Внутри сервиса секреты доступны как файлы:

# В скрипте или конфигурации приложения
DB_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/db-password")
API_KEY=$(cat "$CREDENTIALS_DIRECTORY/api-key")

Интеграция с TPM2

Для максимальной защиты systemd-creds может использовать TPM2 (Trusted Platform Module), привязывая секреты к конкретному железу:

# Шифрование с привязкой к TPM2
systemd-creds encrypt --with-key=tpm2 \
    --name=secret - /etc/credstore.encrypted/myapp-secret

# Шифрование с комбинацией TPM2 и host-ключа
systemd-creds encrypt --with-key=tpm2+host \
    --name=secret - /etc/credstore.encrypted/myapp-secret

# Проверка поддержки TPM2
systemd-creds has-tpm2

С TPM2 секреты можно расшифровать только на конкретном физическом сервере. Даже если злоумышленник скопирует зашифрованные файлы на другую машину — расшифровать не получится.

SetCredential — встроенные значения

[Service]
# Для нечувствительных конфигурационных значений
SetCredential=config-mode:production
SetCredential=log-level:warn

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

Практическое руководство: пошаговый hardening

Теория — это хорошо, но давайте посмотрим, как всё работает на практике.

Усиление безопасности nginx

Начнём с аудита:

systemd-analyze security nginx.service
# Результат: OVERALL EXPOSURE LEVEL: 9.2 UNSAFE

9.2 из 10 — не самый приятный результат. Создаём override-файл:

systemctl edit nginx.service

Вот полный набор директив для усиления:

[Service]
# === Изоляция файловой системы ===
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/log/nginx /var/cache/nginx /run/nginx
InaccessiblePaths=/etc/ssh /root

# === Пространства имён ===
PrivateDevices=yes
PrivateIPC=yes
ProtectProc=invisible
ProcSubset=pid
ProtectHostname=yes

# === Защита ядра ===
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes

# === Capabilities ===
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH CAP_SETUID CAP_SETGID
AmbientCapabilities=
NoNewPrivileges=yes

# === Системные вызовы ===
SystemCallFilter=@system-service
SystemCallFilter=~@mount @clock @debug @module @raw-io @reboot @swap @obsolete @cpu-emulation
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

# === Ограничение ресурсов ===
MemoryMax=512M
TasksMax=256
LimitNOFILE=65536
LimitNPROC=64

# === Дополнительные ограничения ===
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
UMask=0077

# === Сетевая безопасность ===
IPAddressDeny=any
IPAddressAllow=0.0.0.0/0 ::/0

Перезапускаем и проверяем:

systemctl daemon-reload
systemctl restart nginx.service
systemd-analyze security nginx.service
# Результат: OVERALL EXPOSURE LEVEL: 2.8 OK

С 9.2 до 2.8 — из UNSAFE в OK. Неплохо, правда? Но обязательно убедитесь, что nginx продолжает работать:

# Проверка работоспособности
curl -I http://localhost
systemctl status nginx.service
journalctl -u nginx.service --no-pager -n 20

Усиление пользовательского приложения

Возьмём типичный API-сервис на Python, Go или Node.js, слушающий порт 8080. Вот как обычно выглядит его юнит:

[Unit]
Description=My Custom API Service
After=network.target

[Service]
Type=simple
ExecStart=/opt/myapp/bin/api-server --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Оценка: 9.6 UNSAFE. А вот усиленная версия:

[Unit]
Description=My Custom API Service
After=network.target
Documentation=https://internal.docs/myapp

[Service]
Type=simple
ExecStart=/opt/myapp/bin/api-server --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5

# === Идентичность ===
DynamicUser=yes
StateDirectory=myapp
CacheDirectory=myapp
LogsDirectory=myapp

# === Секреты ===
LoadCredentialEncrypted=db-password:/etc/credstore.encrypted/myapp-db-password
LoadCredentialEncrypted=jwt-secret:/etc/credstore.encrypted/myapp-jwt-secret

# === Изоляция файловой системы ===
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadOnlyPaths=/opt/myapp /etc/myapp
InaccessiblePaths=/etc/ssh /boot
TemporaryFileSystem=/opt:ro
BindReadOnlyPaths=/opt/myapp/bin /opt/myapp/lib

# === Пространства имён ===
PrivateDevices=yes
PrivateIPC=yes
PrivateUsers=yes
ProtectProc=invisible
ProcSubset=pid
ProtectHostname=yes

# === Защита ядра ===
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes

# === Capabilities ===
CapabilityBoundingSet=
NoNewPrivileges=yes

# === Системные вызовы ===
SystemCallFilter=@system-service @network-io
SystemCallFilter=~@mount @clock @debug @module @raw-io @reboot @swap @obsolete @cpu-emulation @privileged
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

# === Ограничение сети ===
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

# === Дополнительные ограничения ===
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
UMask=0077
MemoryDenyWriteExecute=yes

# === Ограничение ресурсов ===
MemoryMax=256M
MemoryHigh=192M
CPUQuota=100%
TasksMax=32
LimitNOFILE=4096
LimitNPROC=16
LimitCORE=0

[Install]
WantedBy=multi-user.target

Результат:

systemd-analyze security myapp.service
# Результат: OVERALL EXPOSURE LEVEL: 1.4 OK

С 9.6 до 1.4 — отличный результат. Обратите внимание на ключевые моменты:

  • DynamicUser=yes автоматически обеспечил изоляцию пользователя
  • CapabilityBoundingSet= (пустое значение) убрал все capabilities — порт 8080 не требует привилегий
  • MemoryDenyWriteExecute=yes предотвращает JIT-атаки (но учтите: несовместим с Node.js/V8)
  • LoadCredentialEncrypted исключил хранение секретов в открытом виде

Отладка и устранение проблем

При hardening сервис может сломаться. Это нормально — вот пошаговая методика отладки:

# 1. Смотрим журналы
journalctl -u myapp.service -f

# 2. Проверяем статус и код выхода
systemctl status myapp.service

# 3. Временно ослабляем защиту для поиска проблемной директивы
# Закомментируйте директивы по одной и перезапускайте сервис

# 4. Трассировка системных вызовов (если проблема в SystemCallFilter)
strace -f -o /tmp/myapp-strace.log -p $(pidof api-server)

# 5. Проверка доступа к файлам
ls -la /proc/$(pidof api-server)/root/

# 6. Проверка capabilities процесса
grep Cap /proc/$(pidof api-server)/status
capsh --decode=0000000000200000

Типичные проблемы и решения:

  • Сервис не может писать логи — добавьте ReadWritePaths=/var/log/myapp или используйте LogsDirectory=myapp
  • Ошибка DNS-резолвинга — убедитесь, что AF_UNIX и AF_INET есть в RestrictAddressFamilies, а /run/systemd/resolve доступен
  • Не загружаются shared-библиотеки — проверьте ProtectSystem и добавьте нужные пути в ReadOnlyPaths
  • SIGSYS при запуске — расширьте SystemCallFilter или добавьте SystemCallErrorNumber=EPERM для диагностики

Автоматизация и мониторинг

Hardening — это не разовая акция, а постоянный процесс. Автоматизация аудита гарантирует, что безопасность не деградирует со временем (а она обязательно будет деградировать без контроля).

Интеграция с CI/CD

Скрипт для проверки безопасности в конвейере:

#!/bin/bash
# security-audit.sh - Скрипт аудита безопасности сервисов systemd
set -euo pipefail

MAX_SCORE=${MAX_SCORE:-5.0}
SERVICES=("nginx.service" "myapp.service" "postgres.service")
FAILED=0

echo "=== Аудит безопасности сервисов systemd ==="
echo "Максимально допустимая оценка: ${MAX_SCORE}"
echo ""

for service in "${SERVICES[@]}"; do
    # Получаем оценку из JSON-вывода
    SCORE=$(systemd-analyze security --json=short "$service" 2>/dev/null | \
        python3 -c "import sys,json; print(json.load(sys.stdin)['overall_exposure'])" 2>/dev/null || echo "N/A")

    if [ "$SCORE" = "N/A" ]; then
        echo "[SKIP] $service - не удалось получить оценку"
        continue
    fi

    # Сравниваем с пороговым значением
    if (( $(echo "$SCORE > $MAX_SCORE" | bc -l) )); then
        echo "[FAIL] $service - оценка $SCORE (максимум $MAX_SCORE)"
        FAILED=1
    else
        echo "[PASS] $service - оценка $SCORE"
    fi
done

echo ""
if [ "$FAILED" -eq 1 ]; then
    echo "АУДИТ НЕ ПРОЙДЕН: Один или более сервисов превышают пороговую оценку."
    exit 1
else
    echo "АУДИТ ПРОЙДЕН: Все сервисы соответствуют требованиям безопасности."
    exit 0
fi

Пример для GitLab CI:

# .gitlab-ci.yml
security-audit:
  stage: test
  script:
    - cp deploy/*.service /etc/systemd/system/
    - systemctl daemon-reload
    - bash scripts/security-audit.sh
  variables:
    MAX_SCORE: "4.0"
  allow_failure: false
  rules:
    - changes:
      - deploy/*.service

И для GitHub Actions:

# .github/workflows/security.yml
name: Systemd Security Audit
on:
  push:
    paths:
      - 'deploy/*.service'
  pull_request:
    paths:
      - 'deploy/*.service'

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install service files
        run: |
          sudo cp deploy/*.service /etc/systemd/system/
          sudo systemctl daemon-reload

      - name: Run security audit
        run: |
          sudo bash scripts/security-audit.sh
        env:
          MAX_SCORE: "4.0"

      - name: Generate security report
        if: always()
        run: |
          for service in deploy/*.service; do
            name=$(basename "$service")
            sudo systemd-analyze security "$name" > "report-${name}.txt" 2>/dev/null || true
          done

      - name: Upload reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: security-reports
          path: report-*.txt

Периодический аудит

Systemd-таймер для регулярных проверок — чтобы аудит происходил автоматически, без вашего участия:

# /etc/systemd/system/security-audit.service
[Unit]
Description=Periodic systemd security audit
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/security-audit-full.sh
DynamicUser=yes
ProtectSystem=strict
ProtectHome=yes
NoNewPrivileges=yes
# /etc/systemd/system/security-audit.timer
[Unit]
Description=Run security audit weekly

[Timer]
OnCalendar=Mon *-*-* 06:00:00
Persistent=true
RandomizedDelaySec=1h

[Install]
WantedBy=timers.target

Скрипт полного аудита с отправкой уведомлений в Slack:

#!/bin/bash
# /usr/local/bin/security-audit-full.sh
set -euo pipefail

REPORT_FILE="/var/log/security-audit/$(date +%Y%m%d-%H%M%S).json"
ALERT_THRESHOLD=7.0
WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"

mkdir -p /var/log/security-audit

echo "{\"timestamp\": \"$(date -Iseconds)\", \"services\": [" > "$REPORT_FILE"

FIRST=1
ALERTS=""

# Получаем список всех пользовательских сервисов
for service in $(systemctl list-units --type=service --state=running --no-legend | \
    awk '{print $1}' | grep -v '^systemd-'); do

    SCORE=$(systemd-analyze security --json=short "$service" 2>/dev/null | \
        python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('overall_exposure', 'N/A'))" 2>/dev/null || echo "N/A")

    [ "$SCORE" = "N/A" ] && continue

    [ "$FIRST" -eq 0 ] && echo "," >> "$REPORT_FILE"
    FIRST=0

    echo "{\"name\": \"$service\", \"score\": $SCORE}" >> "$REPORT_FILE"

    if (( $(echo "$SCORE > $ALERT_THRESHOLD" | bc -l) )); then
        ALERTS="${ALERTS}\n- $service: $SCORE"
    fi
done

echo "]}" >> "$REPORT_FILE"

# Отправка уведомления при обнаружении проблем
if [ -n "$ALERTS" ] && [ -n "$WEBHOOK_URL" ]; then
    curl -s -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"Внимание: Обнаружены сервисы с высокой оценкой экспозиции:${ALERTS}\"}" \
        "$WEBHOOK_URL"
fi

echo "Аудит завершён. Отчёт: $REPORT_FILE"

Мониторинг с Prometheus

Для непрерывного мониторинга можно написать простой экспортёр метрик:

#!/usr/bin/env python3
# systemd_security_exporter.py
"""Prometheus exporter для оценок безопасности systemd."""

import subprocess
import json
import time
from http.server import HTTPServer, BaseHTTPRequestHandler

METRICS_PORT = 9101
REFRESH_INTERVAL = 300  # 5 минут

cached_metrics = ""
last_refresh = 0

def collect_metrics():
    """Собирает оценки безопасности всех активных сервисов."""
    lines = []
    lines.append("# HELP systemd_security_exposure Exposure score of systemd service (0-10)")
    lines.append("# TYPE systemd_security_exposure gauge")

    result = subprocess.run(
        ["systemctl", "list-units", "--type=service", "--state=running",
         "--no-legend", "--plain"],
        capture_output=True, text=True
    )

    for line in result.stdout.strip().split("\n"):
        service = line.split()[0] if line.strip() else None
        if not service:
            continue
        try:
            sec_result = subprocess.run(
                ["systemd-analyze", "security", "--json=short", service],
                capture_output=True, text=True, timeout=30
            )
            data = json.loads(sec_result.stdout)
            score = data.get("overall_exposure", -1)
            if score >= 0:
                lines.append(
                    f'systemd_security_exposure{{service="{service}"}} {score}'
                )
        except (json.JSONDecodeError, subprocess.TimeoutExpired, KeyError):
            continue

    return "\n".join(lines) + "\n"

class MetricsHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        global cached_metrics, last_refresh
        if self.path == "/metrics":
            now = time.time()
            if now - last_refresh > REFRESH_INTERVAL:
                cached_metrics = collect_metrics()
                last_refresh = now
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(cached_metrics.encode())
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, format, *args):
        pass

if __name__ == "__main__":
    server = HTTPServer(("", METRICS_PORT), MetricsHandler)
    print(f"Exporter listening on port {METRICS_PORT}")
    server.serve_forever()

Правила алертинга для Alertmanager:

# prometheus-rules.yml
groups:
  - name: systemd_security
    interval: 10m
    rules:
      - alert: SystemdServiceInsecure
        expr: systemd_security_exposure > 7.0
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "Сервис {{ $labels.service }} имеет высокую оценку экспозиции"
          description: "Оценка экспозиции {{ $value }} превышает порог 7.0"

      - alert: SystemdSecurityRegression
        expr: delta(systemd_security_exposure[24h]) > 1.0
        for: 30m
        labels:
          severity: critical
        annotations:
          summary: "Регрессия безопасности в {{ $labels.service }}"
          description: "Оценка экспозиции выросла на {{ $value }} за последние 24 часа"

Шаблон безопасного юнита

Используйте этот шаблон как отправную точку для каждого нового сервиса. Скопируйте, адаптируйте под свои нужды — и будете стартовать с хорошей оценки:

[Unit]
Description=Secured Service Template
After=network.target
Documentation=man:myservice(8)

[Service]
Type=notify
ExecStart=/usr/bin/myservice

# Идентичность и привилегии
DynamicUser=yes
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=

# Файловая система
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
StateDirectory=myservice
LogsDirectory=myservice
CacheDirectory=myservice
UMask=0077

# Пространства имён
PrivateDevices=yes
PrivateIPC=yes
PrivateUsers=yes
ProtectProc=invisible
ProcSubset=pid
ProtectHostname=yes

# Защита ядра
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes

# Системные вызовы
SystemCallFilter=@system-service
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM

# Дополнительные ограничения
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes

# Ресурсы
MemoryMax=256M
TasksMax=64
LimitNOFILE=4096
LimitNPROC=32
LimitCORE=0

# Перезапуск
Restart=on-failure
RestartSec=5
WatchdogSec=60

[Install]
WantedBy=multi-user.target

Заключение

Усиление безопасности сервисов systemd — один из самых эффективных способов снизить поверхность атаки на Linux-серверах. И в отличие от SELinux или AppArmor, тут не нужны специальные модули ядра, сложные политики и глубокая экспертиза. Директивы безопасности systemd декларативны, понятны и легко поддаются аудиту.

Вот что стоит запомнить:

  1. Начинайте с аудитаsystemd-analyze security покажет текущее состояние всех сервисов
  2. Применяйте постепенно — включайте директивы по одной, проверяя работоспособность после каждого изменения
  3. Используйте override-файлыsystemctl edit service не трогает оригинальные юниты пакетов
  4. Стремитесь к оценке ниже 4.0 — это реально для большинства сервисов
  5. Автоматизируйте — интегрируйте аудит в CI/CD и настройте периодический мониторинг
  6. Документируйте — если директиву нельзя включить, зафиксируйте причину в комментариях юнита
  7. Используйте DynamicUser и systemd-creds — они упрощают управление идентичностью и секретами
  8. Следите за обновлениями — каждая версия systemd добавляет новые директивы безопасности

Безопасность — это процесс, а не конечное состояние. Регулярный аудит, мониторинг и обновление политик должны стать частью повседневной работы. Но хорошая новость в том, что инвестиции в hardening systemd-сервисов окупаются многократно — каждая включённая директива делает жизнь злоумышленников чуточку сложнее.

Об авторе Editorial Team

Our team of expert writers and editors.