Christian Lehnert — Linux, Hacking & Faith

systemd Service Hardening - The Sandbox You Already Have and Do Not Use

Christian Lehnert2026-08-15~6 min read

systemd Service Hardening

Run One Command and Get Uncomfortable

Here is a thing to do right now on any Linux box you run. Ask systemd
to score how exposed your services are:

systemd-analyze security

It ranks every service by how much damage it could do if compromised.
The scores will be bad. A stock nginx, postgresql, or sshd unit
typically lands around 9.6 out of 10, labelled UNSAFE. Almost
everything on a default system scores in that range, because the
default is a service with essentially no restrictions.

That is not an nginx problem or a Postgres problem. It is the default.
A service, unless you say otherwise, runs able to write across the
filesystem, see every other process's temp files, call three hundred
syscalls it will never use, and acquire new privileges at will. It got
all of that because nobody took it away. And systemd, the thing already
managing every one of those services, has a built-in toolkit to take it
away, that almost nobody turns on.

What the Default Actually Costs

Think about what a typical service needs versus what it has.

A web app needs to read its own files, write to a couple of
directories, and listen on a port. That is the job. What it actually
has, by default, is write access to the whole filesystem, visibility
into every other process, the ability to load kernel modules, access to
every device, three hundred-plus syscalls, and a clear path to escalate
privileges through any setuid binary on the system.

The gap between those two lists is pure attacker profit. When that
service is compromised, through a bad dependency, an exploit, a
poisoned input, the attacker inherits everything the process could
technically reach, which is the entire second list, not the small first
one. The service never needed that power. It just carried it around,
latent, until someone else got to use it.

This is blast radius, the same idea that runs through everything worth
saying about security. The damage a compromise does is bounded by what
the compromised thing could reach. A service sandboxed down to its
actual needs is a service whose compromise is contained. A stock
9.6-UNSAFE service is a service whose compromise is the whole box.

The Fix Does Not Touch the Application

The part that makes this worth doing is that you harden a service
without changing a line of its code. The directives go in a systemd
drop-in override, beside the unit, and systemd applies them every time
the service starts. The application does not know and does not need to.

A handful of directives carry most of the value, and they are cheap and
rarely break anything.

Block privilege escalation. The single most important directive stops
the service and everything it spawns from ever gaining new privileges.
Even if an attacker tricks it into running a setuid binary, the kernel
refuses to hand over elevated rights. It is one line, it neutralises a
whole escalation path, and it almost never breaks a service.

Make the filesystem read-only. Turn the whole filesystem read-only for
the service, then whitelist the two or three directories it genuinely
writes to. A compromised service can no longer scatter files across the
system, modify binaries, or drop a persistence mechanism, because
everything except its own data directories rejects writes.

Give it a private temp. A per-service private temp directory, invisible
to every other process, closes off the entire class of attacks that
work through shared temp files, and stops the service from snooping on
anyone else's.

Drop the capabilities. Strip every Linux capability and add back only
the ones the service actually uses. A web server that binds a low port
keeps exactly the capability for that and nothing else. The dozens of
other capabilities, the ones that let a process rewrite the clock or
load kernel modules or override file permissions, are simply gone.

Beyond these, syscall filtering restricts the service to the syscalls a
normal service uses, and kernel-protection directives hide the tunables
and modules and logs a service has no business touching. Each one you
add closes off another avenue, and each one drops the exposure score.

Harden Incrementally, Because It Will Break Things

There is an honest catch, and ignoring it is how people paste a giant
block of directives and then disable the whole thing in frustration.

You are putting a service under restrictions it was never designed to
run under. Some services will misbehave. The failure mode is
predictable: you make the filesystem read-only, the service starts,
tries to write to a path you did not whitelist, and dies with a
read-only-filesystem error.

That is the feature working, not breaking. The log names the exact path
it was denied. You read the journal, see the path, add it to the
whitelist, restart, and move on. This is why you harden one step at a
time rather than all at once: add a few directives, restart, confirm the
service still works, check the new score, then tighten further. A
giant block applied blind gives you a broken service and no idea which
of thirty directives did it. A few at a time gives you a working,
hardened service and a clear trail.

Re-run the security score after each round. Watching a service drop from
9.6 to the 4s and then lower is the feedback loop that makes this
concrete, and it tells you exactly how much attack surface each change
removed.

When To Bother, and When Not

Not every service needs the full treatment, and pretending otherwise
wastes effort on things that do not matter.

Harden the services that are exposed and long-lived: anything listening
on the network, anything processing untrusted input, anything that is a
plausible foothold. Those are the ones whose blast radius you actually
care about, and the ones an attacker actually reaches. A public-facing
web server or a service parsing external data earns every directive you
can put on it.

For a one-shot internal script that runs for two seconds as a dedicated
user with no network and no untrusted input, the full sandbox is effort
without much return. Be honest about which services are real attack
surface and spend the effort there. The four high-value directives are
cheap enough to apply broadly; the deeper syscall and namespace work is
worth reserving for the services that genuinely face something hostile.

The Point

Every service on your system runs with far more power than its job
requires, and that surplus power is exactly what an attacker inherits
when the service falls. systemd already manages those services and
already ships the sandbox to strip that power away, no extra tooling, no
application changes, just directives in a drop-in. Almost nobody turns
it on.

Tagged:
#linux #systemd #security
← Back to posts