Linux Systemd Sandboxing: Ten Security Directives for Production Daemons

Most production Linux services run with default systemd unit configurations that grant full filesystem access and unrestricted access to kernel interfaces. When a network service or internal daemon is compromised through remote code execution, default process permissions give attackers an open path to read arbitrary files, inspect local user directories, and tamper with kernel parameters. Systemd ships native security directives that configure Linux namespaces, seccomp filters, and cgroup limits directly within the unit definition without external container overhead.

A compromised root daemon inside a sandboxed systemd service cannot load kernel modules, modify sysctl tunables, or traverse user home directories.

Administrators can apply these security directives directly to vendor units using systemd drop-in override files:

# /etc/systemd/system/myapp.service.d/security.conf
[Service]
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
MemoryDenyWriteExecute=yes
SystemCallFilter=@system-service ~@privileged ~@resources
ReadWritePaths=/var/log/myapp /var/lib/myapp

Checklist

  • ProtectSystem=strict – Mounts the entire filesystem hierarchy including /usr, /boot, /efi, and /etc as read-only. Any attempt by the service to write outside directories explicitly declared with ReadWritePaths returns an immediate EROFS error. Daemons that only persist state to specific application directories should define their writable storage explicitly. This blocks attackers with arbitrary file-write primitives from overwriting binaries, libraries, or system configuration files.
  • ProtectHome=yes – Makes /root, /home, and /run/user completely inaccessible to the service unit. The service process sees empty directories or receives EACCES when attempting to access user home directories. This prevents compromised daemons from reading SSH private keys, bash history logs, or user environment secrets. If a background worker requires access to a specific home folder, use ProtectHome=read-only or map directories with BindReadOnlyPaths.
  • PrivateTmp=yes – Allocates an isolated file system namespace for /tmp and /var/tmp using a private mount namespace. The daemon process cannot see or tamper with temporary files generated by other system processes. This mitigates local race conditions, symlink traversal attacks, and shared-file snooping vulnerabilities in world-writable temporary directories. The temporary directory unmounts and cleans up automatically when the service stops.
  • NoNewPrivileges=yes – Sets the PR_SET_NO_NEW_PRIVS bit on the service and its children. This prevents the process tree from gaining elevated privileges across execve calls, disabling setuid and setgid binaries such as sudo, passwd, or su. Even if an attacker executes a local setuid binary from disk, the kernel executes the binary with the unprivileged caller credentials without granting elevated permissions.
  • ProtectKernelTunables=yes – Mounts /proc/sys, /sys, /proc/sysrq-trigger, /proc/latency_stats, and /proc/acpi as strictly read-only. This prevents processes from altering kernel parameters at runtime. Attackers cannot disable network filtering rules, alter virtual memory behavior through /proc/sys/vm, or issue sysrq commands to force system crashes and memory dumps.
  • ProtectKernelModules=yes – Drops the CAP_SYS_MODULE capability and denies explicit module loading or unloading requests from the service process. The daemon cannot invoke init_module or finit_module system calls to insert kernel modules. Furthermore, /usr/lib/modules becomes inaccessible, preventing processes from installing rootkits to bypass user-space security controls.
  • ProtectControlGroups=yes – Mounts the /sys/fs/cgroup hierarchy as read-only inside the unit namespace. Daemons cannot alter cgroup resource allocations, manipulate process assignments, or escape resource limits. This directive prevents rogue processes from adjusting memory limits, CPU shares, or process controller bounds.
  • RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 – Restricts socket allocation to specified address families at the kernel level. By default, processes can allocate raw sockets, Netlink sockets, or packet sockets, which expose broad kernel surface area. Restricting the service to standard IPv4, IPv6, and local Unix domain sockets prevents exploitation of vulnerabilities in obscure network protocol implementations.
  • MemoryDenyWriteExecute=yes – Blocks memory mappings that are simultaneously writable and executable using mprotect and mmap system calls. The kernel disallows creating W^X violations and prevents turning existing writable pages into executable memory. This blocks runtime shellcode injection and memory corruption exploitation techniques. Interpreters with JIT compilers like Node.js or Java require this disabled or configured separately.
  • SystemCallFilter=@system-service ~@privileged ~@resources – Installs a seccomp filter on the service unit. The @system-service set permits standard system calls needed by network daemons while blocking dangerous syscalls. Appending ~@privileged blocks operations like reboot, kexec_load, and hardware clock manipulation. Appending ~@resources blocks resource limit modifications like setrlimit, nice, and ioprio_set.

After adding these directives, inspect the service security profile using the systemd security audit tool:

systemctl daemon-reload
systemctl restart myapp.service
systemd-analyze security myapp.service

The systemd-analyze utility grades unit exposure on a scale from 0.0 (safe) to 10.0 (permissive). Applying these ten sandboxing directives drops exposure scores from above 9.0 down below 2.0, adding layered defense to host-level workloads.

Press Cmd K to search برای جستجوی سایت از Cmd+K استفاده کنید