Your Daemons Are Over-Privileged: The systemd Sandboxing Playbook
ProtectSystem, PrivateTmp, NoNewPrivileges, and the scoring tool that grades your work — lock down any systemd service with a drop-in file and fifteen minutes.
Here's a number most people never check: how much of your system a running service can touch. Take a random daemon on your machine — the monitoring agent, the web server, that little Go binary you wrote two years ago. Odds are it can read every file in /etc, write to /tmp where other services play, see all your users' home directories, and load kernel modules if it feels ambitious. It doesn't need any of that. It's just never been told otherwise.
systemd has a sandboxing toolkit that fixes this with a few lines in a unit file. You don't rewrite the service. You don't need SELinux or AppArmor profiles. You describe what the service is allowed to touch, and the kernel enforces it.
The starter pack: eight directives that cover 80%
Pick a service and open an override file — never edit the vendor's unit directly:
sudo systemctl edit myservice.service
That creates a drop-in at /etc/systemd/system/myservice.service.d/override.conf. Everything below goes in a [Service] section. Add them in groups, restart, and verify the service still works after each group. Incremental beats heroic.
Group 1 — stop privilege escalation cold:
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/myservice /var/log/myservice
ProtectHome=yes
NoNewPrivileges=yes kills setuid/setgid escalation for the process and everything it spawns — the single highest-value line in this file. ProtectSystem=strict mounts /usr, /boot, and /etc read-only (and makes most of the rest of the filesystem invisible), with ReadWritePaths carving out exactly the state directories the service actually needs. ProtectHome=yes hides all user home directories.
Group 2 — isolate the service from other processes:
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
PrivateDevices=yes
PrivateTmp=yes gives the service its own /tmp namespace — no more snooping on (or being snooped by) other services' temp files. The ProtectKernel* trio blocks writes to /proc/sys and /sys, blocks loading kernel modules, and blocks cgroup manipulation. PrivateDevices=yes hides physical device nodes, leaving a minimal private /dev. Most network services never touch any of these, so they break nothing.
Group 3 — narrow the syscall and network surface:
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallFilter=@system-service
RestrictAddressFamilies limits which socket families the service can create — the three above cover virtually every network daemon; drop AF_INET6 if you're IPv4-only. SystemCallFilter=@system-service applies a curated allowlist of common syscalls and rejects the exotic ones malware loves.
The part everyone skips: grade your work
systemd ships a scoring tool that audits a unit and rates its exposure on a 0–10 scale:
systemd-analyze security myservice.service
It walks through every sandboxing directive, flags the missing ones, and gives you an overall verdict. A stock service with none of the above typically scores in the scary zone — "EXPOSED". After the three groups above, most services land in "OK" territory with a score under 4. This is the feedback loop that makes the whole exercise practical: harden, score, iterate, harden.
The pitfalls (learned the hard way)
Don't lead with the aggressive ones. MemoryDenyWriteExecute=yes breaks just-in-time compilers (Node, Java, some databases). PrivateUsers=yes breaks anything that needs to resolve real UIDs. SystemCallFilter with a strict custom list will murder a service that needs one oddball syscall you forgot about. The starter pack above is deliberately conservative — apply it first, score it, then tighten.
Test the service's actual functions, not just its startup. A service that starts cleanly but can't write its cache, rotate its logs, or open its database socket will fail at 3 a.m. Run its real workload after each change: send the request, run the backup, trigger the webhook. journalctl -u myservice -f is your friend.
The vendor unit is not yours. That's why systemctl edit exists — drop-ins survive package updates, edited vendor files don't. If you ever find yourself hand-editing /usr/lib/systemd/system/, stop and use the drop-in.
Why this matters more than it looks
Sandboxing doesn't fix bugs in the service. It contains them. The next CVE in your web framework, your monitoring agent, or your backup tool lands differently when the exploited process can see a read-only /usr, an empty /home, its own private /tmp, and nothing else. The attacker gets a box inside your box — small, boring, and hard to escape.
Fifteen minutes, one override file, and a score you can show your work against. There's no cheaper hardening win on a modern Linux system.
Sources: systemd.exec man page — sandboxing options (freedesktop.org) · systemd-analyze — security auditing (freedesktop.org)
Reader feedback
Was this article helpful?
0 people found this article helpful.
