Three weeks ago, I wrote
systemd service sandboxing and security hardening 101: an introduction to Linux security features for service processes managed by
This week, I’ll explore how you can use some of the more advanced security features offered by
systemd. You’ll want to read the 101-introduction before proceeding with this article.
Last week, researchers at Qualys disclosed a remote code execution (RCE) vulnerability in OpenSMTPD: an open-source email server. This seems like an opportune time to make sure you’ve locked down this service. It will serve as our example service for this tutorial.
Most parts of OpenSMTPD is designed to run in unprivileged processes. However, this was a “worst-case scenario”, as Gilles Chehade put it. The vulnerability lets attackers execute remote commands with full administrative privileges. Remotely executed arbitrary code running rampant is the last thing you want on your email server.
I’m an OpenSMTPD user and my server was actively targeted by this vulnerability. Hopeful attackers were actively trying to exploit it hours after the vulnerability was disclosed publicly. I’ll be honest here: I’d not done my job properly and hadn’t secured the
smtpd service running on my system. Sheer dumb luck protected my system against exploitation.
Sandboxing is all about restricting a service’s permissions, access, and capabilities to a point where it can’t perform any options except what’s expected of it. To block unexpected behavior, you’ll need to gather detailed knowledge of how it’s expected to work.
I’ll start with some of the most complex security directives first. These can make the most impact to your system security.
I suggest you apply directives one at a time and thoroughly test that your service still performs as expected after applying each one.
Linux kernel capabilities are per-process thread security policies that control access to specific kernel features. You can increase security and reduce the impact of an intrusion by limiting services to expected capabilities only.
The available capability sets are listed in the
capabilities(7) manual page.
If you don’t know what capabilities your service requires then here’s how to work it out. You’ll notice that the manual page lists system calls (e.g.
chroot(2)). If you’re securing an open-source program service, then you can locate and search through its source code for these specific system calls.
Alternatively, you can try removing all capabilities and see if and how the service fails. Explicitly set the
CapabilityBoundingSet= directive to nothing to remove all capabilities.
I was familiar with OpenSMTPD’s architecture so I knew which capabilities it would need. I’ve set the following allow-listed capabilities for my instance of the OpenSMTPD’s
OpenSMTPD runs as the root user and spawns unprivileged child processes in a minimal root directory. It needs the capability to change the root directory (
chroot) and assign these processes user and group identities (UID/GUID). Lastly, the process needs to listen to several privileged network ports (ports below 1024).
We don’t want any of these child processes (or any compromised process spawned from the process) to inherit these capabilities. A compromised process could for example potentially take over the role as the web server (ports 80 and 443).
AmbientCapabilities option to nothing to prevent any child processes from inheriting their parent’s capabilities.
Allow-listing system calls
We can also block or allow-list specific system calls (syscall) or sets of syscalls. This gives an even more granular control than kernel capability sets.
systemd.exec(5) manual page contains a few predefined syscall sets.
@system-service is one such set. It contains some common syscalls used by services. It notably removes the ability to reboot the system, interfere with swap memory, and change the system clock. Few services need these capabilities.
I’ve set the following allow-list of syscalls for OpenSMTPD:
@mount in order to use
Limiting access to the file system
The Linux kernel can change how a process sees the file system. This can be a powerful way to limit what a compromised process has access to and how much damage it can cause to your system.
You can prevent it from reading into the
/tmp (temporary files) of other processes or reading or writing to
/dev (hardware devices including disks). You can also mount the entire file system read-only.
This last capability can help prevent malicious code running on your system from getting a persistent foothold on your system.
The following configuration does all of the above:
However, the OpenSMTPD email server wouldn’t be much use to anyone if it couldn’t write anywhere on the disk. It needs access to some specific directories to store runtime information, state, and your emails OpenSMTPD store all of these things under different points in
You can override the read-only file-system restriction for specific paths using the
ReadWritePaths directive. However, you can make things even more secure by creating a fake temporary file system and only mapping the required directories to it. This will act as an allow-list of read-write paths.
The following example configuration for OpenSMTPD demonstrates this approch:
By now, you’re probably wondering how to identify the above list of directories. It may be different in your configuration! I’ll walk you through the process for the
/etc (configuration) directory.
You can work out the required directories by reading the available documentation and source code. However, this can take all day and you may simply not have the required programming skills.
The easiest way is to run the software through
strace and use it as normal.
strace can track and print the file system operations a program performs. More importantly, it can print the file paths a program interacts with.
The following command launches OpenSMTPD and traces which files it interacts with under the
This should give you the information you need to create an allow-list. Note that you must use the program normally for a while as its behavior, and thus file operations, may change over time and when using different functionality.
Here’s the configuration I came up with for my instance of OpenSMTPD.
These paths all fit with what we’d expect from an email service. They’re notably all read-only access. It can’t suddenly change the DNS resolvers, interfere with cyrpto-library policies, or change the configuration of a completely unrelated program.
The system’s persistent configuration should remain fairly safe in the event of a service compromise.
You can build on the above to cover other root-level directories too. However, you may want to consider a blocklist instead. It can be far quicker to come up with and you can share it among multiple different services.
The following blocklist restricts access to programs commonly invoked in shell-based remote execution vulnerabilities.
Your email server is unlikely to have a legitimate reason to execute any of these programs. Neither do many of your other services.
Prohibit risky and obscure behavior
There are a couple of more security directives in
systemd.exec(5) manual page. You can likely enable all of the following directives for most of the services on your systems with no ill effects. (Except for the first directive.)
I recommend you enable them one by one and test your service after enabling each one.
I’ll not go into more detail on any of these directives. Some of them are self-explanatory but they all prohibit the use of rarely used and obscure features. You can find more information about each of them in the manual page.
I hope this article has raised your awareness of some of the security features you can enable through
systemd. Over time, I believe many Linux distributions will enable at least some of these directives by default for most of their
systemd.service. Ultimately, it will be up to systems administrators to configure more restrictive and secure policies based on their usage.
These directives combined would have stopped the specific remote code execution vulnerability that afflicted OpenSMTPD. However, the key takeaway is that you should strive to sandbox long-running and internet-exposed services. There’s no need for your web server to be able to load a kernel module, your email server to change the hostname, or your DNS server to launch
wget and schedule reoccurring tasks with
You shouldn’t rely on
systemd alone to save the day. However, it can be an additional layer of security to help protect your systems in the event of an acively exploited remote code execution vulnerability.