Короткий ответ
Долгоживущий процесс с uid 0 после bind порта — лишняя поверхность. В systemd задайте User=/Group= (или DynamicUser=yes где уместно), NoNewPrivileges=yes, ProtectSystem=strict, capabilities вместо полного root. Привилегированный bind :443 решайте AmbientCapabilities=CAP_NET_BIND_SERVICE + drop rest, либо socket activation, либо reverse-proxy от www-data. Не чините Permission denied через User=root и не отключайте AppArmor enforcing.
Хост host.example (10.0.20.10). Пользователь-админ admin — не путать с User= сервиса.
Симптомы и как отличить
ps -o user,pid,cmd -C java→ root;systemctl show myservice -p User,UID,MainPID;- логи приложения пишутся в
/root; - после компромисса веб-приложения сразу
/etc/shadow.
| Запуск | Оценка |
|---|---|
nginx master root, workers www-data | типично, смотрите workers |
gunicorn root | плохо |
Docker --privileged | хуже, чем User=root в unit |
cron @reboot sudo python | не unit, тоже uid 0 |
Отличие от sudo ALL у человека: sudoers. Отличие от docker.sock: группа docker = root, даже если контейнер non-root: сокет.
Возможные причины
- Unit из gist:
ExecStart=/usr/bin/python3 app.pyбез User. - «Порт 80 только root» — устаревшая мантра без capabilities.
- Файлы в
/opt/appпринадлежат root700, проще не chown. - Docker host network + root в контейнере.
sudoвнутри ExecStart.- Старый SysV скрипт.
Диагностика
ps -eo user,uid,pid,cmd | awk '$2==0 {print}' | head -n 50
systemctl list-units --type=service --state=running --no-pagerПо подозрительному unit:
systemctl cat myservice.service
systemctl show myservice -p User,Group,UID,GID,CapabilityBoundingSet,NoNewPrivileges,PrivateTmp,ProtectSystem
sudo grep -E '^User=|^Group=|^ExecStart=' /etc/systemd/system/myservice.service /lib/systemd/system/myservice.service 2>/dev/nullПроверка порта:
sudo ss -tlnp | grep myservice
getcap $(readlink -f /usr/sbin/nginx) 2>/dev/nullAppArmor профиль может DENY после понижения uid — это сигнал поправить профиль, не вернуть root: AppArmor.
Решение
Сценарий A. Свой unit в /etc/systemd/system
Создайте системного пользователя:
sudo adduser --system --group --home /var/lib/myservice --no-create-home myservice
sudo mkdir -p /var/lib/myservice /var/log/myservice
sudo chown -R myservice:myservice /var/lib/myservice /var/log/myservice /opt/myserviceOverride:
sudo systemctl edit myservice[Service]
User=myservice
Group=myservice
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myservice /var/log/myservice
CapabilityBoundingSet=
AmbientCapabilities=Если нужен bind :80:
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICEЛучше слушать 127.0.0.1:8080 и отдать 80/443 nginx/caddy от www-data.
sudo systemctl daemon-reload
sudo systemctl restart myservice
ps -o user,cmd -C python3Сценарий B. Пакетный nginx/apache
Не переводите master в www-data вручную против пакета. Workers уже non-root. Проверьте, что нет user root в nginx.conf.
Сценарий C. Docker
В образе USER ненулевой UID, без --privileged, без -v /:/host. Root в контейнере + bind mount / = root хоста. Понижение на хосте не заменяет это.
Сценарий D. Нужны привилегии на устройство
DeviceAllow, группа disk точечно, не User=root. SUID на своём бинаре — хуже: SUID.
Практические ловушки на Ubuntu 22.04/24.04. Пакетные unit в /lib/systemd/system не правьте на месте: только systemctl edit или файл в /etc/systemd/system/имя.service.d/override.conf. Иначе apt вернёт User=root или затрёт ExecStart. После edit обязателен daemon-reload; забытый reload оставляет старый uid в памяти до следующего restart.
Проверьте, что сервис не поднимает helper от root через ExecStartPre=/bin/chown -R на всё /opt: это снова окно uid 0 и гонка с live-данными. chown — разово в окне миграции, не каждый старт.
Логи journald от non-root пишутся без проблем; проблемы начинаются, когда приложение само открывает /var/log/myservice.log как файл 644 root. Либо StandardOutput=journal, либо файл заранее chown myservice. Не chmod 666 лог «на время».
Socket activation (*.socket unit) позволяет процессу без CAP_NET_BIND слушать 80: systemd отдаёт fd. Это чище capabilities на самом бинаре. На 24.04 так часто устроен ssh.socket — не копируйте этот паттерн слепо на Java, но имейте в виду для своих reverse-proxy.
Если после понижения uid «не видно» USB/тюнера/GPU — это DeviceAllow и группа video/render, не повод вернуть root. Смотрите journalctl -u myservice на Permission denied с путём /dev/. AppArmor может отдельно DENY /dev/nvidia; профиль, не uid 0.
Связка с SUID: не ставьте chmod u+s на бинарь приложения вместо User= в unit. SUID-обёртка хуже контролируемого systemd. См. SUID.
Проверьте systemctl show myservice -p SupplementaryGroups,PrivateUsers,ProtectKernelTunables. На 24.04 имеет смысл ProtectKernelTunables=yes и LockPersonality=yes, если приложение не настраивает sysctl само. Не включайте PrivateNetwork=yes у сервиса, которому нужна LAN — получите «как будто User= виноват».
Проверьте SupplementaryGroups= в unit: лишняя группа docker или sudo у сервиса = снова root-эквивалент. Список групп должен быть минимальным для устройства/лога.
Как проверить, что проблема устранена
ps -o user,uid,pid,cmd -p $(systemctl show -p MainPID --value myservice)
systemctl show myservice -p User,NoNewPrivileges,ProtectSystem
sudo -u myservice test -w /var/lib/myservice && echo data_ok
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/healthMainPID не uid 0 (исключения: мастер с drop в дочерних — тогда дочерние не root). Сервис отвечает. admin по SSH жив. AppArmor профиля в enforce, если был.
Если не помогло
- Permission denied на лог:
ReadWritePaths, не root. - Порт 80 не берётся: capabilities или proxy, не uid 0.
- DynamicUser и фиксированный UID в volume — несовместимо; обычный
adduser --system. - SELinux denials на Rocky:
ausearch -m AVC, неsetenforce 0навсегда. На Ubuntu —journalctl | grep DENIED. - После понижения сервис пишет в
/root/.config— задайтеEnvironment=HOME=/var/lib/myservice.
Профилактика
- Шаблон unit в ansible с User= обязателен для своих сервисов.
- Список процессов uid 0 в мониторинге (белый список systemd/sshd/cron).
- Code review Dockerfile
USER. - Не запускать из crontab
sudo -u rootто, что должно быть timer + User=.
FAQ
CAP_NET_BIND_SERVICE безопаснее root?
Да на порядки, это не uid 0. Всё ещё не идеал: лучше unprivileged порт + proxy.
DynamicUser vs adduser --system
DynamicUser удобен без persistent state. Для БД-файлов — фиксированный system user.
Можно ли оставить root «только в docker»?
Нет как политика. Root в контейнере + слабые mounts = хост. См. docker.sock.
systemd --user для сервиса сайта?
Для пользовательских сессий да. Системный сайт — system user, не интерактивный admin.
ProtectSystem ломает запись в /usr
Это цель. Данные в /var/lib, бинари обновляет apt, не приложение.