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.