Hardening Systemd Services: От основ к профессиональной конфигурации

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

От стандартного сервиса к песочнице

Исходный конфиг для демона на Python:

ini
[Service]
ExecStart=/usr/bin/python3 /opt/myapp/main.py
User=appuser

Добавим базовую изоляцию:

ini
[Service]
ExecStart=/usr/bin/python3 /opt/myapp/main.py
User=appuser
PrivateTmp=yes
NoNewPrivileges=yes
RestrictSUIDSGID=yes

Уже лучше: сервис получил отдельный /tmp, потерял возможность создавать SUID-файлы и повышать привилегии. Но это только начало.

Контроль ресурсов и capabilities

Capabilities

Зачем давать процессу все возможности по умолчанию? Для веб-сервера достаточно:

ini
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

Теперь процесс может открыть порт ниже 1024 без полного root-доступа.

Cgroups: не только для Docker

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

ini
MemoryMax=512M
CPUQuota=150%

Для распределенных систем важнее контроль ввода-вывода:

ini
IOWeight=10
DeviceAllow=/dev/nvme0n1 rw

Сетевой карантин

Ограничим сетевую активность только необходимыми интерфейсами:

ini
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

Для выделенного VPN-интерфейса vpn0:

ini
BindReadOnlyPaths=/sys/class/net/vpn0

Файловая система как иммунная система

Полная изоляция файловой системы:

ini
ProtectSystem=strict
ProtectHome=tmpfs
ReadWritePaths=/var/lib/myapp

Принудительный RO доступ для системных директорий:

ini
InaccessiblePaths=/usr/lib/firmware /boot

Безопасный дебаггинг

Включение отладки без снятия защиты:

ini
NotifyAccess=all
StandardOutput=journal

Логирование в режиме strace без остановки сервиса:

bash
systemd-inhibit --what=handle-suspend strace -p $(pgrep -f myapp.service)

Проверка перед запуском

Тестирование конфига:

bash
systemd-analyze verify --recursive-errors=yes /etc/systemd/system/myapp.service

Эмуляция старта с полным выводом:

bash
systemd-run --unit=test-hardening --service-type=exec -p User=appuser -p PrivateTmp=yes /opt/myapp/main.py
journalctl -u test-hardening -f

Когда защита мешает: анализ проблем

Симптом: сервис падает с "Permission denied" при работе с /dev/ttyUSB0.

Диагностика:

  1. Проверка прав: ls -l /dev/ttyUSB0
  2. Поиск недостающих capability: CAP_DAC_OVERRIDE или CAP_SYS_RAWIO
  3. Добавляем временный доступ:
    ini
    DeviceAllow=/dev/ttyUSB0 rw
    TemporaryFileSystem=/dev:ro
    BindPaths=/dev/ttyUSB0
    

Баланс безопасности и удобства

Полное отключение сети (PrivateNetwork=yes) может сломать работу с локальным unix-сокетом. Вместо этого:

ini
IPAddressDeny=any
IPAddressAllow=192.168.10.0/24

Для стека TCP/IP:

ini
RestrictAddressFamilies=AF_INET
RestrictNamespaces=yes

Эволюция подхода

  1. Начните с ProtectSystem и PrivateTmp
  2. Добавляйте по одному ограничению за шаг
  3. Используйте systemd-analyze security myapp.service для оценки
  4. Сравнивайте вывод ps auxZ до и после изменений
  5. Мониторьте журналы через journalctl -f -u myapp.service

Финал config'а может выглядеть так:

ini
[Service]
ExecStart=/usr/bin/python3 /opt/myapp/main.py
User=appuser
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
MemoryMax=1G
PrivateTmp=yes
ProtectSystem=full
NoNewPrivileges=yes
RestrictRealtime=yes
LockPersonality=yes
SystemCallFilter=@system-service

С такими настройками даже уязвимость в вашем Python-коде не позволит атакующему получить shell или прочитать приватные ключи SSH. Это не паранойя — это совместимость с PCI DSS и SOX для production-систем.

text