BlogLinux & scripting

Sandboxing Linux services with systemd security directives

Turn a plain unit file into a locked-down sandbox with ProtectSystem, NoNewPrivileges, and capability limits.

Mar 10, 2026·4 min readAdvanced·By the SecOpsLog team · command-tested

You do not always need a container to shrink attack surface. systemd can sandbox long-running services using directives in the unit file — read-only filesystem views, dropped capabilities, private /tmp, and syscall allowlists — with no application code changes. Each directive removes a privilege the process almost certainly does not need. systemd-analyze security scores how far you have locked a unit down and points at the next knob to turn.

This note hardens a typical API service unit, explains what each directive buys you, and shows how to iterate without breaking production on the first deploy. For broader Linux hardening context — capabilities, namespaces, audit — see Linux hardening and Linux essentials if unit files are new territory.

Hardening a systemd service incrementally

Add directives in layers. Reload, restart, exercise the service, check journal for EPERM. Roll back one directive at a time if something breaks.

1Baseline unitExecStart only, note behavior2Filesystem sandboxProtectSystem, PrivateTmp3Drop privilegesNoNewPrivileges, DynamicUser4CapabilitiesCapabilityBoundingSet= empty5Network filterRestrictAddressFamilies6SyscallsSystemCallFilter=@system-service7Score + documentsystemd-analyze security

A hardened unit file

ReadWritePaths is the escape hatch — list only directories the service must mutate. Everything else under / becomes read-only or invisible. DynamicUser=yes creates an ephemeral uid/gid pair so you do not manage service accounts by hand.

Drop-in directives like ProtectKernelTunables=yes and ProtectKernelModules=yes block many container-escape primitives on bare metal too. RestrictRealtime=yes stops SCHED_FIFO abuse. Combine with AmbientCapabilities= empty and PrivateUsers=yes only after testing — some daemons expect fixed uids in config files.

/etc/systemd/system/api.service
[Service]
ExecStart=/usr/local/bin/api
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ReadWritePaths=/var/lib/api
CapabilityBoundingSet=
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

Measure with systemd-analyze

Run systemd-analyze security api.service before and after each change. The tool suggests EXPOSED vs SAFE for each setting. Aim for incremental improvement — a score jump from 3.2 to 7.8 is meaningful even if you never hit a perfect 10 on legacy binaries that expect broad filesystem access.

Ship hardened unit files through configuration management — Ansible, cloud-init, or golden AMIs — so manual edits on one host do not drift. Version the unit in git and reload with systemctl daemon-reload in your deploy pipeline. Document which directives were tried and reverted so the next engineer does not repeat a broken SystemCallFilter.

bash — analyze and applylive
systemd-analyze security api.service
ProtectSystem=strict (SAFE)
CapabilityBoundingSet= (SAFE)
IPAddressDeny= (EXPOSED)
systemctl daemon-reload && systemctl restart api
journalctl -u api -n 20 --no-pager
Service started without EPERM errors
Directive effects
Filesystem isolation
ProtectSystem=strict → / read-only
ProtectHome=yes → /home hidden
PrivateTmp=yes → isolated /tmp
ReadWritePaths → explicit write allow
Privilege reduction
NoNewPrivileges → blocks setuid
CapabilityBoundingSet= → drop all caps
SystemCallFilter → syscall allowlist
DynamicUser → ephemeral UID/GID
Strict sandboxing breaks legacy apps
Services that shell out to `/usr/bin/curl`, write logs under `/var/log/myapp` outside ReadWritePaths, or load plugins from `$HOME` will fail with EPERM after hardening. Test in staging with the same unit file. Use `strace` or journal stack traces to find missing paths before prod.

Where this goes next

Combine systemd sandboxing with Linux capabilities tuning for containers and bare metal, forward logs via journald to a central host, and watch syscalls with audit rules on sensitive binaries. Linux hardening covers SSH, firewall, and AppArmor/SELinux layers that complement unit-file sandboxing.

Not every service belongs in a maximum sandbox — databases need predictable uid files and wide ReadWritePaths. Tune per unit, store the final file in git, and re-run systemd-analyze security after every package upgrade in case the vendor changed paths.

Go deeper in a courseLinux hardeningsystemd, SSH, firewall, and defense-in-depth for production hosts.View course

Related posts