С чего начать Первые шаги трек · 15 ступеней
Linux Labs

Ограничения службы: User=, ProtectSystem, NoNewPrivileges

12 мин Ubuntu 24.04 · systemd 255

Ключи юнита, отбирающие у службы лишнее: root, запись в систему, новые привилегии; оценка — systemd-analyze security.

Шпаргалка

7 команд Версия для печати
  • 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:

text
student@ubuntu:~$ ps -o user,pid,cmd -C python3
USER         PID CMD
root        2382 python3 -m http.server 8090 --bind 127.0.0.1
  --directory /srv/demo
student@ubuntu:~$ sudo grep -E 'Uid|CapEff' /proc/2382/status
Uid:	0	0	0	0
CapEff:	000001ffffffffff

От чьего имени

User= и Group= запускают службу от названного пользователя. Ему не нужны ни пароль, ни вход, ни домашний каталог:

bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin demo

Тот же cat от root читает /etc/shadow, от demo — нет:

text
student@ubuntu:~$ sudo systemd-run --pipe --wait -q -p User=demo \
> cat /etc/shadow
/usr/bin/cat: /etc/shadow: Permission denied

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=, открыт для записи
text
student@ubuntu:~$ sudo systemd-run --pipe --wait -q \
> -p ProtectSystem=strict touch /etc/demo-file
/usr/bin/touch: cannot touch '/etc/demo-file': Read-only file system
student@ubuntu:~$ sudo systemd-run --pipe --wait -q -p ProtectHome=yes \
> ls -la /home
total 4
d---------  2 root root   40 Oct 21 05:05 .
drwxr-xr-x 21 root root 4096 Oct 20 23:33 ..

Для логов, кэша и сокетов есть такие же ключи: LogsDirectory= (/var/log/<имя>), CacheDirectory=, RuntimeDirectory= (/run/<имя>).

demo-web при старте дописывает строку в /var/lib/demo/started. Каталог создан ещё при запуске от root, и после User=demo и ProtectSystem=strict запись не проходит — служба падает:

text
student@ubuntu:~$ sudo journalctl -u demo-web -o cat -n 3
/bin/sh: 1: cannot create /var/lib/demo/started: Permission denied
demo-web.service: Main process exited, code=exited,
  status=2/INVALIDARGUMENT
demo-web.service: Failed with result 'exit-code'.

StateDirectory=demo чинит это: systemd отдаёт каталог пользователю demo и открывает его для записи, остальная система так и остаётся только для чтения:

text
student@ubuntu:~$ ls -ld /var/lib/demo
drwxr-xr-x 2 demo demo 4096 Oct 21 05:06 /var/lib/demo

Новые привилегии

NoNewPrivileges=yes ставит процессу флаг no_new_privs: ни он, ни его потомки не поднимут права через setuid-программы (sudo, su) и capabilities файлов. sudo внутри такой службы отвечает прямо:

text
student@ubuntu:~$ sudo systemd-run --pipe --wait -q -p User=demo \
> -p NoNewPrivileges=yes sudo -n true
sudo: The "no new privileges" flag is set, which prevents sudo from
  running as root.
sudo: If sudo is running in a container, you may need to adjust the
  container configuration to disable the flag.

Без флага sudo дошёл бы до проверки пароля (sudo: a password is required) — а у пользователя со своим правилом в sudoers и дальше.

Capabilities

CapabilityBoundingSet= — потолок capabilities службы: пустое значение отбирает все, список оставляет только названные. Служба от root с пустым потолком не обойдёт права файлов и не загрузит модуль ядра. AmbientCapabilities= выдаёт части службе с User=, например порт ниже 1024:

ini
[Service]
User=demo
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

Часть из 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

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:

