systemd service hardening: sandboxing unit files
A service that runs with the full privileges of its user can do a lot of damage when it is compromised. Most systemd unit files never restrict anything: the service can read the whole filesystem, write to /tmp, spawn children, open any network socket, and load kernel modules. systemd ships a set of sandboxing directives that change this, and most of them cost almost nothing to enable. The catch is knowing which ones to apply and how to verify that you did not break the service. This article covers the high-value directives, how to measure exposure, and a safe sequence for tightening units.
The directives that matter most
The highest-value settings are cheap to enable and hard to get wrong:
- NoNewPrivileges=yes prevents the process and its children from gaining privileges through setuid binaries.
- ProtectSystem=strict makes most of the filesystem read-only, with only whitelisted paths writable.
- ProtectHome=yes hides /home, /root, and /run/user from the service.
- PrivateTmp=yes gives the service its own /tmp and /var/tmp.
- PrivateDevices=yes removes access to raw device nodes.
- RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 limits which socket types the service can create.
- CapabilityBoundingSet= trims the kernel capabilities the service may ever acquire.
- MemoryDenyWriteExecute=yes blocks writable-and-executable memory, which raises the bar for common exploit techniques.
- SystemCallFilter=@system-service allows only a conservative set of system calls.
ProtectKernelTunables, ProtectControlGroups, RestrictSUIDSGID, and ProtectProc=invisible add further layers for services that do not need kernel interfaces. Each directive is a separate reduction in what a compromised process can reach, and they compose: a service that cannot write to /home, cannot open sockets, and cannot gain privileges has very little left to offer an attacker.
Measure before you tighten
systemd-analyze security ships with the system and prints an exposure score for any unit:
systemd-analyze security your-service.service
The output lists every sandboxing directive with its current setting and an impact level. Run it on your units and fix the high-impact gaps first. The score is a rough heuristic — a service that needs network access will never score perfect, and that is fine — but it turns “is this unit hardened?” into a number you can track over time.
Tighten in stages
Do not flip every directive at once. Apply a small group, restart the service, and watch its logs. Many breakages surface immediately: a service that needs to write state under /var/lib will fail with ProtectSystem=strict until you add ReadWritePaths. Services that call chroot or need ptrace will hit SystemCallFilter. Use RuntimeDirectory for transient state, ReadWritePaths for the specific files a service must write, and IPAddressAllow or SocketBindDeny for network restrictions. systemd’s documentation describes each directive, and journalctl -u your-service.service will show you the first failure fast. Enable hardening gradually, keep the working unit in version control, and re-run systemd-analyze security after each change.
Hardening the services that touch credentials
Sandboxing matters most for long-running services that hold or forward secrets: credential brokers, keyring helpers, and proxy processes are prime targets because they sit between your secrets and everything else. If you run such a service under systemd, apply the full set of directives above and verify them the same way. A credential broker such as trustless (https://github.com/ikkun1222/trustless) is a single static Go binary that injects secrets into subprocess environments and proxies API traffic; running it under a sandboxed unit with NoNewPrivileges, ProtectSystem=strict, and a tight capability set keeps that service from becoming the foothold that your other hardening was meant to prevent.
Make hardening a habit
Run systemd-analyze security as part of your deploy checklist, and treat a new service with a high exposure score as a review item rather than a launch blocker. The directives are free; the discipline of verifying them is the actual work.
Which of your services would lose the most if it were compromised today, and what does systemd-analyze security say about it?