The most surprising thing about systemd’s capability dropping is that it doesn’t actually remove privileges; it grants them, but only the bare minimum needed.

Let’s see it in action. Consider a simple service that just needs to bind to a privileged port (like 80 or 443). Without capabilities, this would require running the entire process as root.

[Unit]
Description=Simple HTTP Server

[Service]
ExecStart=/usr/bin/python3 -m http.server 80
User=nobody
Group=nogroup
# Now, let's drop privileges, but grant what's needed
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Here, CapabilityBoundingSet acts like a ceiling, preventing the process from ever gaining more capabilities than listed. AmbientCapabilities are the capabilities that persist after the process drops root. When this service starts, systemd will ensure that /usr/bin/python3 is executed with only CAP_NET_BIND_SERVICE in its effective, permitted, and inheritable sets, while the User is nobody. The Python script can then bind to port 80, but it can’t, for example, open raw sockets or manipulate network interfaces, because those capabilities were never granted.

This mechanism is built around the Linux Kernel’s "capabilities" system, which breaks down the monolithic power of root into smaller, distinct privileges. Traditionally, a process was either root (with all privileges) or not root (with none). Capabilities allow for fine-grained control. systemd leverages this by allowing you to declare, per-unit, which specific capabilities a service needs.

The core problem this solves is the principle of least privilege. If a web server, for instance, only needs to bind to a privileged port and serve files, it shouldn’t have the ability to remount filesystems, kill arbitrary processes, or load kernel modules. By using CapabilityBoundingSet and AmbientCapabilities, systemd ensures that even if a vulnerability is found in the service’s code, the attacker’s ability to exploit it is severely limited to only what the service was explicitly allowed to do.

The User and Group directives in the unit file are still crucial. They define the identity under which the process runs. Capabilities are then layered on top of this identity. So, User=nobody combined with AmbientCapabilities=CAP_NET_BIND_SERVICE means the process runs as the nobody user, but it also has the specific permission to bind to network ports, which the nobody user by itself would not have.

It’s important to understand the interplay between CapabilityBoundingSet, AmbientCapabilities, SecureBits, and the User/Group directives. CapabilityBoundingSet is the most restrictive; it defines the maximum set of capabilities the process can ever possess. AmbientCapabilities are capabilities that the service can retain even after dropping root privileges (e.g., if it starts as root and then uses setuid() to switch to a non-root user). SecureBits can further restrict how capabilities are handled, for example, preventing a process from ever regaining full root privileges.

The one thing most people don’t realize is that systemd doesn’t just drop capabilities; it actively sets them for the process. When you specify AmbientCapabilities=CAP_NET_BIND_SERVICE, systemd is ensuring that this capability is present in the process’s capability sets before it even starts executing your ExecStart command. This is distinct from a program manually dropping capabilities using cap_set_ambient() or similar syscalls.

The next thing you’ll likely run into is needing to manage file access for a service that has been restricted this way, specifically when the required files are not world-readable or writable.

Want structured learning?

Take the full Systemd course →