systemd — это не только автозапуск и рестарты сервисов. В современных дистрибутивах он даёт мощный набор механизмов sandboxing/hardening: можно ограничить файловую систему, временные каталоги, права ядра (capabilities) и даже запускать процесс без постоянного системного пользователя.
Ниже — практичный набор настроек systemd для Nginx: ProtectSystem, PrivateTmp, DynamicUser, CapabilityBoundingSet и NoNewPrivileges. Цель простая: уменьшить ущерб, если Nginx или модуль окажется уязвимым — процесс сможет сделать меньше «плохих вещей».
Важно: hardening всегда проверяйте на вашей схеме (статика, проксирование, кеши, ACME, логирование, ротации). Двигайтесь от безопасных опций к более «ломким» и опирайтесь на журнал при отладке.
Быстрый обзор: что именно мы защищаем
У Nginx типичные точки воздействия:
- запись в каталог логов и временные каталоги (client body temp, proxy temp и т. п.);
- доступ к конфигам, сертификатам и статике;
- привилегия на bind к портам ниже 1024 (80/443), если процесс запускается не от root;
- попытки «поднять привилегии» через exec и setuid-бинарники;
- доступ к лишним частям файловой системы (например,
/home,/root,/etcцеликом).
systemd hardening закрывает это через:
ProtectSystem— монтирует часть FS как read-only для сервиса;ReadWritePaths— точечно разрешает запись туда, где она реально нужна;PrivateTmp— изолирует/tmpи/var/tmp;DynamicUser— запускает сервис под временным UID/GID (без постоянного пользователя);NoNewPrivileges— запрещает получать новые привилегии через exec;CapabilityBoundingSet— оставляет только необходимые capabilities.
Подготовка: смотрим текущий unit и baseline
Сначала выясните, как Nginx запускается сейчас (unit может отличаться между Debian/Ubuntu, RHEL-подобными, кастомными сборками):
systemctl cat nginx
Полезно посмотреть текущие параметры sandboxing (если дистрибутив уже их включает):
systemd-analyze security nginx
systemd-analyze securityне «гарантирует безопасность», но быстро показывает, какие ограничения уже включены и где остаются «дыры» в профиле.
Дальше не правим unit напрямую. Правильный путь — сделать drop-in override:
systemctl edit nginx
systemd создаст файл вида /etc/systemd/system/nginx.service.d/override.conf.

