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
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=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
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