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.

Systemd Analyze

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:

Systemd Service Start

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.

PrivateTmp

It 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.

NoNewPrivileges

It 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.

Posted by Contributor