Safeguard
Security Guides•

Systemd service sandboxing: five directives to add first

Add NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp and a non-root user to a systemd unit first. What each does, and what breaks when you add it.

Safeguard Security Team
3 min read

To sandbox a long-running service with systemd, start with a non-root user, NoNewPrivileges=yes, ProtectSystem=strict, ProtectHome=yes and PrivateTmp=yes. Then list the few paths the service must write with ReadWritePaths=. Each line is cheap, and together they remove most of what a compromised process could touch.

Where to put the settings

Do not edit the vendor unit file under /usr/lib/systemd/system. Run systemctl edit myservice to create a drop-in override, which survives package updates. Then systemctl restart myservice and read journalctl -u myservice for new permission errors.

The five, in order

A non-root user. Set User= and Group= to an account that owns only what the service needs. Everything below is weaker if the process is root.

NoNewPrivileges=yes. The systemd.exec manual says this ensures the service process and its children can never gain new privileges through execve(), for example via setuid or setgid bits or file capabilities. It calls it the simplest and most effective way to stop re-elevation. It defaults to false. One caveat from the same page: it only affects the unit's own process tree, not things started through at, crontab, systemd-run or other IPC services.

ProtectSystem=strict. With strict, the whole file system is mounted read-only for the service, except /dev, /proc and /sys. The manual recommends enabling this for all long-running services unless they manage system updates. true covers /usr and the boot directories, and full adds /etc. It defaults to off.

ProtectHome=yes. Makes /home, /root and /run/user inaccessible and empty. The manual recommends it for long-running services, especially network facing ones. It gives no protection if home directories live outside those paths.

PrivateTmp=yes. Gives the service its own /tmp and /var/tmp, cleaned up when the service stops. This blocks a class of shared temp file tricks. The manual notes it makes sharing between processes through /tmp impossible, so a service that relies on that will need a change.

What breaks, and how to fix it

ProtectSystem=strict is where people get stuck, because the service now cannot write anywhere. Grant exactly what it needs:

[Service]
User=appsvc
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/myapp

The manual also says StateDirectory=, LogsDirectory= and related settings exclude their directories from ProtectSystem=, which is often cleaner than naming paths by hand.

Change one thing at a time on a test host. If the service fails, the journal usually names the denied path.

Limits to keep in mind

The manual says these settings cannot ensure protection in every case, and that many can be undone if the service keeps the privilege to change mounts. Treat them as strong layers, not a wall. Measure the result with systemd-analyze security, and pair the unit with host controls such as a firewall and file permissions.

Safeguard works on the code and dependency side of the same problem: reachability analysis and SBOM scanning show which vulnerable libraries a service actually uses, and a free scan is at gold.safeguard.sh/scan.

Quick check

Is NoNewPrivileges enabled by default?

No. It defaults to false, so you have to set it. DynamicUser implies it.

Why did my service fail after ProtectSystem=strict?

It can no longer write outside /dev, /proc and /sys. Add the needed directories with ReadWritePaths= or use StateDirectory= and LogsDirectory=.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.