funwithlinux guide

Securing Systemd Services: Best Practices and Techniques

Systemd has become the de facto init system for most Linux distributions, managing everything from service startup and supervision to logging and process control. While its flexibility and feature richness simplify system administration, misconfigured systemd services can expose critical vulnerabilities—from privilege escalation to data leaks. Securing systemd services is therefore foundational to overall Linux system security. This blog explores actionable best practices and advanced techniques to harden systemd services, ensuring they run with minimal privilege, limited attack surface, and robust monitoring. Whether you’re a system administrator, DevOps engineer, or security practitioner, these guidelines will help you mitigate risks and fortify your infrastructure.

Table of Contents

  1. Understanding Systemd Service Units
  2. The Least Privilege Principle: Running Services as Non-Root
  3. Hardening Service Files: Key Directives
    • 3.1 Filesystem Isolation
    • 3.2 Privilege Restriction
    • 3.3 Network and Resource Limiting
  4. Securing Service Execution
  5. Monitoring and Auditing Systemd Services
  6. Advanced Hardening Techniques
  7. Common Pitfalls to Avoid
  8. Conclusion
  9. 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.target for 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.

DirectivePurposeExample
ProtectSystem=strictMakes /usr, /boot, and /etc read-only; /var remains writable.ProtectSystem=strict
ProtectHome=read-onlyMakes /home, /root, and /run/user read-only (or tmpfs if empty).ProtectHome=read-only
PrivateTmp=yesIsolates /tmp and /var/tmp to a private tmpfs for the service.PrivateTmp=yes
ReadOnlyPaths=/etcExplicitly marks paths as read-only (override ProtectSystem if needed).ReadOnlyPaths=/etc /usr/local/bin
InaccessiblePaths=/proc/sysHides 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.

DirectivePurposeExample
NoNewPrivileges=yesBlocks 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=yesPrevents 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.

DirectivePurposeExample
RestrictAddressFamilies=AF_INET AF_INET6Limits network protocols (e.g., block raw sockets).RestrictAddressFamilies=AF_INET AF_INET6
PrivateNetwork=yesIsolates the service from the host network (no external connectivity).PrivateNetwork=yes
MemoryLimit=512MCaps 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=root unless absolutely necessary (e.g., kernel modules).
  • Overly Permissive CapabilityBoundingSet: Avoid CAP_SYS_ADMIN or CAP_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 security and 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.

9. References