Free Tool

Is Your systemd Service Sandboxed, or Running Wide Open?

Paste the [Service] section of a unit file. The checker explains every sandboxing setting it finds, in plain language, and builds a drop-in hardening override from the settings you tick. It runs entirely in your browser — nothing you paste leaves the page.

Most services run as root with no sandboxing at all. A bare service with only ExecStart= scores 9.4 out of 10 on systemd's own exposure scale. Check yours before you find out the hard way.

Check your unit

Paste a [Service] section below. The checker reads it in your browser, explains each sandboxing setting in plain language, and builds a drop-in override from the settings you tick.

What the checker looks for

The checker judges 26 hardening settings, grouped by what they protect. Every meaning below comes from systemd.exec(5); the device and IP settings come from systemd.resource-control(5).

Who runs it. User= and DynamicUser= decide which user the service runs as. With neither set, a system service runs as root. DynamicUser=yes creates a throwaway user that exists only while the service runs, and it also turns on PrivateTmp=, NoNewPrivileges=, RestrictSUIDSGID=, RemoveIPC=, ProtectSystem=strict, and ProtectHome=read-only by itself. One warning: User=nobody is not a safe choice — the analyzer flags it directly ("Special user nobody configured, this is not safe!"). Use a dedicated user or DynamicUser=yes.

What it can write. ProtectSystem=strict makes the whole file system read-only except /dev, /proc, and /sys. ProtectHome=true hides /home, /root, and /run/user. PrivateTmp=true gives the service its own /tmp and /var/tmp, wiped when the service stops. ProtectKernelTunables=, ProtectKernelModules=, and ProtectControlGroups= make kernel settings, module loading, and the control-group tree read-only.

What it can see and reach. PrivateDevices=true gives the service a /dev with only safe pseudo-devices. PrivateUsers=true runs it in its own user namespace, with no privileges on the host. ProtectProc=invisible hides other users' processes from /proc — but it has no effect on a service that still runs as root. ProtectHostname= and ProtectClock= block hostname and clock changes. ProtectKernelLogs= blocks the kernel log ring buffer.

What it can do. NoNewPrivileges=true stops the service and its children from ever gaining privileges again. CapabilityBoundingSet= (empty) drops every Linux capability. SystemCallFilter=@system-service allow-lists the system calls an ordinary service needs and kills the process on any other call. RestrictAddressFamilies= limits which socket families it may open. RestrictRealtime= refuses realtime scheduling. MemoryDenyWriteExecute= forbids memory that is writable and executable at the same time. LockPersonality=, RestrictSUIDSGID=, RemoveIPC=, and UMask=0077 close the remaining gaps.

Network. PrivateNetwork=true is the strongest network setting: the service gets its own network namespace with only loopback. IPAddressDeny=any plus IPAddressAllow=localhost is the milder version — it blocks all IP traffic except localhost.

The score this page cannot give you

systemd-analyze security reports an overall exposure level from 0.0 to 10.0, where high means very little sandboxing. The man page states the range and gives one worked example: systemd-logind.service at 4.1 OK. But the man page gives no band table. The bands below come from the source code itself — badness_table in analyze-security.c, version 255, confirmed identical on the main branch:

Exposure Band
0.0 PERFECT
0.1–0.9 SAFE
1.0–4.9 OK
5.0–7.4 MEDIUM
7.5–8.9 EXPOSED
9.0–9.9 UNSAFE
10.0 DANGEROUS

Two corrections to what secondary articles say: OK ends at 4.9, not 5.0 — 5.0 to 7.4 is MEDIUM. And there are seven bands, not three: articles that list only OK, EXPOSED, and UNSAFE omit DANGEROUS, MEDIUM, SAFE, and PERFECT.

The score is a weighted sum over 81 checks in systemd 255. Each check contributes its badness times its weight, divided by its range, and the total is scaled to 0–100 with ceiling division. The weights are fixed in the source, so a web page cannot reproduce the score exactly — and this page does not try. What it gives instead is coverage: how many of the 26 hardening settings are in place. Coverage is not a score. To get the real number, run the analyzer on the machine that owns the service:

systemd-analyze security --offline=true --no-pager ./your.service

The --offline=true flag reads a unit file directly, with no running service manager — it works on a plain file, even in a container or on WSL. To check the syntax of a unit file (unknown keys, bad values) before you restart anything, run:

systemd-analyze verify ./your.service

Note that verify takes the file directly; --offline= is supported for security only.

Two limits the man page states plainly. First, the analyzer only grades systemd's own per-service features. A high score does not mean the service is actually vulnerable, and a low score does not mean it is safe — the service's own code may add or undo protections. Second, single settings can be circumvented unless combined: a service that can still change mount points can undo most sandboxing by itself, and access to D-Bus or another IPC system can request operations from services under no restrictions at all.

Three test services, run through the real analyzer

We ran three fixture units through systemd-analyze security --offline=true (systemd 255) and recorded the real output. The fixtures are the same units the checker's example buttons load.

Fixture Overall exposure Band
Bare — [Service] with only ExecStart=/bin/true 9.4 UNSAFE
Partial — User=nobody, NoNewPrivileges=true, PrivateTmp=true, ProtectSystem=full, ProtectHome=true 8.7 EXPOSED
Hardened — the full set the checker offers, including DynamicUser=yes and an empty CapabilityBoundingSet= 2.1 OK

