I set Linux capabilities to operate server services according to the principle of minimal access, thereby granting only the absolutely necessary partial permissions. This allows me to reduce the Attack surface noticeable, without blocking any functions.
Key points
- Least privilege Consistent: Services are provided only with the exact capabilities they need.
- fine-grained Instead of root: About 40–50 capabilities replace full access.
- Separation Processes: Privilege separation minimizes damage from exploits.
- File Capabilities: Bind permissions directly to binary files.
- Auditable: getcap provides a clear overview of special privileges.
Why Root Is Risky—and How Capabilities Change That
In the past, almost every server service ran on Root rights, which, if compromised, could immediately lead to a takeover of the system. Today, I assign permissions selectively by using capabilities such as CAP_NET_BIND_SERVICE assign for ports below 1024 and remove all other powerful privileges. This allows the web server to bind, but not to load kernel modules or change file ownership, which Security significantly increases. A clear separation of tasks makes attacks less effective because a compromised process is only allowed to perform a limited set of actions. Those who want to add more structure to this concept can fine-tune permissions very precisely break down in detail and thus systematically limit critical operations. A monolithic root service is thereby transformed into a set of services with minimal, clearly defined permissions.
How Capability Sets Work in the Kernel
Every process has several Capability Sets, which the kernel checks during sensitive operations. The effective set determines what a process is currently allowed to do, while the permitted set contains the pool of possible privileges. Using the inheritable set, I can control what happens when the execve() is passed on to child processes, which is particularly important for wrappers and startup scripts. The Bounding Set defines a strict upper limit, ensuring that certain capabilities can never be regained, even if errors occur in the application. With the Ambient Set, I pass capabilities to regular programs without SUID and maintain the Route of attack small. Together, these sets allow me to exercise very fine control that goes far beyond the classic "all-or-nothing" approach of UID 0.
| Set | Purpose | Typical use | Risk of incorrect configuration |
|---|---|---|---|
| Effective | Skills That Work Now | Verification of Each Privileged Operation | The process can immediately become too much |
| Permitted | Pool of Permitted Skills | Source for the effective set | Unnecessary reserves remain available |
| Inheritable | Inheritable Abilities | Controlled Data Passing in execve() | Children Inherit Rights Unnecessarily |
| Bounding | Upper Limit on All Rights | Define Permanent Exclusions | It Is Possible to Reclaim Significant Rights |
| Ambient | Passing without SUID | Regular programs are assigned capabilities | Broader, Quiet Granting of Rights |
In practice, two additional aspects are important: First, the decision Securebits whether a process continues to run after a change in the user ID (e.g., via setuid()) retains its capabilities. With PR_SET_KEEPCAPS This can be controlled in a targeted manner—typical procedure: start as root briefly, create the necessary sockets or resources, switch the UID to an unprivileged user, and retain only the required capabilities. Second, the Bounding Set is now permanently part of the current process flow. If you remove unnecessary capabilities early on in the startup path, you won't be able to acquire any „prohibited“ permissions later on, even due to misconfigurations.
Control File Permissions Using File Capabilities
Instead of a temporary position, a permanent one Special Rights To avoid this, I prefer to link them directly to the binary file. Via setcap cap_net_bind_service=+eip /usr/bin/node I allow port bindings without the process having to run as root. With getcap /usr/bin/node or recursively getcap -r / 2>/dev/null I review the assignments and maintain control. To remove them, use setcap -r /path/to/binary, so I revoke the temporary permissions after the task is complete. Capabilities are often lost during copying, so I explicitly preserve them during deployment in order to regressions to avoid this. This ensures that builds remain reproducible and that permissions are always documented in a traceable manner.
File capabilities are stored as extended attributes (security.capability) on the file system. This requires a compatible file system and appropriate mount options. Tools such as tar and rsync must explicitly include XAttrs (e.g.,. tar --xattrs, rsync -XA), otherwise permissions are silently removed. Package managers can set capabilities during post-installation steps; I prefer to define them in the build/release process to avoid surprises during upgrades. Another critical point is that interpreter scripts (e.g., those with a shebang) do not inherit file capabilities like ELF binaries do. Powerful capabilities on interpreter Setting it up is risky anyway—I prefer to decouple it and work with dedicated, small helper binaries.
The Principle of Minimalism for Server Services in Practice
I start the web server as an unprivileged user and grant only CAP_NET_BIND_SERVICE, so that the process can bind to 80/443 and no other Privileges provides. I continue to control files and directories using POSIX permissions and, optionally, MAC profiles, which keep configuration and content securely separated. Monitoring and logging agents are granted specific network permissions and read access to logs, but no permissions to make system changes. In container environments, I further reduce the capability set and combine it with system call filters to tightly control behavior. This combination mitigates the impact of successful exploits and increases the Transparency actual authority. Services remain operational, but the scope for action remains limited.
Instead of assigning capabilities, I sometimes eliminate them entirely: Socket activation provides privileged listeners (e.g., 443/tcp) via the init process and passes only the open file descriptor to the service. The application process then does not need a CAP_NET_BIND_SERVICE more. Similarly, one-time root actions (e.g., creating a PID directory) can be performed in advance, after which privileges can be consistently relinquished. The fewer capabilities at all The more components are involved, the more resilient the system is to chain failures.
Implementing Privilege Separation Correctly
I break down large-scale services into several Subprocesses, each of which has only the necessary capabilities. A frontend process establishes TLS connections and binds to ports, but has no file system permissions to make critical changes. A backend process processes data internally, has minimal read access to configuration, and communicates with databases without its own network capabilities. Administrative tasks such as log rotation or maintenance are performed using dedicated tools with time-limited capabilities. If an attacker targets one part of the system, the rest of the system remains unaffected because the Authorizations are narrowly defined. This way, security scales with the application's structure rather than with all-powerful system privileges.
A clear startup orchestration is ideal for this setup. In traditional setups, a supervisor handles this; on modern systems, I prefer to use systemd because it directly integrates capabilities, cgroups, and namespaces. This allows me to launch the network frontend, workers, and admin tools, each in its own sandbox, limit their resources, and have them automatically restart in the event of a failure—without ever granting root privileges across the board.
Combining Security Controls: POSIX, MAC, and Capabilities
Capabilities work best when I combine them with traditional File Permissions and MAC systems. SELinux or AppArmor can further restrict actions despite assigned capabilities, thereby providing multiple layers of protection. For example, a process may be allowed to bind to a port, but a policy prevents it from reading sensitive files. Anyone who wants to explore the differences between these approaches in more depth will find a clear comparison in SELinux vs. AppArmor and can then select an appropriate policy strategy. The result is a unified defense system that stops attacks on multiple levels and the Attack surface further reduced. This ensures that the assignment of rights remains verifiable, repeatable, and consistent.
It becomes particularly restrictive when I also NoNewPrivileges Enable: Processes and children are then prevented from acquiring new privileges (e.g., via SUID or newly set file capabilities). Combined with a strict capability-bounding list, this creates a security barrier that prevents subsequent privilege escalation even in the event of a misconfiguration.
Securely Assign and Audit Capabilities
I consider the assigned Skill Set as small as possible, and avoid anything that sounds like a „second root,“ such as CAP_SYS_ADMIN. Interpreters such as Python, Perl, or shell scripts are not granted powerful capabilities because their functionality can easily be misused. Through regular audits of getcap -r / 2>/dev/null I detect and fix outliers. Binary files with capabilities are read-only, owned by root, and are not located in paths that regular users are allowed to modify. In addition, I check my own binaries before every release and document changes so that Review and ensure that reproduction works reliably. This keeps rights management under control and makes adjustments traceable.
At runtime, I check processes using /proc//status (Fields CapEff, CapPrm, CapInh). This returns the hex values of the active sets and immediately shows whether an application is capable of more than intended. Tools such as capsh --print or getpcaps make debugging easier. Using the Linux Audit Subsystem, I also log changes to capabilities or to security.capability-file attributes to track tampering. Treating capabilities as configuration objects and rigorously reviewing changes makes audits reproducible and simplifies compliance verification.
Common pitfalls and how I avoid them
A common pitfall: When copying, Attributes lost, causing services to suddenly fail to start or, conversely, to be insufficiently restricted. I therefore explicitly secure capabilities during the build, or I assign them automatically during the post-install step. Another common mistake is the liberal use of general-purpose capabilities that grant more access than necessary. It’s better to use specific capabilities such as CAP_NET_RAW or CAP_CHOWN only use them where they actually serve a purpose. I also use the Ambient Set sparingly so that no unwanted Passing on widespread. By systematically reducing and regularly checking, you can prevent security vulnerabilities caused by user errors.
Also important: Systematically phase out SUID binaries. In cases where SUID was previously required (e.g., sending ICMP), it is often possible to use CAP_NET_RAW work—or, better yet, offload the function to the smallest possible helper process with very strict specifications. I also avoid placing capabilities in temporary or user-writable paths. A strict ownership and deployment policy (Root:root, 0755/0555, immutable paths) prevents the „loss“ of permissions due to replaced binaries.
Capabilities in Containers and DevSecOps
In container environments, I reduce the Capabilities aggressively and remove everything the workload doesn't absolutely need. In addition, I create a Seccomp Profile that blocks risky system calls, thereby adding another layer of security. In build pipelines, I define capabilities declaratively, test them in staging, and track them with version control. This benefits compliance because I can demonstrate adherence to the principle of least privilege and thoroughly document changes to permissions. This keeps containers tightly controlled without hindering their tasks, and the Attack surface remains small. When combined with images that contain only the bare essentials, security is further enhanced.
Important in the context of containers: Capabilities are in namespaces Relative. Within a user namespace, a process can indeed be „root,“ but its privileges apply only to the associated namespaces—which significantly limits the scope of potential damage. On the other hand, „--privileged“ is practically always off-limits: It disables the strict bounding limit and opens up far more than necessary. That’s why I start containers by default with „drop everything, add selectively“ and add NoNewPrivileges, cgroup limits, and read-only mounts. For services that only need to listen, I use socket activation or sidecars to avoid using any additional capabilities at all.
Systemd Example: Declaratively Restricting Capabilities
In service units, I define the maximum limits for a process—in a clear, repeatable, and version-controlled manner. Here’s a concise example of a web service that’s only allowed to bind to port 443 and is otherwise heavily restricted:
[Unit]
Description=Minimal web service without root privileges
[Service]
User=web
Group=web
ExecStart=/usr/bin/my-web
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/my-web
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@basic-io @network-io
LockPersonality=yes
MemoryDenyWriteExecute=yes
[Install]
WantedBy=multi-user.target
The combination of Ambient Capabilities and a tough CapabilityBoundingSet ensures that the service receives only the one necessary capability and nothing more. NoNewPrivileges prevents retroactive privilege escalation, ProtectSystem and ReadWritePaths control write access, and a strict system call filter prevents unnecessary kernel entry points.
Commonly Used Capabilities—and Secure Alternatives
- CAP_NET_BIND_SERVICE: Bind to ports <1024. Alternative: Enable sockets; place a reverse proxy in front.
- CAP_NET_RAW: Raw sockets (Ping, DHCP). Alternative: a small helper process instead of broad interpreter privileges.
- CAP_CHOWN/CAP_FOWNER: Owner/ACL adjustments. Alternative: preconfigured directories, dedicated maintenance tools.
- CAP_SYS_PTRACE: Debugging/Tracing – only in staging, never widely deployed in production.
- CAP_SYS_ADMIN: „Second root“—avoid this; specify exactly what is needed.
I always choose the smallest amount of access that exactly enables the required functionality. If a capability opens up multiple attack vectors (e.g., RAW sockets), I encapsulate the functionality in a separate, short-lived process and revoke the privileges once the task is complete.
Practical Checklist for Robust Capabilities
- Does the service start without root access? If not, why not—and can this be resolved by enabling sockets or using small helper binaries?
- Are all Are the assigned capabilities demonstrably necessary (proof of functionality, test cases)?
- Is the bounding set defined as tightly and as early as possible?
- Are XAttrs preserved consistently during builds, deployments, and backups (rsync/tar flags, package scripts)?
- Do I consistently avoid using capabilities with interpreters and SUID binaries?
- Are owner and file permissions (Root:root, 0755/0555) and paths protected against modification?
- Are additional controls (NoNewPrivileges, Seccomp, MAC profiles) in effect?
- Are process capabilities audited at runtime (
/proc//status, getpcaps) and changes documented? - Are containers configured by default with „drop all, add minimal“ and without „privileged“?
Briefly summarized
Linux Capabilities break down traditional root privileges into small, manageable units, thereby implementing the principle of least privilege in a technically sound manner. I assign services only the capabilities they truly need, and combine this with POSIX permissions and MAC policies. File capabilities ensure that permissions are directly tied to binaries, and audits clearly show who is allowed to do what. Using privilege separation, reduced container permissions, and system call filters, I limit the damage if a vulnerability is exploited. Regular reviews, strict ownership and write permissions, and a documented release process keep the assignment of privileges lean. This ensures the server service remains functional, yet the Scope is consistently kept to a minimum for attackers.