Шаг 1. NoNewPrivileges: дёшево, безопасно, почти не ломает
Начните с базовой защиты: запрет на получение новых привилегий. Это помогает против сценариев, когда процесс смог выполнить другой бинарник и «надеется» поднять права через setuid или file capabilities.
[Service]
NoNewPrivileges=yes
Примените изменения и перезапустите сервис:
systemctl daemon-reload
systemctl restart nginx
systemctl status nginx --no-pager
Если Nginx стартует — отлично. Обычно эта опция не влияет на работу.
Шаг 2. PrivateTmp: изоляция временных файлов
PrivateTmp даёт сервису отдельные /tmp и /var/tmp. Это снижает риск атак через подмену файлов в общих временных каталогах и уменьшает «соприкосновение» разных сервисов.
[Service]
PrivateTmp=yes
Перезапуск и проверка — как выше. Если у вас есть внешние скрипты или задания, которые ожидают увидеть файлы Nginx в общем /tmp, они перестанут их видеть. Для Nginx это чаще всего нормально, но нестандартные обвязки встречаются.
Шаг 3. ProtectSystem + ReadWritePaths: делаем FS «почти read-only»
ProtectSystem — ключевая ручка sandboxing. В режиме strict systemd делает read-only почти всё, кроме виртуальных путей. После этого нужно явно перечислить, куда Nginx имеет право писать.
Типичные пути записи для Nginx:
/var/log/nginx— логи (если пишете в файлы);/run/nginx— PID-файл и/или сокеты (зависит от конфигурации);/var/lib/nginxили/var/cache/nginx— кеши и temp-файлы (зависит от сборки и директив).
Включаем жёсткий режим и разрешаем запись точечно:
[Service]
ProtectSystem=strict
ReadWritePaths=/var/log/nginx
ReadWritePaths=/run/nginx
ReadWritePaths=/var/lib/nginx
ReadWritePaths=/var/cache/nginx
Нюанс: лучше дозировать ReadWritePaths по реальным ошибкам. Начните с минимума, перезапустите и добавляйте только то, без чего Nginx не стартует или не обслуживает запросы.
Как быстро понять, куда Nginx пытался писать
После неудачного старта смотрите журнал:
journalctl -u nginx -b --no-pager -n 200
И проверьте temp-пути в конфиге (директивы client_body_temp_path, proxy_temp_path, fastcgi_temp_path, uwsgi_temp_path, scgi_temp_path). Если они указывают на нестандартные каталоги — именно их и нужно добавить в ReadWritePaths или перенести на системные каталоги.
Шаг 4. DynamicUser: без постоянного пользователя и «домика» в системе
DynamicUser=yes просит systemd запускать сервис под динамическим UID/GID. Пользователь не живёт в /etc/passwd постоянно. Это удобно, когда вы хотите минимизировать «след» и не держать отдельные аккаунты.
Практическая оговорка для Nginx: исторически он стартует master-процессом с root ради bind на 80/443 и затем форкает worker’ы под пользователем из директивы user в конфиге. При DynamicUser удобнее запускать Nginx сразу под динамическим UID, а право на bind дать через capabilities (следующий шаг).
Минимальный вариант:
[Service]
DynamicUser=yes
И добавьте управляемые systemd каталоги (чтобы systemd сам создавал их с корректными правами на старте):
[Service]
RuntimeDirectory=nginx
RuntimeDirectoryMode=0755
StateDirectory=nginx
CacheDirectory=nginx
LogsDirectory=nginx
Если ваш unit уже готовит каталоги через
ExecStartPre, не дублируйте логику. Выберите один «источник правды»: либоStateDirectory/CacheDirectory/LogsDirectory, либо подготовительные скрипты.
Подводные камни DynamicUser для Nginx
Чаще ломается не Nginx, а окружение:
- сертификаты и ключи лежат в каталогах с правами «только root», и динамический пользователь не может их читать;
- используются UNIX-сокеты (например, PHP-FPM) с жёсткими правами/группами: нужно согласовать владельцев и группы или перейти на TCP;
- нестандартные пути cache/temp: их нужно перенести на каталоги, созданные systemd, или разрешить запись отдельно.
Частый компромисс: оставить статического пользователя (например, www-data), но всё равно включить ProtectSystem, PrivateTmp и capabilities-ограничения. DynamicUser особенно хорошо подходит для «чистых» reverse-proxy узлов.
Шаг 5. CapabilityBoundingSet: оставляем только то, что нужно
Linux capabilities позволяют дать процессу конкретные «частичные root-права» без полного root. Для Nginx в типичном сценарии нужна только возможность слушать привилегированные порты 80/443: CAP_NET_BIND_SERVICE.
Ограничиваем набор capabilities и делаем нужную capability доступной процессу без root:
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
После этого Nginx можно запускать не от root. Если в unit не задан пользователь, задайте его явно или используйте DynamicUser=yes:
[Service]
User=www-data
Group=www-data
Не добавляйте capabilities «наугад». Если что-то сломалось — сначала разберитесь по логам, что именно требуется процессу и почему.
Рабочий пример override.conf (как отправная точка)
Ниже профиль, который часто подходит для «обычного» Nginx (статик + reverse proxy). Его почти всегда нужно подогнать под ваши пути логов, cache/temp и расположение сертификатов.
[Service]
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
RuntimeDirectory=nginx
RuntimeDirectoryMode=0755
StateDirectory=nginx
CacheDirectory=nginx
LogsDirectory=nginx
DynamicUser=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
ReadWritePaths=/run/nginx
ReadWritePaths=/var/cache/nginx
ReadWritePaths=/var/lib/nginx
ReadWritePaths=/var/log/nginx
Применение:
systemctl daemon-reload
systemctl restart nginx
systemctl status nginx --no-pager
Проверка: что hardening реально включился
Проверьте оценку безопасности:
systemd-analyze security nginx
Проверьте, под каким пользователем работает master/worker и какие effective capabilities у процессов:
ps -eo user,pid,ppid,cap_eff,cmd | grep -E 'nginx: master|nginx: worker' | grep -v grep
Проверьте, что Nginx слушает порты:
ss -lntp | grep nginx
Если включён HTTPS, отдельно проверьте чтение ключей и сертификатов: проблемы почти всегда видны в journald.

Типовые проблемы и быстрые решения
Nginx не может открыть access.log или error.log
Сверьте пути логов в конфиге и права. Если вы используете LogsDirectory=nginx, проще всего привести пути к /var/log/nginx. Если логи пишутся в нестандартный каталог — добавьте его в ReadWritePaths.
Ошибка создания temp-файлов (client_body_temp, proxy_temp)
Приведите temp-пути Nginx к /var/cache/nginx или /var/lib/nginx и убедитесь, что эти каталоги доступны на запись. При DynamicUser в связке с CacheDirectory=nginx чаще всего удобнее опираться на системный cache-каталог.
Не стартует из-за PID-файла
Либо уберите явный PID-файл из конфигурации (если он не нужен), либо используйте RuntimeDirectory=nginx и путь /run/nginx/nginx.pid, который гарантированно доступен сервису.
Куда двигаться дальше: дополнительные ручки systemd hardening
Мы сфокусировались на ключевых параметрах: ProtectSystem, PrivateTmp, DynamicUser, CapabilityBoundingSet, NoNewPrivileges. Но у systemd есть и другие ограничения, которые часто хорошо ложатся на Nginx:
ProtectHome=yes— закрыть доступ к/home;ProtectKernelTunables=yesиProtectKernelModules=yes— запретить менять tunables и модули;RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX— оставить только нужные семейства;SystemCallFilter=— фильтрация системных вызовов (мощно, но чаще всего самое «ломкое»).
Практика: усиливайте постепенно, фиксируйте изменения, и после каждого шага прогоняйте чек-лист (reload/restart, запросы на 80/443, загрузка больших файлов, проксирование, вебхуки, ACME, ротации логов).
Итог
systemd sandboxing — быстрый способ повысить безопасность Nginx без сторонних инструментов: сделать файловую систему почти read-only (ProtectSystem), изолировать временные каталоги (PrivateTmp), запретить получение новых привилегий (NoNewPrivileges), убрать лишние root-возможности и оставить только bind на низкие порты через capabilities (CapabilityBoundingSet и AmbientCapabilities). А при желании — уйти от постоянного пользователя через DynamicUser.
Главное — относиться к hardening как к инженерной настройке: минимально необходимые права, прозрачные пути записи и обязательная проверка по журналам при любом отказе.


