Systemd Service Hardening: Sandbox a Unit (2026)

Most services on a Linux VPS run with far more power than they need. A web app that only has to read its own files and listen on a port often runs able to write anywhere on the filesystem, see every other process's temp files, and acquire new privileges at will. If that service is ever compromised, all of that latent power becomes the attacker's. Systemd has a deep sandboxing toolkit built directly into the service manager — no extra software, just directives in the unit file — that strips a service down to only what it actually requires. This guide hardens a unit step by step and scores the result.

Quick Verdict
You harden a systemd service by adding sandboxing directives to a unit override, not by rewriting the application. The highest-value four are NoNewPrivileges=true, ProtectSystem=strict, PrivateTmp=true, and a tight CapabilityBoundingSet. Always use systemctl edit to create a drop-in override rather than editing the vendor unit, and re-run systemd-analyze security after each change to measure progress.

Measure the Starting Point

Before hardening anything, get a baseline. Systemd can score how exposed a service is on a scale from 0 (locked down) to 10 (dangerously open):

1systemd-analyze security nginx.service

The output is a long table of every sandboxing option, whether it is set, and an overall exposure score with a verdict like UNSAFE or MEDIUM. A fresh service from a distro package often scores 9-plus. That table is also your checklist — each red row is a directive you can add. Note the number; you will watch it drop as you apply the steps below.


Create a Drop-In Override

Never edit the unit file shipped by the package — a /lib/systemd/system/myapp.service is overwritten on upgrade. Instead, create a drop-in override that systemd merges on top of the original:

1sudo systemctl edit myapp.service

This opens an empty override file at /etc/systemd/system/myapp.service.d/override.conf. Anything you put under a [Service] heading here wins over the vendor unit and survives upgrades. All the directives below go in this file.


Block Privilege Escalation

The single most important directive prevents the service — and anything it spawns — from gaining new privileges, for example via setuid binaries:

1[Service]
2NoNewPrivileges=true

With this set, even if an attacker tricks the service into executing sudo or a setuid helper, the kernel refuses to grant elevated rights. It is cheap, broadly compatible, and one of the most effective single lines you can add.


Make the Filesystem Read-Only

By default a service can write across the whole filesystem. ProtectSystem flips that. The strict level mounts the entire filesystem read-only for the service, except for a few explicitly granted paths:

1ProtectSystem=strict
2ProtectHome=true
3ReadWritePaths=/var/lib/myapp /var/log/myapp

ProtectSystem=strict is the strongest setting — the process sees /usr, /etc, and /boot as read-only and cannot modify them at all. ProtectHome=true hides /home, /root, and /run/user entirely, so a compromised web service cannot rummage through user data. ReadWritePaths then carves out the specific directories the app legitimately needs to write, such as its data and log directories.

If strict breaks the service because it writes somewhere unexpected, the journal will show a read-only-filesystem error pointing at the path — add it to ReadWritePaths and reload.


Isolate Temp Files and Devices

Services share /tmp by default, which means one compromised process can read another's temporary files. Give the service its own private namespace:

1PrivateTmp=true
2PrivateDevices=true
3ProtectKernelTunables=true
4ProtectControlGroups=true

PrivateTmp=true gives the service a /tmp and /var/tmp that no other process can see. PrivateDevices=true replaces /dev with a minimal set, cutting off access to raw disks and hardware. The two Protect* lines make kernel tunables under /proc/sys and the cgroup hierarchy read-only, blocking a class of escalation tricks.


Drop Unneeded Capabilities

Linux capabilities split root's power into discrete pieces. Most services need none of them; a web server that binds to port 80 needs only one. Strip the bounding set to exactly what is required:

1CapabilityBoundingSet=CAP_NET_BIND_SERVICE
2AmbientCapabilities=CAP_NET_BIND_SERVICE

CapabilityBoundingSet is the hard ceiling — the service can never hold any capability outside this list, even if it tries. Here it keeps only CAP_NET_BIND_SERVICE, which permits binding to ports below 1024. If your service does not bind low ports at all, set CapabilityBoundingSet= empty to grant nothing. Restricting capabilities sharply limits what an exploited process can do to the rest of the system.

You can also restrict which system-call families are reachable:

1SystemCallFilter=@system-service
2SystemCallFilter=~@privileged @resources

The first line allows the common set used by normal services; the second subtracts privileged and resource-control calls.

Two more directives close off common exploit techniques. Restrict the network address families the service can use, and forbid it from creating memory that is both writable and executable — the foundation of many code-injection attacks:

1RestrictAddressFamilies=AF_INET AF_INET6
2MemoryDenyWriteExecute=true

RestrictAddressFamilies here permits only IPv4 and IPv6 sockets, blocking exotic families a typical web service never needs. MemoryDenyWriteExecute=true is potent but test it carefully — some runtimes that JIT-compile code (certain JavaScript or JVM workloads) legitimately need write-execute memory and will crash with it on. For a static binary or a typical interpreter it is safe and valuable.


Apply and Re-Score

Drop-in changes require a daemon reload before they take effect, then a restart of the service:

1sudo systemctl daemon-reload
2sudo systemctl restart myapp.service

Confirm the service still starts cleanly — a sandbox that breaks the app helps no one:

1sudo systemctl status myapp.service

Now re-run the scorer and compare to your baseline:

1systemd-analyze security myapp.service

The exposure number should have fallen substantially — a well-sandboxed service commonly lands in the OK or GOOD range. Each directive you added shows as a satisfied row in the table.


Sandbox What You Run

Filesystem and SSH hardening protect the server's perimeter; systemd sandboxing limits the blast radius inside it. If a service is breached, these directives decide whether the attacker is trapped in a read-only, capability-stripped, private-temp jail or free to roam the box.

Build the override with systemctl edit, add NoNewPrivileges, ProtectSystem=strict, PrivateTmp, and a tight CapabilityBoundingSet, then let systemd-analyze security tell you exactly how much you closed off. It is among the highest-leverage hardening you can do, and it costs nothing but a few lines per unit.

Posts in this series