Защита сервисов 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— привязка к портам ниже 1024CAP_DAC_READ_SEARCH— обход проверок прав чтения файловCAP_SYS_CHROOT— использование chrootCAP_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=strictProtectHome=read-onlyPrivateTmp=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 декларативны, понятны и легко поддаются аудиту.
Вот что стоит запомнить:
- Начинайте с аудита —
systemd-analyze securityпокажет текущее состояние всех сервисов - Применяйте постепенно — включайте директивы по одной, проверяя работоспособность после каждого изменения
- Используйте override-файлы —
systemctl edit serviceне трогает оригинальные юниты пакетов - Стремитесь к оценке ниже 4.0 — это реально для большинства сервисов
- Автоматизируйте — интегрируйте аудит в CI/CD и настройте периодический мониторинг
- Документируйте — если директиву нельзя включить, зафиксируйте причину в комментариях юнита
- Используйте DynamicUser и systemd-creds — они упрощают управление идентичностью и секретами
- Следите за обновлениями — каждая версия systemd добавляет новые директивы безопасности
Безопасность — это процесс, а не конечное состояние. Регулярный аудит, мониторинг и обновление политик должны стать частью повседневной работы. Но хорошая новость в том, что инвестиции в hardening systemd-сервисов окупаются многократно — каждая включённая директива делает жизнь злоумышленников чуточку сложнее.