First of all: What is a systemd service ? systemd is an init system in the Linux world, similar but not identical to init or SysVinit init systems (see [8]). There has been much discussion about this, but in the end all major distributions have for some time now switched to systemd. Essentially, systemd moves away from shell scripts to a declarative notation for services.
Each service in systemd is described by a unit file, usually located in /usr/local/lib/systemd/system and ending with .service. Once a service is enabled, a link is created in /etc/systemd/system .
There are several angles to optimizing a systemd service. In this post, I will focus on security-related topics using OpenRGB as an example. OpenRGB is a project to control all the fancy colorful lights that are available for today’s desktop computers and their accessories, especially relevant for gamers. It is available for Linux and Windows and replaces all the bloatware wonderful kinds of software that come with each kind of RGB gadget that you buy.
I will start with the example [3] from the Arch Linux repository (not the one from the OpenRGB FAQ). OpenRGB stores its config files in ~/.config/OpenRGB/, we will need this information later.
[Unit]
Description=Run openrgb server
After=network.target
[Service]
RemainAfterExit=no
ExecStart=/usr/bin/openrgb --server --noautoconnect
Restart=always
[Install]
WantedBy=multi-user.target
Code language: JavaScript (javascript)
First we check whether our newly defined service works
sudo systemctl daemon-reload
sudo systemctl restart openrgb
sudo systemctl status openrgb
So what does this all do ? Let me explain:
In the [Unit] section we explain in the Description part in plain text what this service is about and in the After part which services need to start before this service can be started. In our case we need network.target (because our server will open a port).
In the [Service] section we define the properties of the service. This is the main area we will be working on in this post. RemainAfterExit meaning that the service should be considered running when the process is terminated. ExecStart is the actual command that is run. Restart defines the behavior when the service is terminated, in this case we want to restart the service no matter why it terminated.
And finally, in [Install] with WantedBy we define when the service should be started. In this case as soon as we enter the multi-user stage, more or less the same as init level 3 to 5 in SysVinit.
Caveat: With SELinux enabled (as it should be), that systemd definition would not get us far should the executable be in another directory than /usr/bin, because /usr/bin is one of the pre-approved directories for executables.
The next step is to analyze our (very simple) service using
systemd-analyze security openrgb
What does this do ? With systemd-analyze [4] we can check and/or verify different aspects of systemd, e.g. boot time, service startup time, etc. Because we focus on security in this example, we will just analyze security-related aspects of our service, mainly sandbox-related attributes. Basically, systemd puts each service in a sandbox. Using this sandbox, systemd can limit various system aspects of a service. If you want to know more, please take a look at [9].
On this first run we get “9.6 UNSAFE”, which is not great. But we will improve this rating step by step. In the [Service] part of the service definition file we can add the initial options from one of the available systemd hardening guides (using [5] in this case).
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
DevicePolicy=closed
ProtectSystem=strict
ProtectHome=read-only
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
MemoryDenyWriteExecute=yes
LockPersonality=yes
We retry with our newly found knowledge and – nothing works anymore. Lesson learned: Never blindly copy stuff from Slashdot or GitHub. So: what went wrong ? The openrgb service in fact needs access to i2c devices, because that is where the fancy RGB stuff is (i2c is a standard chip interface used for hardware monitoring/interaction and is found on most modern mainboards). So we are taking this step by step.
For starters, we just use the first two options from the hardening guide. This should be a no-brainer: We don’t need new privileges (NoNewPrivileges=yes) and we are fine with a private directory for temporary files (PrivateTmp=yes).
After changing the unit file, let’s do another check using systemd-analyze security openrgb. Now we get “9.0 UNSAFE”. Better, but not good enough. Let’s have a look at the other options one by one and find out what we can do to get better check results.
So what does PrivateDevices=yes do ? Essentially it prevents access to physical devices. This is not what we want in this case.
DevicePolicy=closed will prevent access to physical devices unless explicitly allowed. Good choice for regular software, but not for us.
ProtectSystem=strict mounts everything read-only with the exception of /dev, /proc and /sys. That seems reasonable in our case, so we will use it.
ProtectControlGroups=yes protects the Linux Control Groups from modification. Our service does not do anything with control groups, so we choose the more secure option.
ProtectKernelModules=yes prevents explicit Kernel module loading. Our service should not do anything like this, so we can enable this setting.
ProtectKernelTunables=yes prevents Kernel tunables from being modified. Again, we don’t want to modify any Kernel parameters, so we default to the more secure option.
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK This setting will restrict the available socket address families. This setting is a safe bet for most regular services.
RestrictNamespaces=yes With this setting access to namespacing functionality is restricted. This service doesn’t do anything with namespaces, so we – again – default to the more secure setting.
RestrictRealtime=yes Prevents the service from enabling realtime scheduling policies, which could be used to fully occupy the system (Denial-of-Service Attack).
RestrictSUIDSGID=yes Prevents the setting of the SUID or GUID on files or directories. Again, something this service has no intention to do, so defaulting to security.
MemoryDenyWriteExecute=yes Prevents the creation or modification of memory mappings as executable. Again defaulting to the more secure option.
LockPersonality=yes Prevents the change of the personality settings for this process.
After adding the options that make sense for us, let’s check again with systemd-analyze security openrgb . We get “6.6 MEDIUM”. Better, but still not good enough.
Now we tackle the most obvious choice (and at the same time implement an often overlooked security best practice): We define a user and a group. This is one of the attributes with the most impact on the security rating. To avoid the hassle of maintaining a separate user and group for just this purpose, we could use a systemd feature called DynamicUser. In a nutshell, user and group will be created dynamically at the start of the service and will vanish when the service is stopped. Unfortunately, because the config information and the logs are stored in the home directory of the user running the service, we can’t use DynamicUser. So we’ll just use a static user definition by adding the following line after having created the openrgb user and moving over the configuration from beneath the root home directory.
User=openrgb
A quick check with systemd-analyze security openrgb results in “6.2 MEDIUM” . Unfortunately, the service can’t access the hardware devices anymore although proper udev rules are installed, so we need to revert the change in this case – and we will fall back to “6.6 MEDIUM” again. There are still a few other options we haven’t tackled yet, so let’s try those.
ProtectClock=yes denies all write requests to the hardware clock. But it also does a lot of magic behind the scenes that essentially prevents access to the hardware. Generally a good idea, but not useful in our case. So we set this to no.
ProtectHostname=yes prevents the service from changing the hostname and/or react to changes of the hostname. We can enable this setting because OpenRGB does not care about the hostname at all.
ProtectKernelLogs=true denies access to the Kernel log ring buffer. We don’t need this kind of access, so we can opt for more security.
PrivateUsers=yes essentially prevents the access to the home directories of other users. We don’t need any of this, so we can enable this setting.
CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE disables (via the ‘~’ sign) various potentially dangerous capabilities that this service doesn’t need anyway.
Another quick check with systemd-analyze security openrgb results in “6.2 MEDIUM” . So we have at least compensated the loss of having a dedicated service user.
After some iterations to find more useful attributes for the CapabilityBoundingSets we have a much better result now. The final outcome looks like this
[Unit]
Description=Run OpenRGB server
After=network.target lm_sensors.service
[Service]
RemainAfterExit=yes
ExecStart=/usr/bin/openrgb --server --noautoconnect
Restart=always
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectControlGroups=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
MemoryDenyWriteExecute=yes
LockPersonality=yes
ProtectClock=no
ProtectHostname=yes
ProtectKernelLogs=yes
PrivateUsers=yes
CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_RAWIO CAP_SYS_TIME CAP_SYS_TTY_CONFIG
CapabilityBoundingSet=~CAP_WAKE_ALARM CAP_MAC_ADMIN CAP_MAC_OVERRIDE
CapabilityBoundingSet=~CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_CHOWN CAP_NET_ADMIN
CapabilityBoundingSet=~CAP_CHOWN CAP_FSETID CAP_SETFCAP
CapabilityBoundingSet=~CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER
[Install]
WantedBy=multi-user.target
Code language: JavaScript (javascript)
And this definition yields “4.5 OK” with systemd-analyze security openrgb. Having achieved that, there is just one thing left: Sending a merge request!
Links
[1] OpenRGB on GitLab – https://gitlab.com/CalcProgrammer1/OpenRGB
[2] OpenRGB FAQ – https://gitlab.com/CalcProgrammer1/OpenRGB/-/wikis/Frequently-Asked-Questions#can-i-set-up-openrgb-as-a-systemd-service
[3] ArchLinux openrgb – https://aur.archlinux.org/cgit/aur.git/tree/openrgb.service?h=openrgb
[4] Systemd-analyze man page – https://www.freedesktop.org/software/systemd/man/systemd-analyze.html
[5] Systemd hardening options – https://gist.github.com/ageis/f5595e59b1cddb1513d1b425a323db04
[6] FOSDEM 2020 – Using systemd security features to build a more secure distro – https://archive.fosdem.org/2020/schedule/event/ussftbasd/
[7] Open Source Libs – Systemd Service Hardening – https://opensourcelibs.com/lib/systemd-service-hardening
[8] The story behind ‘init’ and ‘systemd’ – https://www.tecmint.com/systemd-replaces-init-in-linux/
[9] systemd-analyze man page – https://www.freedesktop.org/software/systemd/man/systemd-analyze.html
One reply on “Optimizing a systemd service for security”
[…] Optimizing a systemd service for security, […]