Run nginx workers as an unprivileged user
/etc/nginx/nginx.confuser www-data; # Debian, Ubuntu# user nginx; # RHEL, Rocky, AlmaDistribution packages already do this. The page matters for hand-built installs,
containers, and configs where someone set user root; to fix a permissions
problem.
Why it matters — and the part that confuses people
Section titled “Why it matters — and the part that confuses people”nginx runs as two kinds of process, and only one of them should worry you:
- The master starts as root. It has to: binding ports below 1024 requires privilege, and so does reading a TLS private key that is correctly locked down. It does no request handling.
- The workers are what actually parse requests, terminate TLS and talk to
your application. They drop to the
useryou configure.
So ps showing root for one nginx process is normal and not a finding. Every
other line showing root is.
That distinction matters because the workers are where the risk is. nginx has
shipped memory-safety bugs in request-handling code — the 1.30.4/1.31.3 releases
in 2026 fixed a buffer overflow in map with regex, a memory disclosure in the
slice module, and a use-after-free. Those are worker-side. Whether such a bug is
a contained compromise or a root shell is decided entirely by the user
directive.
Don’t use root to fix a permissions problem
Section titled “Don’t use root to fix a permissions problem”The usual road to user root; is a static file nginx can’t read, or a socket it
can’t connect to. Root makes the error go away instantly.
Fix the permission instead:
sudo chown -R www-data:www-data /var/www/sitesudo chmod 640 /etc/nginx/ssl/site.key # readable by root; master reads it before droppingThe key file is the one that trips people. The master reads it at startup
while still root, so it does not need to be readable by www-data — and it
shouldn’t be. A key readable by the worker user is a key readable by anything
that compromises a worker.
systemd sandboxing
Section titled “systemd sandboxing”Cheap additional containment for the same bug:
[Service]NoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=trueReadWritePaths=/var/log/nginx /var/lib/nginx /runProtectSystem=strict makes the filesystem read-only apart from the paths you
name, so even a worker running as root couldn’t write to /etc.
Note NoNewPrivileges is compatible with binding port 80 here — the master binds
before dropping privileges, so nothing needs to gain privilege later.
Related
Section titled “Related”- The alias path traversal — what a worker can read matters more once a path bug exists.