systemd Service Hardening: Sandboxing with ProtectSystem, PrivateTmp, and CapabilityBoundingSet

A hands-on guide covering configuration, troubleshooting, and best practices for systemd service hardening: sandboxing with protectsystem, privatetmp, and capabilityboundingset.

Terminal-style illustration for systemd Service Hardening: Sandboxing with ProtectSystem, PrivateTmp, and CapabilityBoundingSet

Defense in Depth for Services

A compromised service should not become a compromised server. Systemd provides built-in sandboxing that works with any service β€” no code changes needed.

Apply hardening via drop-in:

sudo systemctl edit my-service.service

Core Directives

Filesystem isolation:

[Service]
ProtectHome=yes
ProtectSystem=strict
PrivateTmp=yes
ReadWritePaths=/var/lib/my-service /var/log/my-service

Network restrictions:

PrivateNetwork=yes  # No network at all
# Or: RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

Capability and syscall filtering:

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources

Kernel protection:

ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectKernelLogs=yes

Memory hardening:

MemoryDenyWriteExecute=yes

Full Example: Hardened Nginx

[Service]
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/log/nginx /var/cache/nginx /var/run
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
PrivateDevices=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
MemoryDenyWriteExecute=yes

Auditing

systemd-analyze security nginx.service

Aim for a score above 8.0. Start with ProtectSystem=strict and NoNewPrivileges=yes β€” they provide the most benefit with the fewest breakages. Add directives incrementally, testing after each change.

Common Breakages

Nginx can’t write logs β†’ add /var/log/nginx to ReadWritePaths. Docker needs CAP_NET_ADMIN. Database needs data directory in ReadWritePaths. Test with ausearch -m AVC -ts recent to see denials.