Running NGINX without root

Running NGINX without root

Update 08.01.2021: Use LogsDirectory and CacheDirectory instead of ReadWritePaths.

I like creating my own systemd services for software that hasn't one or only a very basic one with little to no sandboxing. See my goals when creating my own services and explanations for all the sandboxing features here: https://github.com/stephan13360/systemd-services

The default NGINX service is very basic. I'm surprised that a company this big has no resources to create a solid service file with good default sandboxing. Especially since NGINX is a widely deployed webserver and also reachable from the internet. It looks like this when installed from the mainline NGINX repository:

[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf
ExecReload=/bin/sh -c "/bin/kill -s HUP $(/bin/cat /var/run/nginx.pid)"
ExecStop=/bin/sh -c "/bin/kill -s TERM $(/bin/cat /var/run/nginx.pid)"

[Install]
WantedBy=multi-user.target

The service runs as root and uses Type=forking, but it also needs to for what it wants to do. The default NGINX config starts the master process as root and then starts child processes as the user nginx which handle the actually webserver part. The job of the master process is to listen on port 80 and 443 (or other ports below 1024) which non root users can't normally do. It also reads SSL/TLS private keys which are often not readable by non root users.

The second part I simply fix by putting my keys in a directory that can be read by the group acme (other names are available) and add this group the the nginx service with SupplementaryGroups=acme.

To start the service as the nginx user directly and stop it from forking, we need to stop it from running as a daemon in the nginx.conf like this:

daemon off;
pid /run/nginx/nginx.pid;

Since we switched from Type=forking to Type=exec we no longer need to watch for a PID file, so we don't need the PIDFile=/var/run/nginx.pid from the default service. But there is no option to disable the creation of the PID file in the nginx.conf so we at least change its path from the default /var/run/nginx.pid which is deprecated to the /run/nginx directory. To make sure this directory exists and is owned my nginx we can use RuntimeDirectory=nginx which creates a directory called nginx inside /run and adjust the permissions according to the User= option. Moving the PID file is not needed but it feels cleaner.

Since NGINX no longer starts as root, it can not create its own logs and cache directory. This can be solved by adding `LogsDirectory=nginx` and `CacheDirectory=nginx`. Just like RuntimeDirectory these options ensure that the directories /var/log/nginx and /var/cache/nginx exists and are owned by the user nginx.

Without running as root the service would not be able to listen on port 80 or 443. But we are in luck, there is a capability for it to allow non root users to listen on ports below 1024. We add this capability with AmbientCapabilities=CAP_NET_BIND_SERVICE.

The ExecStart= command is the same as the default service. The ExecStop= command is no longer needed since we are not forking anymore and systemd can just stop the binary directly. The ExecReload= does no longer need to read a PID file for the process id and can instead use the systemd variable $MAINPID which systemd keeps track of itself.

We also add a Restart=on-failure which most services should use, to recover of the service should ever crash. Which would be especially bad for a service that provides a public website.

And lastly we add all the sandboxing features this service should have had from the start. We also add two directories to ReadWritePaths= to allow nginx to write logs and also use the proxy_cache and fastcgi_cache directives. If you overwrite the default service before starting NGINX for the first time the directories /var/log/nginx/ and /var/cache/nginx/ will not exists, since only root could create them. So make sure to create these directories and set their owner to nginx.

This is what the complete service then looks like:

[Unit]
Description=nginx
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=exec
User=nginx
Group=nginx
SupplementaryGroups=acme
RuntimeDirectory=nginx
LogsDirectory=nginx
CacheDirectory=nginx

ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf
ExecReload=/bin/kill -HUP $MAINPID

Restart=on-failure
RestartSec=10s

# filesystem access
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true

# network
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

# misc
SystemCallArchitectures=native
NoNewPrivileges=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
ProtectKernelLogs=true
LockPersonality=true
ProtectHostname=true
RemoveIPC=true
RestrictSUIDSGID=true
ProtectClock=true

# capabilities
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

The current version of my NGINX systemd service can always be found in my GitHub repository: https://github.com/stephan13360/systemd-services/tree/master/nginx