Systemd service hardening

First written 2020-01-25; Last updated 2026-07-01

Systemd provides many configuration settings to reduce privileges and restrict a service’s access, thereby hardening it against potential vulnerabilities. However, these settings are scattered throughout the documentation, making them more difficult to find than necessary. In addition, the commonly suggested settings are insufficient to restrict privileged processes (running as root or with special capabilities) because they can still access sensitive files like private keys (e.g., in /etc/) or sockets (e.g., in /run/).

Note
One remaining limitation of this setup is that privileged processes can still send signals to any other process. If possible, don’t run services as root but instead as a separate user (User, Group). Required capabilities can be granted even for non-root processes with AmbientCapabilities.

The following configuration snippet is a collection of all relevant hardening options I could find, followed by a short explanation of what they do and why they are useful; see the systemd man pages (man systemd.directives) for details.

systemd provides the systemd-analyze security command to check if a service is hardened. It does not account for all possible hardening settings, but gives a good overview of which services require further tweaks.

CapabilityBoundingSet=
KeyringMode=private
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateIPC=yes
PrivateMounts=yes
PrivateNetwork=yes
PrivateTmp=disconnected
PrivateUsers=yes
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
# Permit AF_UNIX for syslog(3) to aid debugging. This is also the default
# recommended by systemd. (Empty setting permits all families! A possible
# workaround would be to blacklist AF_UNIX afterwards.)
RestrictAddressFamilies=
RestrictAddressFamilies=AF_UNIX
RestrictFileSystems=@basic-api tmpfs btrfs ext4 xfs
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=
SystemCallFilter=@system-service
SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @privileged @raw-io @reboot @resources @setuid @swap userfaultfd mincore

# Restrict access to potentially sensitive data (kernels, config, mount
# points, private keys). The paths will be created if they don't exist and
# they must not be files.
TemporaryFileSystem=/boot:ro /etc/luks:ro /etc/ssh:ro /etc/ssl/private:ro /media:ro /mnt:ro /run:ro /srv:ro /var:ro
# Permit syslog(3) messages to journald
BindReadOnlyPaths=/run/systemd/journal/dev-log
Note
All settings marking mounts as read-only (e.g., ProtectSystem or ReadOnlyPaths) cannot protect mount points created after the service was started (see the systemd man page for ReadOnlyPaths for details).
All path-based restrictions (e.g., from the previous paragraph or TemporaryFileSystem) can be undone by a privileged process that can perform mount syscalls. The CapabilityBoundingSet and SystemCallFilter settings above prevent this, but one should be aware of this potential risk.

When restricting existing services, I use systemctl edit $service to create an override file with these settings (or I manually place the file in the appropriate location). This ensures that my settings override the default restrictions of the service and persist through system updates.

After this block of default options, specific settings can be changed or extended. For example, PrivateUsers is often too strict; adding PrivateUsers=no after this block will restore the default. Or, to permit access to keyring syscalls, one can add SystemCallFilter=@keyring. Having the default options first, followed by service-specific modifications, makes it easy to update the baseline settings across multiple service files.

CapabilityBoundingSet restricts the capabilities (man 7 capabilities) of this service; setting it to an empty value removes all capabilities. Capabilities allow for more fine-grained permissions; for example, CAP_NET_RAW allows creating raw network sockets without needing root privileges.

LockPersonality prevents changing the “process execution domain” (man 2 personality), a rarely used feature with potential bugs.

MemoryDenyWriteExecute prevents memory mappings that are both writable and executable to mitigate (simple) exploits.

NoNewPrivileges prevents the process from gaining any additional privileges during exec (man 2 execve); for example, when running setuid or setcap programs.

Private* provides a separate instance of the named feature to the process. This way, devices (PrivateDevices), System V IPC and POSIX message queues (PrivateIPC), mounts (PrivateMounts), network interfaces (PrivateNetwork), /tmp/ and /var/tmp/ directories (PrivateTmp) and users (PrivateUsers) are isolated from the regular system and the process cannot modify them. For temporary directories, this also protects the process from other users of the system; for example TOCTOU (time-of-check time-of-use, Wikipedia) races in /tmp/ can no longer be used to attack the process. These settings use Linux’s namespaces (man 7 namespaces) to provide isolation. For finer-grained access to devices, use DeviceAllow and DevicePolicy. To restrict network access based on IP addresses, use IPAddressAllow and IPAddressDeny.

Protect* restricts access to the named features. This prevents the process from modifying cgroups (man 7 cgroups, ProtectControlGroups), sysctls and other kernel tunables in /proc/ and /sys/ (ProtectKernelTunables), kernel modules (ProtectKernelModules) and the hostname (ProtectHostname). ProtectHome=yes (other values are possible) makes /home/, /root/ and /run/user/ inaccessible. ProtectSystem=strict (other values are possible) mounts the whole file system hierarchy read-only (except for /dev/, /proc/, /sys/; those are protected by PrivateDevices, ProtectKernelTunables, ProtectControlGroups). ReadWritePaths can be used to grant write-access selectively. Most of these settings are also implemented using namespaces.

Restrict* also restricts access to the named features. This controls supported address families (RestrictAddressFamilies), accessible file systems (RestrictFileSystems), namespaces (RestrictNamespaces), real-time scheduling (RestrictRealtime) and the ability to set suid/guid bits on files/directories (RestrictSUIDSGID). Note that setting RestrictAddressFamilies to an empty value permits all address families! This is unlike other options where an empty value is the most restrictive.

SystemCallFilter restricts access to syscalls via seccomp (man 2 seccomp). First, the setting is reset to the default (first line); then, the systemd defaults for services are permitted (second line), followed by the removal of additional syscalls that are generally unnecessary for most services (third line). Two extra syscalls are blacklisted: userfaultfd, which can be used to facilitate timing-sensitive attacks, and mincore, which can leak kernel information. By default, the process is terminated when a restricted syscall is used. With SystemCallErrorNumber, the specified error is returned instead.

TemporaryFileSystem mounts tmpfs (read-only with :ro suffix, other settings possible) over the specified directories. This is similar to InaccessiblePaths, which also prevents access to the directory’s contents, but TemporaryFileSystem permits nested mounts to provide access to sub-directories. In the example, this is used with BindReadOnlyPaths to permit logging to syslog. To provide write access to sub-directories, use BindPaths in combination with ReadWritePaths.

This restrictive use of TemporaryFileSystem is especially important for privileged processes, which still have access to all root-owned files despite the restrictions mentioned above. Since this often includes private keys, restricting access via TemporaryFileSystem is very useful.

Last updated 2026-07-01

Impressum Datenschutzerklärung