Создание надежных systemd-сервисов — это не просто добавление ExecStart
в unit-файл. Современные стандарты безопасности требуют минимизации поверхности атаки через изоляцию процессов и контроль разрешений. Разберем, как превратить типичный сервисный файл в форт Нокс, сохранив функциональность.
От стандартного сервиса к песочнице
Исходный конфиг для демона на Python:
[Service]
ExecStart=/usr/bin/python3 /opt/myapp/main.py
User=appuser
Добавим базовую изоляцию:
[Service]
ExecStart=/usr/bin/python3 /opt/myapp/main.py
User=appuser
PrivateTmp=yes
NoNewPrivileges=yes
RestrictSUIDSGID=yes
Уже лучше: сервис получил отдельный /tmp
, потерял возможность создавать SUID-файлы и повышать привилегии. Но это только начало.
Контроль ресурсов и capabilities
Capabilities
Зачем давать процессу все возможности по умолчанию? Для веб-сервера достаточно:
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
Теперь процесс может открыть порт ниже 1024 без полного root-доступа.
Cgroups: не только для Docker
Ограничение памяти и CPU:
MemoryMax=512M
CPUQuota=150%
Для распределенных систем важнее контроль ввода-вывода:
IOWeight=10
DeviceAllow=/dev/nvme0n1 rw
Сетевой карантин
Ограничим сетевую активность только необходимыми интерфейсами:
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
Для выделенного VPN-интерфейса vpn0
:
BindReadOnlyPaths=/sys/class/net/vpn0
Файловая система как иммунная система
Полная изоляция файловой системы:
ProtectSystem=strict
ProtectHome=tmpfs
ReadWritePaths=/var/lib/myapp
Принудительный RO доступ для системных директорий:
InaccessiblePaths=/usr/lib/firmware /boot
Безопасный дебаггинг
Включение отладки без снятия защиты:
NotifyAccess=all
StandardOutput=journal
Логирование в режиме strace
без остановки сервиса:
systemd-inhibit --what=handle-suspend strace -p $(pgrep -f myapp.service)
Проверка перед запуском
Тестирование конфига:
systemd-analyze verify --recursive-errors=yes /etc/systemd/system/myapp.service
Эмуляция старта с полным выводом:
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
.
Диагностика:
- Проверка прав:
ls -l /dev/ttyUSB0
- Поиск недостающих capability:
CAP_DAC_OVERRIDE
илиCAP_SYS_RAWIO
- Добавляем временный доступ:
ini
DeviceAllow=/dev/ttyUSB0 rw TemporaryFileSystem=/dev:ro BindPaths=/dev/ttyUSB0
Баланс безопасности и удобства
Полное отключение сети (PrivateNetwork=yes
) может сломать работу с локальным unix-сокетом. Вместо этого:
IPAddressDeny=any
IPAddressAllow=192.168.10.0/24
Для стека TCP/IP:
RestrictAddressFamilies=AF_INET
RestrictNamespaces=yes
Эволюция подхода
- Начните с
ProtectSystem
иPrivateTmp
- Добавляйте по одному ограничению за шаг
- Используйте
systemd-analyze security myapp.service
для оценки - Сравнивайте вывод
ps auxZ
до и после изменений - Мониторьте журналы через
journalctl -f -u myapp.service
Финал config'а может выглядеть так:
[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-систем.