Ограничить службу drop-in'ом
Каждый ключ проверяй работой службы: active ещё не значит, что она
отвечает и пишет свои данные. Падает — смотри journalctl -u: путь из
ошибки уходит в ReadWritePaths= или StateDirectory=.
Ключи юнита, отбирающие у службы лишнее: root, запись в систему, новые привилегии; оценка — systemd-analyze security.
systemd-analyze security
оценка всех служб
systemd-analyze security app
таблица ключей и итог
systemd-analyze security --threshold=50 app
код 1 выше 5.0
systemctl show app -p User -p ProtectSystem -p NoNewPrivileges
sudo systemctl edit app
drop-in с ограничениями
ps -o user,pid,cmd -C python3
от кого работает процесс
sudo systemd-run --pipe --wait -q -p User=demo id
примерить ключ
По умолчанию служба получает всё: работает от root, пишет в любой
каталог, видит домашние каталоги, может запустить sudo. Ошибка в ней —
это root на машине. Ограничения — ключи секции [Service]: systemd
накладывает их на процесс до запуска ExecStart, программа о них не
знает. Ключи кладут в drop-in, и юнит из пакета остаётся
как есть. Общее правило —
минимальные привилегии: службе только то, без
чего она не работает.
Учебная служба demo-web — python3 -m http.server на порту 8090. Без
ограничений она работает от root со всеми capabilities:
User= и Group= запускают службу от названного пользователя. Ему не
нужны ни пароль, ни вход, ни домашний каталог:
Тот же cat от root читает /etc/shadow, от demo — нет:
DynamicUser=yes обходится без useradd: systemd заводит временного
пользователя на время работы службы (uid=64021(run-u50)) и убирает его
после. Данные такой службы живут в /var/lib/private/<имя>.
| Ключ | Что видит служба |
|---|---|
ProtectSystem=yes |
/usr и /boot только для чтения |
ProtectSystem=full |
то же и /etc |
ProtectSystem=strict |
вся файловая система только для чтения, кроме /dev, /proc, /sys и разрешённых путей |
ProtectHome=yes |
/home, /root, /run/user пусты и закрыты |
ProtectHome=read-only |
домашние каталоги только для чтения |
PrivateTmp=yes |
свои пустые /tmp и /var/tmp |
ReadWritePaths= |
пути, открытые для записи при strict |
StateDirectory= |
каталог /var/lib/<имя>: создан, отдан User=, открыт для записи |
Для логов, кэша и сокетов есть такие же ключи: LogsDirectory=
(/var/log/<имя>), CacheDirectory=, RuntimeDirectory= (/run/<имя>).
demo-web при старте дописывает строку в /var/lib/demo/started. Каталог
создан ещё при запуске от root, и после User=demo и
ProtectSystem=strict запись не проходит — служба падает:
StateDirectory=demo чинит это: systemd отдаёт каталог пользователю
demo и открывает его для записи, остальная система так и остаётся только
для чтения:
NoNewPrivileges=yes ставит процессу флаг no_new_privs: ни он, ни его
потомки не поднимут права через setuid-программы (sudo, su) и
capabilities файлов. sudo внутри такой службы отвечает прямо:
Без флага sudo дошёл бы до проверки пароля (sudo: a password is
required) — а у пользователя со своим правилом в sudoers и дальше.
CapabilityBoundingSet= — потолок capabilities службы: пустое
значение отбирает все, список оставляет только названные. Служба от root с
пустым потолком не обойдёт права файлов и не загрузит модуль ядра.
AmbientCapabilities= выдаёт части службе с User=, например порт ниже
1024:
Часть из AmbientCapabilities= действует, только если она есть и в
потолке. Пустой CapabilityBoundingSet= в другом drop-in'е срезает её, и
служба снова получает PermissionError: [Errno 13] Permission denied.
| Ключ | Что отбирает |
|---|---|
PrivateDevices=yes |
устройства: в /dev остаются null, zero, random, urandom, tty и подобные |
ProtectKernelTunables=yes |
запись в /proc/sys и /sys |
ProtectKernelModules=yes |
загрузку модулей ядра |
ProtectKernelLogs=yes |
журнал ядра |
ProtectClock=yes |
смену системного времени |
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX |
остальные типы сокетов |
RestrictNamespaces=yes |
создание пространств имён |
RestrictSUIDSGID=yes |
создание setuid- и setgid-файлов |
SystemCallFilter=@system-service |
системные вызовы вне обычного для служб набора |
IPAddressDeny=any с IPAddressAllow=localhost |
сеть, кроме своей машины |
UMask=0077 |
чтение созданных файлов чужими |
systemd-analyze security <юнит> проверяет около 80 ключей и печатает
таблицу из трёх столбцов: NAME, DESCRIPTION, EXPOSURE. ✗ — защита
не включена, справа её вес; ✓ — включена. Несколько строк для demo-web:
| NAME | DESCRIPTION | EXPOSURE |
|---|---|---|
✗ RootDirectory=/RootImage= |
Service runs within the host's root directory | 0.1 |
SupplementaryGroups= |
Service runs as root, option does not matter | |
✗ User=/DynamicUser= |
Service runs as root user | 0.4 |
✗ CapabilityBoundingSet=~CAP_SYS_TIME |
Service processes may change the system clock | 0.2 |
✗ NoNewPrivileges= |
Service processes may acquire new privileges | 0.2 |
В конце — итог, оценка открытости от 0 до 10:
Без имени юнита команда печатает одну строку на службу: на учебной машине
почти все службы — 9.5–9.6 UNSAFE, у rsyslog — 6.3 MEDIUM, у polkit —
1.6 OK. Уровни:
| Оценка | Уровень |
|---|---|
| 10.0 | DANGEROUS |
| 9.0–9.9 | UNSAFE |
| 7.5–8.9 | EXPOSED |
| 5.0–7.4 | MEDIUM |
| 1.0–4.9 | OK |
| 0.1–0.9 | SAFE |
| 0.0 | PERFECT |
Как менялась оценка demo-web:
| Что включено | Оценка |
|---|---|
| ничего, служба от root | 9.6 UNSAFE |
User=demo, ProtectSystem=strict, NoNewPrivileges=yes, StateDirectory=demo |
8.7 EXPOSED |
и ProtectHome, PrivateTmp, PrivateDevices, пустой CapabilityBoundingSet=, ProtectKernel*, RestrictAddressFamilies, SystemCallFilter и др. |
1.5 OK |
и PrivateUsers, ProtectProc, IPAddressDeny и др. |
0.9 SAFE |
Оценка — сумма весов строк. Первые ключи закрывают самое опасное, но строк
у них три, и оценка падает только на 0.9. Больше всего строк у
capabilities: пустой CapabilityBoundingSet= закрывает их разом.
--threshold=N превращает оценку в проверку. Порог — в десятых долях:
оценка 9.6 — это 96. Выше порога — код 1, равна или ниже — 0:
Оценка считается по настройкам юнита: после daemon-reload она уже новая,
а работающий процесс остаётся прежним до restart. Проверяй и процесс —
ps -o user, /proc/<PID>/status.
Каждый ключ проверяй работой службы: active ещё не значит, что она
отвечает и пишет свои данные. Падает — смотри journalctl -u: путь из
ошибки уходит в ReadWritePaths= или StateDirectory=.
systemd-run запускает команду временной службой с теми же ключами:
Без -q systemd-run печатает имя временного юнита (Running as unit:
run-u49.service) и итог: Finished with result: exit-code,
Main processes terminated with: code=exited/status=1.
В журнале — Read-only file system или Permission denied на запись.
Служба пишет вне разрешённых путей. Свой каталог данных — в
StateDirectory=, чужой путь — в ReadWritePaths=. Если каталог создал
root до User=, Permission denied будет и без strict: StateDirectory=
сменит владельца сам.
Пользователя из User= нет на машине. Заведи его useradd --system или
поставь DynamicUser=yes.
После User= служба потеряла право на порт 80 или 443. Нужна
AmbientCapabilities=CAP_NET_BIND_SERVICE и та же часть в
CapabilityBoundingSet= — подробнее в capabilities.
systemd-analyze security показывает 1.5 OK, а ps — служба от root.
Был daemon-reload без restart: оценка уже по новым настройкам, процесс
запущен со старыми. Перезапусти службу.
Скрипт службы вызывает sudo или другую setuid-программу, а у юнита
NoNewPrivileges=yes. Дай службе нужное право напрямую — User=, группу,
capability — без sudo внутри.
Служба может стартовать и отвечать, а упасть позже — на первой записи в
базу или журнал. Перед strict выясни, куда служба пишет, и проверь
именно запись.
Строки, вписанные прямо в /lib/systemd/system/<юнит>.service, пропадут
при обновлении пакета. Ограничения — только в drop-in'е в
/etc/systemd/system/<юнит>.d/.
show, restart, daemon-reload.Есть неточность? Сообщить об ошибке в статье