Systemd Service Hardening
Introduction
In an age where hacker attacks are a daily occurrence, it is of fundamental importance to minimize the attack surface. Containerization is probably the best way to isolate a service provided for the public, but this is not always possible for several reasons. For example, think of a legacy system application developed on systemd. This could make the most of the capabilities provided by a systemd-based operative system and it could be managed via a systemd unit, or it could automatically pull updates using a systemd timer, and so on.
For this reason, we are going to explain how to improve the security of a systemd service. But first, we need to step back for a moment. With the latest releases systemd has implemented some interesting features relating to security, especially sandboxing. In this article we are going to show step-by-step how to strengthen services using specific directives, and how to check them with the provided systemd suite.
Debugging
Systemd provided an interesting tool named systemd-analyze. This command analyzes the security and the sandboxing settings of one or more specified services. The command checks for various security-related service settings, assigning each a numeric "exposure level" value, depending on how important the setting is. It then calculates an overall exposure level for the whole unit through an estimation in the range 0.0…10.0, which tells us how exposed a service is security-wise.
This allows us to check the improvements applied to our systemd service step-by-step. As you can see, several services are now marked as UNSAFE, this is probably due to the fact that not all of the applications are applying the features provided by systemd.
Getting Started
Let's start from a basic example. We want to create a systemd unit to start the command python3 -m http.server
as a service:
[Unit] Description=Simple Http Server Documentation=https://docs.python.org/3/library/http.server.html [Service] Type=simple ExecStart=/usr/bin/python3 -m http.server ExecStop=/bin/kill -9 $MAINPID [Install] WantedBy=multi-user.target
Save the file and place it under the specific systemd directory of yor distribution.
By checking the security exposure through systemd-analyze security
we get the following result:
The security value is now 9.6/10 and it is marked as UNSAFE. Let's see now how to strengthen the current service to make it safer.
PrivateTmpIt creates a file system namespace under /tmp/systemd-private-*-[unit name]-*/tmp
rather than a shared /tmp
or /var/tmp
. Many of the unit files released with Red Hat Enterprise Linux include this setting, which removes an entire class of vulnerabilities related to the prediction and replacement of files used in /tmp
. [4]
This is how the service appears after we insert the following directive:
[Unit] Description=Simple Http Server Documentation=https://docs.python.org/3/library/http.server.html [Service] Type=simple ExecStart=/usr/bin/python3 -m http.server ExecStop=/bin/kill -9 $MAINPID # Sandboxing features PrivateTmp=yes [Install] WantedBy=multi-user.target
This is the result we get from systemd-analyze
:
simplehttp.service 9.2 UNSAFE ?
Good! We lowered it from 9.6 to 9.2. Let's see how to make it even safer.
NoNewPrivilegesIt prevents the service and related child processes from escalating privileges. [4] Add the following row:
NoNewPrivileges=true
The next result is:
simplehttp.service 9.0 UNSAFE ?RestrictNamespaces
It limits all or a subset of namespaces to the service. The directive accepts cgroup
, ipc
, net
, mnt
, pid
, user
, and uts
. [4]. Add the following row:
RestrictNamespaces=uts ipc pid user cgroup
As you can see above, the net
namespace has not been set since the service needs to bind itself on a network interface. Isolating net
from a network service will make it useless.
simplehttp.service 8.8 EXPOSED ?Final results
Once we add the other directives to the service, we get a service like this:
[Unit] Description=Simple Http Server Documentation=https://docs.python.org/3/library/http.server.html [Service] Type=simple ExecStart=/usr/bin/python3 -m http.server ExecStop=/bin/kill -9 $MAINPID # Sandboxing features PrivateTmp=yes NoNewPrivileges=true ProtectSystem=strict CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH RestrictNamespaces=uts ipc pid user cgroup ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes PrivateDevices=yes RestrictSUIDSGID=true IPAddressAllow=192.168.1.0/24 [Install] WantedBy=multi-user.target
It finally reaches this result:
simplehttp.service 4.9 OK ?
We lowered it from 9.6 to 4.9, which is a very good result. Now the entire system is partially secure.
Conclusions
We are now able to improve our system security. But remember we won’t always need to apply all of the systemd directives. That is why we have to check them step-by-step to be sure they are all valid. We also don’t need to reach a low value for each service. What is important is to protect our system using the right precautions.
You can find here a little Ansible playbook to setup a demo about the following article. This could help you to make some practice with this amazing feature introduced by systemd.
Alessio Greggi is a Computer Scientist graduated at the University of Rome, Tor Vergata. He has been working as Security Analyst and DevOps. He mostly works around Shell Scripting, Python, Go and Ansible. You can reach Alessio via LinkedIn.