Table of Contents
- Understanding Systemd Service Files
- Key Systemd Security Directives
- Advanced Sandboxing Techniques
- Auditing and Monitoring Systemd Services
- Best Practices for Maintaining Hardened Services
- Conclusion
- References
1. Understanding Systemd Service Files
Before diving into hardening, let’s review the structure of a systemd service file (.service). These files define how services start, run, and stop. They are typically stored in /usr/lib/systemd/system/ (distro-provided) or /etc/systemd/system/ (custom overrides).
A basic .service file has three main sections:
[Unit]
Description=Example Service
After=network.target
[Service]
ExecStart=/usr/bin/example-daemon
Restart=on-failure
[Install]
WantedBy=multi-user.target
- [Unit]: Metadata (description, dependencies).
- [Service]: Core execution settings (binary path, restart policy, security directives).
- [Install]: Installation targets (when the service should start).
Critical Note: To customize a service, avoid editing distro-provided files directly. Instead, use systemctl edit <service> to create an override file in /etc/systemd/system/<service>.d/override.conf. This ensures updates to the original service file won’t overwrite your changes.
2. Key Systemd Security Directives
Systemd provides dozens of security-focused directives in the [Service] section. Below, we break down the most impactful ones, grouped by purpose.
2.1 Isolating Service Environments
Isolation prevents a compromised service from affecting other parts of the system. Use these directives to wall off the service’s environment:
PrivateTmp=yes
Creates a private, isolated /tmp and /var/tmp directory for the service. Prevents temp file hijacking attacks (e.g., symlink races).
Example:
[Service]
PrivateTmp=yes
PrivateDevices=yes
Hides physical devices (e.g., /dev/sda) and provides a minimal /dev with only essential devices (e.g., /dev/null, /dev/zero). Blocks direct hardware access.
PrivateUsers=yes
Maps the service’s user/group IDs to a private namespace, isolating it from the system’s user database. Prevents user enumeration and privilege escalation via user namespace attacks.
MountFlags=slave
Makes all mount points private to the service, preventing it from modifying the host’s mount namespace (e.g., mounting malicious filesystems).
2.2 Reducing Privileges
Running services as root is a major risk. Use these directives to drop privileges:
User=, Group=
Run the service as a non-root user/group. Always create a dedicated, least-privilege user (e.g., nginx for the Nginx service).
Example:
[Service]
User=nginx
Group=nginx
DynamicUser=yes
Creates a temporary, ephemeral user/group for the service (no persistent entry in /etc/passwd). Ideal for services that don’t need a persistent identity (e.g., logging daemons).
NoNewPrivileges=yes
Prevents the service from gaining new privileges via setuid/setgid binaries, capabilities, or file capabilities. Blocks common privilege escalation paths.
Why it matters: Even if a service is compromised, an attacker can’t use sudo or su to gain root access.
2.3 Protecting the Filesystem
Restrict which parts of the filesystem the service can read/write to:
ProtectSystem=strict
Makes /usr, /boot, and /etc read-only. Use ReadWritePaths= to explicitly allow write access to specific directories (e.g., logs, caches).
Example:
[Service]
ProtectSystem=strict
ReadWritePaths=/var/log/nginx /var/cache/nginx
ProtectHome=yes
Hides user home directories (/home, /root, /run/user) by mounting them as empty, read-only, or tmpfs. Prevents access to sensitive user data.
ReadOnlyPaths=, InaccessiblePaths=
ReadOnlyPaths=/etc: Makes/etcread-only (even ifProtectSystemis not set).InaccessiblePaths=/proc/kcore: Hides sensitive paths entirely (service sees them as non-existent).
TemporaryFileSystem=/tmp
Mounts a tmpfs on /tmp (similar to PrivateTmp but more flexible; can be used for other directories).
2.4 Restricting Network Access
Limit the service’s network activity to only what’s necessary:
PrivateNetwork=yes
Disables network access entirely. Use for services that don’t need the network (e.g., local database tools).
IPAddressAllow=, IPAddressDeny=
Whitelist/blacklist IP addresses or CIDRs the service can communicate with.
Example (allow only localhost and 192.168.1.0/24):
[Service]
IPAddressAllow=127.0.0.1/32
IPAddressAllow=192.168.1.0/24
IPAddressDeny=any
RestrictAddressFamilies=AF_INET AF_INET6
Limit the service to specific network protocols (e.g., IPv4/IPv6 only). Blocks less common families like AF_UNIX (local sockets) or AF_PACKET (raw sockets).
2.5 Limiting Process Capabilities
Linux capabilities (e.g., CAP_NET_BIND_SERVICE, CAP_SYS_ADMIN) grant granular privileges without full root access. Use these directives to restrict capabilities:
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
Defines the only capabilities the service can use. Start with an empty set and add only what’s needed.
Example (Nginx needs to bind to port 80/443):
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE # Inherit the capability after dropping root
AmbientCapabilities=
Passes capabilities to the service after it drops root privileges (works with User=).
DropCapabilities=ALL
Drops all capabilities except those explicitly allowed in CapabilityBoundingSet.
2.6 Controlling System Calls
System calls (syscalls) are the interface between user-space and the kernel. Restricting them blocks low-level attacks:
SystemCallFilter=@system-service
Allows a predefined set of syscalls required for most services. Add/remove specific syscalls with +/-.
Example (allow read, write, open; block execve):
[Service]
SystemCallFilter=read write open
SystemCallFilter=-execve # Block execve to prevent spawning new processes
SystemCallArchitectures=native
Restricts syscalls to the host’s CPU architecture (e.g., x86_64), blocking cross-architecture exploits.
3. Advanced Sandboxing Techniques
For high-security services (e.g., exposed web apps), combine basic directives with advanced sandboxing:
ProtectKernelTunables=yes
Prevents modification of kernel parameters (e.g., /proc/sys/, /sys/), blocking attacks that alter system behavior.
ProtectKernelModules=yes
Disables loading/unloading kernel modules, a common vector for rootkits.
LockPersonality=yes
Blocks changes to the process personality (e.g., switching to 32-bit mode on a 64-bit system), preventing architecture-specific exploits.
RestrictRealtime=yes
Prevents the service from using real-time scheduling, which can lead to denial-of-service (DoS) attacks.
SystemCallFilter=~@clock @cpu-emulation @debug @keyring
Explicitly block high-risk syscall groups (e.g., @debug for debugging syscalls like ptrace).
4. Auditing and Monitoring Systemd Services
Hardening isn’t set-it-and-forget-it. Use these tools to audit and monitor your services:
systemd-analyze security <service>
Generates a security score (0 = best, 10 = worst) and highlights missing hardening directives.
Example output for Nginx:
# systemd-analyze security nginx
NAME DESCRIPTION EXPOSURE
✓ PrivateTmp=yes Service has private /tmp/ and /var/tmp/ 0.0
✗ User= Service runs as root 9.0
✗ CapabilityBoundingSet= Service may have excessive capabilities 3.0
...
→ Overall exposure level: 6.5 UNSAFE 😨
Fix issues incrementally to lower the score (aim for ≤ 2.0).
journalctl -u <service>
Monitor service logs for errors (e.g., permission denials due to over-restrictive directives).
Filter for security-related logs:
journalctl -u nginx -g "denied|error|permission"
auditd
Track file access, syscalls, and capability usage by the service. Add rules to /etc/audit/rules.d/:
# Monitor writes to /etc/passwd by nginx
-a always,exit -F auid=1001 -F path=/etc/passwd -F perm=w -k nginx-passwd-write
5. Best Practices for Maintaining Hardened Services
- Test Changes: After modifying a service, run
systemctl daemon-reloadandsystemctl restart <service>. Checksystemctl status <service>and logs for errors. - Start Minimal: Begin with a strict policy (e.g.,
ProtectSystem=strict,NoNewPrivileges=yes) and relax only what’s needed. - Use
systemd-analyze verify <service>: Validates the service file for syntax errors. - Automate: Use tools like Ansible or Puppet to enforce hardening across fleets.
- Stay Updated: Systemd regularly adds new security features (e.g.,
DynamicUser=in v232+). Update your OS to get the latest directives.
6. Conclusion
Systemd isn’t just an init system—it’s a powerful security tool. By combining isolation, privilege reduction, filesystem protection, and syscall filtering, you can drastically reduce your service’s attack surface.
Start small: Pick one service (e.g., Nginx), apply the directives in this guide, and use systemd-analyze security to measure improvement. Over time, extend hardening to all critical services.
Remember: Security is a journey, not a destination. Regularly audit, test, and update your hardening policies to stay ahead of emerging threats.
7. References
- systemd.exec(5) Man Page (Official documentation for security directives)
- systemd.service(5) Man Page
- Linux Capabilities (man page)
- Red Hat: Systemd Security Hardening
- Ubuntu Server Guide: Systemd
- CVE-2021-33910 (Example of a vulnerability mitigated by
NoNewPrivileges=yes)
Stay secure, and happy hardening!