text
student@ubuntu:~$ systemd-analyze security demo-web | tail -1
→ Overall exposure level for demo-web.service: 9.6 UNSAFE :-{

Без имени юнита команда печатает одну строку на службу: на учебной машине почти все службы — 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:

text
student@ubuntu:~$ for t in 95 96; do
>   systemd-analyze security --threshold=$t demo-web >/dev/null
>   echo "threshold=$t rc=$?"
> done
threshold=95 rc=1
threshold=96 rc=0

Оценка считается по настройкам юнита: после daemon-reload она уже новая, а работающий процесс остаётся прежним до restart. Проверяй и процесс — ps -o user, /proc/<PID>/status.

Рецепты

2

Ограничить службу drop-in'ом

bash
sudo mkdir -p /etc/systemd/system/demo-web.service.d
sudo tee /etc/systemd/system/demo-web.service.d/hardening.conf <<'EOF'
[Service]
User=demo
StateDirectory=demo
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
CapabilityBoundingSet=
EOF
sudo systemctl daemon-reload
sudo systemctl restart demo-web
systemctl is-active demo-web
curl -s localhost:8090
systemd-analyze security demo-web | tail -1
ps -o user,pid,cmd -C python3

Каждый ключ проверяй работой службы: active ещё не значит, что она отвечает и пишет свои данные. Падает — смотри journalctl -u: путь из ошибки уходит в ReadWritePaths= или StateDirectory=.

Примерить ключ на одной команде

systemd-run запускает команду временной службой с теми же ключами:

bash
sudo systemd-run --pipe --wait -q -p ProtectSystem=strict \
  -p User=demo sh -c 'touch /etc/x; cat /etc/shadow; id'
echo $?        # код команды

Без -q systemd-run печатает имя временного юнита (Running as unit: run-u49.service) и итог: Finished with result: exit-code, Main processes terminated with: code=exited/status=1.

Неполадки

5

Служба упала после ProtectSystem=strict

В журнале — Read-only file system или Permission denied на запись. Служба пишет вне разрешённых путей. Свой каталог данных — в StateDirectory=, чужой путь — в ReadWritePaths=. Если каталог создал root до User=, Permission denied будет и без strict: StateDirectory= сменит владельца сам.

status=217/USER

text
Main PID: 1772 (code=exited, status=217/USER)

Пользователя из User= нет на машине. Заведи его useradd --system или поставь DynamicUser=yes.

Permission denied на порту ниже 1024

После User= служба потеряла право на порт 80 или 443. Нужна AmbientCapabilities=CAP_NET_BIND_SERVICE и та же часть в CapabilityBoundingSet= — подробнее в capabilities.

Оценка снизилась, а процесс прежний

systemd-analyze security показывает 1.5 OK, а ps — служба от root. Был daemon-reload без restart: оценка уже по новым настройкам, процесс запущен со старыми. Перезапусти службу.

sudo: The "no new privileges" flag is set

Скрипт службы вызывает sudo или другую setuid-программу, а у юнита NoNewPrivileges=yes. Дай службе нужное право напрямую — User=, группу, capability — без sudo внутри.

Опасно

2

ProtectSystem=strict для службы с данными

Служба может стартовать и отвечать, а упасть позже — на первой записи в базу или журнал. Перед strict выясни, куда служба пишет, и проверь именно запись.

Правка юнита из пакета

Строки, вписанные прямо в /lib/systemd/system/<юнит>.service, пропадут при обновлении пакета. Ограничения — только в drop-in'е в /etc/systemd/system/<юнит>.d/.

Термины

3
минимальные привилегии least privilege
Правило: программа и пользователь получают только те права, без которых не сделают свою работу. Службе — свой пользователь, запись только в свой каталог, без capabilities, которые ей не нужны. Ещё встречается в: Capabilities: права root по частям
NoNewPrivileges (запрет новых привилегий)
Флаг процесса no_new_privs: ни процесс, ни его потомки не получат больше прав через setuid-программы (sudo, su) и capabilities файлов. В юните — NoNewPrivileges=yes; sudo внутри отвечает «no new privileges».
оценка открытости службы (exposure) exposure level
Число от 0 до 10 в выводе systemd-analyze security: сколько защит юниту не включено. 9.6 UNSAFE — служба с правами root без ограничений, ниже 5 — MEDIUM и меньше. Ключ --threshold делает проверку с кодом возврата.

См. также

  • systemd — юниты и drop-in.
  • capabilities — части root, потолок и ambient-набор.
  • systemctl — show, restart, daemon-reload.
  • journalctl — почему служба упала.
  • apparmor — ограничение профилем ядра.
  • users-groups — системные пользователи.
  • nologin — оболочка пользователя службы.
Загрузка…