← News
/PLAYBOOK · 2026-09-09

systemd Service Sandbox Hardening in Practice: Minimize Process Privileges

After a service is compromised, limiting lateral movement is a key defense-in-depth layer. Covers systemd sandboxing: ProtectSystem read-only filesystem, PrivateTmp private /tmp, NoNewPrivileges, CapabilityBoundingSet, plus systemd-analyze security scoring. Includes path-miss and CentOS 7 directive compatibility troubleshooting.

Applicable scenarios

When a host runs Nginx, Redis, a Python app, and more at once, the goal of systemd service hardening is: assume a service is compromised by a vuln, lock the attacker's activity inside that service's sandbox—cannot read system config beyond /etc/passwd, cannot tamper with system directories, cannot escalate. Compared with MAC such as AppArmor or SELinux, systemd sandboxing needs no extra software, enables per-service, and is the most practical defense-in-depth built into Linux by default.

Prerequisites

How it works

systemd service hardening uses kernel mount namespaces, capabilities, seccomp, and related mechanisms to drop rights at start: read-only filesystem mounts, private /tmp, drop root capabilities, restrict syscalls. Directives live in the unit file; PID 1 applies them at fork/exec. After the attacker has process control they are already in a restricted context; escape needs an extra kernel vuln. Official `systemd-analyze security` scores each service so you can quantify hardening.

Procedure

Step 1: Check current security score

`systemd-analyze security nginx.service | tail -20`

The end of the output is a 0-10 total score; 10 is most secure. Unhardened Nginx is usually 4-6. Each EXPOSE item maps to a directive you can tighten.

Step 2: Create an override

Do not edit the distro unit file (upgrades overwrite it). Use `systemctl edit` to generate an override:

`systemctl edit nginx.service`

In the editor, write a `[Service]` section (Nginx example):

`[Service]
# Entire filesystem read-only except whitelist directories
ProtectSystem=strict
ReadWritePaths=/var/log/nginx /var/cache/nginx /run

# Private /tmp, isolated from other processes
PrivateTmp=true

# Forbid privilege escalation (setuid/setcap both ineffective)
NoNewPrivileges=true

# /home directory not visible
ProtectHome=true

# Forbid modifying kernel parameters and devices
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true

# Restrict address families (IPv4/IPv6/Unix socket only)
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

# Narrow capability set (Nginx only needs bind-port capability)
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

# Forbid obtaining new privileges
RestrictSUIDSGID=true`

Key params: ProtectSystem=strict mounts the whole filesystem tree read-only; only paths in ReadWritePaths are writable. PrivateTmp=true gives the service its own /tmp (stacks with prior /tmp noexec hardening). NoNewPrivileges=true blocks the classic setuid-binary path to root. Writable paths must be listed in ReadWritePaths one by one, or the service fails at start with Permission denied.

Step 3: Apply and verify

`# Reload unit definition and restart the service
systemctl daemon-reload
systemctl restart nginx

# Confirm sandbox directives have taken effect
systemctl show nginx -p ProtectSystem -p PrivateTmp -p NoNewPrivileges

# Score again; compare before vs after hardening
systemd-analyze security nginx.service | tail -3`

Use the same method for Redis (add `ProtectClock=true`, `RestrictNamespaces=true`) and in-house services, one by one. Target: raise every internet-facing service above 7.

Config verification

Enter the service sandbox context and test write rights:

`# Inside the nginx service namespace, try writing /etc (should be rejected)
systemd-run --scope -p ProtectSystem=strict sh -c 'touch /etc/test && echo write succeeded || echo write blocked'

# Confirm the service is working
curl -sI http://127.0.0.1/ | head -1

# Confirm logs have no permission-denied errors
journalctl -u nginx --since \"5 minutes ago\" | grep -i denied`

FAQ

FAQ 1: After restart, Permission denied, service will not start

Most common cause: a runtime write path was not added to ReadWritePaths. Find it: `journalctl -u SERVICE -p err --since today` — look at the exact denied path, append it to ReadWritePaths, then `systemctl daemon-reload && systemctl restart`. Note Nginx reload also fails if the PID file directory is not writable; keep /run writable.

FAQ 2: CentOS 7 reports Unknown key for some directives, or they do not take effect

CentOS 7 ships systemd 219. It does not support RestrictAddressFamilies, ProtectKernelModules, and other newer directives. systemd prints a warning but still starts; only old directives apply. Trust the actual `systemd-analyze security` output and verify item by item. Upgrade the OS if you can; the full sandbox directive set is complete only after systemd 232.

Summary

systemd service hardening builds a per-service sandbox at near-zero cost: read-only filesystem, private /tmp, no privilege escalation, narrowed capability set, plus a scoring tool to keep quantifying each service's exposure. Complements WAF and firewalls at the perimeter—the perimeter stops inbound; the sandbox stops lateral movement after inbound. Start with internet-facing Nginx and Redis, tighten service by service. Keep all changes in override.d so OS upgrades do not drop them.