The partial fixture is the honest surprise: four well-known settings moved the score only from 9.4 to 8.7. The famous settings are a small part of the total. The weight sits in the long tail — 26 capability rows, 11 system-call-filter rows, and 7 namespace rows. The hardened fixture fell to 2.1 mostly because CapabilityBoundingSet= (empty) cleared all 26 capability rows at once.

The hardened fixture is still not near zero. What remains, from its real output:

  • PrivateNetwork= not set: 0.5 — the single largest remaining row.
  • RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6: 0.3 remains for allowing AF_INET/AF_INET6; only AF_UNIX alone clears the row.
  • SystemCallFilter=@system-service: 0.2 each for @privileged and @resources, which the set does not cover.
  • RestrictNamespaces= not set: 0.1 per namespace type (0.3 for user namespaces).
  • SystemCallArchitectures= not set: 0.2.
  • RootDirectory=/RootImage= not set: 0.1.
  • IPAddressDeny=any with IPAddressAllow=localhost: 0.1 remains.
  • DevicePolicy=closed: 0.1 remains on the device row.

A score of 2.1 OK is a good result, not a perfect one — and chasing 0.0 would mean removing network access, namespaces, and system calls most real services need.

How to use the override safely

  1. Tick only what you have tested. The generator pre-ticks the low-risk settings. The risky ones — PrivateNetwork=, IPAddressDeny=, SystemCallFilter=, RestrictAddressFamilies=, CapabilityBoundingSet=, MemoryDenyWriteExecute= — start unticked. Each carries its breakage warning next to its tick box.
  2. Save it as a drop-in, not an edit. Put the output in /etc/systemd/system/<unit>.d/10-hardening.conf, replacing <unit> with your service name. Drop-in files are read after the main unit file, so they add settings without touching the packaged unit, and they survive package upgrades.
  3. Reload and restart. Run sudo systemctl daemon-reload, then sudo systemctl restart <unit>.
  4. Check it took effect. Run systemctl show <unit> -p ProtectSystem,PrivateTmp,NoNewPrivileges to see the merged values, or systemctl cat <unit> to see the main file plus its drop-ins together — then paste that merged view back into the checker.
  5. Watch the logs. Run journalctl -u <unit> -f for a few minutes. A service killed by a syscall filter or a read-only file system fails fast and loud — that is the filter working, and the fix is to loosen one setting, not to drop the whole override.
  6. Roll back one setting at a time. If the service breaks, remove or comment out the newest setting in the drop-in, run daemon-reload, restart, and re-test. To roll everything back, delete 10-hardening.conf, run daemon-reload, and restart.

Start with the pre-ticked set on a non-production machine. Add the risky settings one at a time, with a test run after each.

Frequently asked questions

Does this page send my unit file anywhere? No. The checker is client-side JavaScript. Your paste stays in your browser tab and is never uploaded, logged, or sent to any server.

Does the checker give me systemd's exposure score? No — and no web page can. The exposure score is computed by systemd-analyze security from weights in systemd's source code. The checker tells you which hardening settings are in place and which are missing, and shows you the exact command to run the real analyzer on your own machine.

What do the exposure bands mean? systemd-analyze security reports an overall exposure level from 0.0 to 10.0. The bands, read from systemd's source code (version 255), are: 0.0 PERFECT, 0.1–0.9 SAFE, 1.0–4.9 OK, 5.0–7.4 MEDIUM, 7.5–8.9 EXPOSED, 9.0–9.9 UNSAFE, and 10.0 DANGEROUS.

Will the override break my service? It can. Each setting in the override carries a breakage warning — ProtectSystem=strict makes the file system read-only, PrivateNetwork=true removes all network access, and SystemCallFilter= can kill a service that needs a syscall outside the allowed set. Only tick settings you have tested, and test on a non-production machine first.

Where do I put the override? Save it as /etc/systemd/system/<unit>.d/10-hardening.conf, then run sudo systemctl daemon-reload and restart the service. Drop-in files are read after the main unit file, so they add settings without editing the packaged unit — and they survive package upgrades.

My service scores 2.1 OK. Should I chase 0.0? No. Our hardened test fixture scores 2.1 OK, and the remaining rows are mostly access a real service needs: network, namespaces, and system calls. A score of 0.0 would mean a service that does almost nothing. OK is a good result.

Why does the checker say "coverage" instead of giving me a number? Because the number belongs to the analyzer. The checker counts how many of the 26 hardening settings are in place. That count is honest about what it is; a made-up score would not be.

Sources

Facts verified 2026-11-05 against systemd-analyze version 255 (255.4-1ubuntu8.18): fixture scores bare 9.4 UNSAFE, partial 8.7 EXPOSED, hardened 2.1 OK; --offline=true reads a unit file with no running service manager; systemd-analyze verify warns on unknown keys. Directive meanings were checked against the systemd 255 manual pages installed on the test machine, the same version as the analyzer.

Keep Exploring

Keep exploring the docs, samples, and free tools to continue your evaluation.