Hardening Services
Scope: systemd's built-in sandboxing — restricting
what a service can touch, so a bug or a compromise in
the program isn't automatically a compromise of the
machine. Functional effects verified for real; scores
quoted from systemd-analyze.
The idea
A service usually needs far less than it gets by
default: it runs as root, can read the whole disk, write
most of it, see every process, and use every syscall.
The [Service] sandboxing directives carve that down —
no code changes, just unit lines. They use the kernel's
namespaces and filters underneath; your program just
sees a smaller world.
The demonstration
A probe service ran with:
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/mere-demo
and tried three writes. From its journal:
write to allowed path:
OK: /var/lib/mere-demo writable
write to /etc (should be blocked by ProtectSystem=strict):
mkdir: cannot create directory ‘/etc/mere-test’: Read-only file system
ProtectSystem=strict— the whole filesystem becomes read-only for the service, except the API filesystems (/dev,/proc,/sys)...- ...minus
ReadWritePaths=— the carve-outs the service genuinely needs (its data directory). ProtectHome=yes— home directories are invisible (empty) to the service. (The probe's home-write failed as well; the journal shows the attempt and no file appeared.)
No code in the probe knew it was sandboxed. That's the appeal: the restrictions hold even against a program that's actively misbehaving.
The greatest hits
| Directive | Effect |
|---|---|
User= / Group= |
don't run as root — the single highest-value line in this table |
NoNewPrivileges=yes |
the process and its children can never gain privileges (no setuid surprises) |
PrivateTmp=yes |
its own /tmp, invisible to others |
ProtectSystem=strict + ReadWritePaths= |
read-only filesystem except named paths |
ProtectHome=yes |
homes hidden or read-only |
PrivateDevices=yes |
no access to physical devices beyond the basics |
CapabilityBoundingSet= |
drop Linux capabilities (empty = none) |
MemoryMax= / CPUQuota= |
resource ceilings — the service can't eat the machine |
Measure it: systemd-analyze security
systemd-analyze security mere-demo.service
grades ~70 checks and ends with:
→ Overall exposure level for mere-demo.service: 9.6 UNSAFE 😨
The bare demo (root, no restrictions) deserves that
score. The probe's three directives alone moved it only
to 9.0 — the rubric counts capabilities, namespaces,
syscall filtering, and more, so no single setting
transforms it. A variant adding User=nobody,
NoNewPrivileges, PrivateTmp, PrivateDevices, the
kernel protections, an empty CapabilityBoundingSet and
friends scored:
→ Overall exposure level for mere-hardened.service: 4.8 OK 🙂
Use the grader as a checklist, not a verdict: work down its ✗ list for any service exposed to a network or to untrusted input, and stop where a restriction starts breaking your program's actual job — which the journal will tell you about, loudly, as the probe demonstrated.
The honest boundary
Sandboxing limits blast radius; it doesn't patch the
bug, authenticate the user, or encrypt anything. It's
seatbelts, not sobriety. And restrictions can bite:
the first time a service mysteriously can't read a file
it plainly should, check status's journal lines for a
sandbox doing its job — the fix is a ReadWritePaths=
or a User= alignment, not removing the walls wholesale.