Table of Contents
- Understanding Systemd Service Units
- The Least Privilege Principle: Running Services as Non-Root
- Hardening Service Files: Key Directives
- 3.1 Filesystem Isolation
- 3.2 Privilege Restriction
- 3.3 Network and Resource Limiting
- Securing Service Execution
- Monitoring and Auditing Systemd Services
- Advanced Hardening Techniques
- Common Pitfalls to Avoid
- Conclusion
- References
1. Understanding Systemd Service Units
Before diving into hardening, it’s critical to understand systemd service units—the core configuration files that define how services behave. These files (typically ending in .service) are stored in:
/etc/systemd/system/: User-defined or overridden services (highest priority)./usr/lib/systemd/system/: Distribution-provided default services.
A basic service unit has three key sections:
[Unit]: Metadata (description, dependencies, documentation).[Service]: Execution details (user, group, start command, restart policy).[Install]: Installation targets (e.g.,multi-user.targetfor boot-time startup).
Example: Basic (Unsecured) Service Unit
[Unit]
Description=Example Unsecured Service
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/example-service
Restart=always
[Install]
WantedBy=multi-user.target
This minimal configuration works but leaves the service exposed to attacks. Let’s harden it step by step.
2. The Least Privilege Principle: Running Services as Non-Root
By default, systemd services run as the root user, granting full system access if compromised. The first hardening step is to enforce the least privilege principle by running services as a dedicated, non-privileged user.
2.1 Define a Dedicated User/Group
Create a system user/group with no login shell and limited home directory access:
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/example-service example-user
Update the service file to use this user:
[Service]
User=example-user
Group=example-user
2.2 Use DynamicUser=yes for Ephemeral Services
For short-lived or stateless services (e.g., cron jobs), use DynamicUser=yes to auto-create a temporary, unprivileged user that is deleted when the service stops:
[Service]
DynamicUser=yes
StateDirectory=example-service # Auto-creates /var/lib/example-service with correct permissions
Why It Matters: Even if the service is compromised, the attacker gains access only to the temporary user’s limited context, not root.
3. Hardening Service Files: Key Directives
Systemd provides dozens of directives to restrict service capabilities. Below are critical ones to implement.
3.1 Filesystem Isolation
Limit access to sensitive directories to prevent data exfiltration or tampering.
| Directive | Purpose | Example |
|---|---|---|
ProtectSystem=strict | Makes /usr, /boot, and /etc read-only; /var remains writable. | ProtectSystem=strict |
ProtectHome=read-only | Makes /home, /root, and /run/user read-only (or tmpfs if empty). | ProtectHome=read-only |
PrivateTmp=yes | Isolates /tmp and /var/tmp to a private tmpfs for the service. | PrivateTmp=yes |
ReadOnlyPaths=/etc | Explicitly marks paths as read-only (override ProtectSystem if needed). | ReadOnlyPaths=/etc /usr/local/bin |
InaccessiblePaths=/proc/sys | Hides paths entirely (service cannot see them). | InaccessiblePaths=/proc/sys /root |
3.2 Privilege Restriction
Prevent the service from gaining elevated privileges or executing dangerous operations.
| Directive | Purpose | Example |
|---|---|---|
NoNewPrivileges=yes | Blocks setuid/setgid binaries and CAP_SYS_ADMIN from elevating privileges. | NoNewPrivileges=yes |
CapabilityBoundingSet= | Limits Linux capabilities (e.g., CAP_NET_BIND_SERVICE for low ports). | CapabilityBoundingSet=CAP_NET_BIND_SERVICE |
AmbientCapabilities= | Grants specific capabilities without full root (use with User=). | AmbientCapabilities=CAP_NET_BIND_SERVICE |
NoExec=yes | Prevents execution of binaries in the service’s temporary directories. | NoExec=yes |
3.3 Network and Resource Limiting
Restrict network access and resource usage to contain attacks.
| Directive | Purpose | Example |
|---|---|---|
RestrictAddressFamilies=AF_INET AF_INET6 | Limits network protocols (e.g., block raw sockets). | RestrictAddressFamilies=AF_INET AF_INET6 |
PrivateNetwork=yes | Isolates the service from the host network (no external connectivity). | PrivateNetwork=yes |
MemoryLimit=512M | Caps RAM usage to prevent DoS attacks. | MemoryLimit=512M |
CPUQuota=50% | Limits CPU usage to 50% of a core. | CPUQuota=50% |
4. Securing Service Execution
Even with isolation, how a service starts and runs can introduce risks.
4.1 Use Absolute Paths in ExecStart=
Always specify full paths for executables to avoid path hijacking:
# Bad: Relative path or unqualified command
ExecStart=example-service
# Good: Absolute path
ExecStart=/usr/bin/example-service
4.2 Avoid Shell Execution
Systemd executes ExecStart= directly by default, but using shell=yes or appending |/& forces shell execution, which is risky (shell injection). Disable it with NoShell=yes (or omit shell features):
# Bad: Uses shell for redirection
ExecStart=/usr/bin/example-service > /var/log/example.log
# Good: Use systemd's logging instead
ExecStart=/usr/bin/example-service
StandardOutput=journal # Logs to systemd-journald
4.3 Restrict Device Access
Prevent access to physical devices (e.g., /dev/sda) with:
PrivateDevices=yes # Isolates /dev to a minimal set (null, zero, random, etc.)
DeviceAllow=/dev/null rw # Explicitly allow specific devices if needed
5. Monitoring and Auditing Systemd Services
Hardening is useless without visibility. Use these tools to detect anomalies.
5.1 Monitor Logs with journalctl
Systemd logs all service activity to systemd-journald. Filter logs for your service:
# Follow real-time logs for "example-service"
journalctl -u example-service -f
# Show critical errors
journalctl -u example-service -p err
Tip: Use Persistent=true in /etc/systemd/journald.conf to retain logs across reboots.
5.2 Audit Service Activity with auditd
The auditd daemon tracks system calls and file access. Add rules to monitor your service:
# Monitor execution of the service binary
sudo auditctl -a always,exit -F path=/usr/bin/example-service -F perm=x -k example-service-exec
# Monitor writes to /var/lib/example-service
sudo auditctl -a always,exit -F dir=/var/lib/example-service -F perm=w -k example-service-write
View audit logs with ausearch -k example-service-exec.
5.3 Score Hardening with systemd-analyze security
Systemd provides a built-in tool to assess service security. Run:
systemd-analyze security example-service
It returns a score (0 = safest, 10 = least safe) and flags missing hardening directives. Aim for a score ≤ 5.
6. Advanced Hardening Techniques
6.1 Seccomp Filters with SystemCallFilter=
Restrict the service to essential system calls (syscalls) using SystemCallFilter=. Block dangerous syscalls like fork, execve, or mount:
SystemCallFilter=@system-service # Allow common syscalls for services
SystemCallFilter=~@privileged # Block privileged syscalls (mount, chroot, etc.)
SystemCallFilter=~fork execve # Explicitly block specific syscalls
6.2 AppArmor/SELinux Profiles
Pair systemd with Mandatory Access Control (MAC) frameworks like AppArmor or SELinux for granular policy enforcement.
Example AppArmor Profile (/etc/apparmor.d/usr.bin.example-service):
#include <tunables/global>
/usr/bin/example-service {
# Allow execution
/usr/bin/example-service mr,
# Allow read access to configs
/etc/example-service/** r,
# Allow write to state directory
/var/lib/example-service/** rw,
# Deny everything else
deny /home/** rw,
}
Enable the profile with:
sudo aa-enforce /etc/apparmor.d/usr.bin.example-service
6.3 CGroup Resource Limits
Use systemd’s cgroup integration to limit CPU, memory, and I/O:
MemoryLimit=512M # Max RAM
CPUQuota=50% # Max CPU (50% of one core)
IOReadBandwidthMax=/dev/sda 10M # Limit read speed to 10MB/s
7. Common Pitfalls to Avoid
- Running as Root: Never leave
User=rootunless absolutely necessary (e.g., kernel modules). - Overly Permissive
CapabilityBoundingSet: AvoidCAP_SYS_ADMINorCAP_ALL—only grant required capabilities. - Ignoring
NoNewPrivileges=yes: This blocks most privilege escalation paths; always enable it. - Missing Logging: Disabling logs (e.g.,
StandardOutput=null) hides attack evidence. - Not Testing Hardening: Use
systemd-analyze securityand manual testing (e.g., try writing to/etc) to validate restrictions.
8. Conclusion
Securing systemd services is a continuous process that combines least privilege, isolation, and monitoring. By implementing the directives and techniques outlined here—running as non-root users, restricting filesystem/network access, and auditing activity—you significantly reduce the attack surface of your Linux systems.
Regularly review service configurations with systemd-analyze security, update hardening rules as services evolve, and stay informed about new systemd features (e.g., SystemCallArchitectures= for 32-bit/64-bit restrictions). With these practices, you’ll build a resilient foundation for your infrastructure.