fine grained privileges beyond root and sudo
Linux capabilities split the traditional root privilege into more than forty individual bits, so a process gets exactly one right, for example binding a privileged port, without inheriting everything else that comes with root. That is the difference between a least privilege design and a server that instantly loses full root access whenever a single compromised service goes wrong.
Table of Contents
- 1. Why "root or nothing" is an outdated model
- 2. How Linux capabilities work: bitmasks instead of root
- 3. The most important capabilities at a glance
- 4. Assigning capabilities to a program with setcap
- 5. Inspecting the capabilities of a running process
- 6. Restricting capabilities in systemd services
- 7. Capabilities in containers: Docker and the default list
- 8. Common mistakes and debugging capability problems
- 9. Capabilities compared to SUID, sudo and full root
- 10. Summary
- 11. FAQ
1. Why "root or nothing" is an outdated model
Traditionally, Linux only knows a binary distinction: a process either runs as root with full access to every privileged operation, or it runs as a regular user and cannot do anything privileged at all. For many real world use cases, this is grossly mismatched. A web server that needs to bind port 80 requires exactly one privileged right for that, but in the classic world it immediately receives full root access to the entire system, including filesystem mounts, kernel module loading and process control over every other user.
Linux capabilities solve exactly this problem by splitting traditional root privileges into more than forty individually grantable abilities. A process using Linux capabilities can be given exactly the one ability it needs, for example binding privileged ports, without inheriting all other root rights. For production servers running PHP-FPM, Nginx or specialized network services, this is the decisive building block for a real least privilege design, where a compromised process does not automatically take over the entire system.
2. How Linux capabilities work: bitmasks instead of root
Technically, the kernel manages several capability sets for each process, represented as bitmasks. The permitted set contains all capabilities a process is generally allowed to hold. The effective set contains the capabilities the kernel actually considers during the current system call check. The inheritable set controls which capabilities can be passed on to a new program on an execve call. This separation allows a process to hold a capability while deliberately keeping it inactive until it is actually needed.
A key difference from SUID binaries: while a SUID program fully switches to the file owner's identity on every start, usually root, Linux capabilities only activate exactly the required bits, while the process identity itself stays unchanged. A program with the CAP_NET_BIND_SERVICE capability can bind privileged ports but has no access whatsoever to other root operations such as setting the system time or loading kernel modules, even if an attacker fully compromises the process.
3. The most important capabilities at a glance
Of the more than forty defined Linux capabilities, only a handful are really relevant for everyday server work. CAP_NET_BIND_SERVICE allows binding ports below 1024, classically used by web servers and mail servers. CAP_NET_ADMIN allows network configuration such as changing routing tables. CAP_SYS_ADMIN is the broadest capability and bundles so many different operations that in practice it behaves almost like root itself, which is why it should be granted with special caution.
# List all capabilities the current shell process holds
capsh --print
# Show the human-readable name of a specific capability bit
capsh --decode=0000000000003000
# List capabilities available on the system with short descriptions
man 7 capabilities | grep -A1 '^ CAP_' | head -40
# Common capabilities relevant to web and database servers:
# CAP_NET_BIND_SERVICE — bind ports below 1024 (e.g. port 80/443)
# CAP_NET_ADMIN — configure network interfaces and routing
# CAP_SYS_ADMIN — broad admin operations, avoid unless required
# CAP_DAC_OVERRIDE — bypass file permission checks
# CAP_CHOWN — change file ownership regardless of permissions
# CAP_SETUID/CAP_SETGID — change process UID/GID
It matters not to grant CAP_SYS_ADMIN as a default fix for "something doesn't work", as unfortunately shown in many Docker tutorials online. This capability covers so many different privileged operations that it is practically almost as risky as full root, undermining the entire point of Linux capabilities as a fine grained privilege model.
4. Assigning capabilities to a program with setcap
The setcap command permanently assigns one or more Linux capabilities to an executable file, stored as an extended filesystem attribute of the file. The classic example: a PHP-FPM or Node.js process that needs to bind port 80 directly, without being started as root.
# Grant a binary the ability to bind privileged ports, nothing else
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/node
# Verify which capabilities are attached to a binary
getcap /usr/bin/node
# /usr/bin/node cap_net_bind_service=ep
# Remove a previously assigned capability
sudo setcap -r /usr/bin/node
# Assign multiple capabilities at once, comma-separated
sudo setcap 'cap_net_bind_service,cap_net_raw=+ep' /usr/sbin/my-network-tool
# Run the binary as a non-root user afterwards — port 80 binding still works
sudo -u www-data /usr/bin/node server.js
After the setcap call, the process can run as a normal, unprivileged user and still bind the privileged port. This combination of a non-root process identity and a precisely scoped Linux capability massively reduces the attack surface compared to a classic setup, where the whole process starts as root and privileges have to be laboriously dropped afterwards.
5. Inspecting the capabilities of a running process
For audits and debugging, it matters to see exactly which Linux capabilities an already running process actually holds, independent of what was configured on the binary file via setcap. The kernel exposes this information directly under /proc/PID/status, in four separate hex bitmasks for the different capability sets.
# Inspect capability sets of a running process by PID
grep -i cap /proc/$(pgrep -f nginx | head -1)/status
# Decode the raw hex bitmask into readable capability names
capsh --decode=0000000000002400
# List capabilities for all processes owned by www-data
for pid in $(pgrep -u www-data); do
echo "PID $pid: $(grep CapEff /proc/$pid/status)"
done
# Compare intended (setcap) vs. actual (runtime) capabilities
getcap /usr/sbin/nginx
grep CapEff /proc/$(pgrep -f 'nginx: master' | head -1)/status
A discrepancy between the capability configured via getcap and the one actually active via /proc/PID/status usually indicates that the process voluntarily dropped the capability at runtime through prctl, a common pattern in security conscious server software that reduces its own privileges after initialization.
6. Restricting capabilities in systemd services
For custom written systemd services, declaring capabilities directly in the unit file is the cleanest way to grant Linux capabilities without manually applying setcap to the binary. systemd offers AmbientCapabilities and CapabilityBoundingSet as two complementary directives for exactly this purpose.
# /etc/systemd/system/my-app.service
[Unit]
Description=Custom application binding a privileged port
After=network.target
[Service]
Type=simple
User=appuser
Group=appuser
ExecStart=/opt/my-app/bin/server
# Grant exactly one capability at process start, without setcap on the binary
AmbientCapabilities=CAP_NET_BIND_SERVICE
# Restrict which capabilities this service could ever gain, even via setuid
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# Additional hardening commonly paired with capability restrictions
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
[Install]
WantedBy=multi-user.target
The decisive advantage of this method: CapabilityBoundingSet acts as a hard upper limit for the entire process tree of the service, even if a subordinate component tries to gain additional privileges through other means. Combined with NoNewPrivileges, this prevents the service from ever acquiring more Linux capabilities than explicitly declared in the unit file, regardless of what the executed program itself attempts.
7. Capabilities in containers: Docker and the default list
Container runtimes such as Docker start containers by default with a restricted selection of Linux capabilities, not with full root, even if the process inside the container runs as UID 0. This default list covers roughly 14 capabilities, significantly fewer than the more than forty available, and can be both restricted further and specifically extended.
For most web application containers, even Docker's default list is still too generous. A PHP-FPM or Node.js container that needs no network configuration and no filesystem permission changes should start with --cap-drop=ALL and then only add back the actually required Linux capabilities specifically via --cap-add. This pattern significantly reduces the attack surface of a compromised container, because even a break out of the process context does not automatically bring far reaching system privileges along.
8. Common mistakes and debugging capability problems
The most common mistake when working with Linux capabilities is to misinterpret a failed privileged operation as a general permission problem and then reflexively grant CAP_SYS_ADMIN or even full root, instead of identifying the specific missing capability. The kernel log via dmesg or audit logs, with auditing enabled, show exactly which capability was missing for which system call.
# Enable auditd rules to log capability check failures
sudo auditctl -a always,exit -F arch=b64 -S capset -k cap_debug
# Search recent audit log for capability-related denials
sudo ausearch -k cap_debug --start recent
# Trace which capability a process needs using strace on the failing syscall
strace -f -e trace=%network ./my-app 2>&1 | grep -i "eperm\|eacces"
# Common mistake: file was copied/rebuilt after setcap, losing the attribute
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/node
cp /usr/bin/node /usr/bin/node.bak # WRONG: cp does not preserve xattrs by default
cp --preserve=all /usr/bin/node /usr/bin/node.bak # RIGHT: preserves capability xattr
Another frequently overlooked pitfall: capabilities are stored as an extended filesystem attribute and get lost on a plain cp without --preserve=all, on a deployment via rsync without -X, or when a Docker image is rebuilt if the setcap assignment is not part of the build process. Anyone using Linux capabilities in a CI/CD pipeline should therefore include the setcap step explicitly in the deployment script, instead of relying on a one time manually set capability.
9. Capabilities compared to SUID, sudo and full root
There are several established mechanisms for giving a process extended privileges, differing significantly in granularity and risk. Linux capabilities are usually the most precise and safest option for server services.
| Mechanism | Granularity | Risk if compromised | Typical use |
|---|---|---|---|
| Linux capabilities | Very fine (bit level) | Low, precisely bounded | Server services, containers, systemd |
| SUID bit | Coarse (full owner context) | High, often full root context | Legacy tools like passwd, ping |
| sudo | Medium (command level) | Depends on sudoers rules | Interactive administration |
| Full root | None | Maximum | Only when truly unavoidable |
| SELinux/AppArmor (complementary) | Very fine (rule based) | Low, but more complex to maintain | Additional hardening alongside capabilities |
In practice, these mechanisms do not exclude each other. A well hardened server combines Linux capabilities for precise process privileges, sudo for controlled interactive administration, and optionally SELinux or AppArmor as an additional layer that further restricts, even within a granted capability, which files or network destinations are reachable.
Mironsoft
Server hardening, least privilege design and container security
Is everything still running as root at your place?
We analyze your server services and containers, replace unnecessary root and SUID privileges with precisely granted Linux capabilities, and harden systemd services with CapabilityBoundingSet.
Capability audit
Analyzing and documenting existing root and SUID usage
Least privilege redesign
Applying setcap, systemd directives and container cap drop deliberately
CI/CD integration
Anchoring capability assignment reproducibly in deployment pipelines
10. Summary
Linux capabilities replace the crude root or nothing model with more than forty individually grantable rights, so a process can perform exactly the privileged operation it needs without inheriting the rest of root. setcap permanently assigns these rights to a binary, capsh and /proc/PID/status make visible which capabilities a process actually holds, and systemd directives such as AmbientCapabilities and CapabilityBoundingSet integrate this model directly into your own services.
The biggest lever is consistently using Linux capabilities instead of SUID bits or blanket root starts, both for classic server services and in containers with --cap-drop=ALL as a starting point. Anyone who maintains this discipline significantly reduces the attack surface of every single service without having to give up the functionality that is actually needed.
Linux Capabilities — The Essentials at a Glance
Principle
Root privileges are split into more than forty individually grantable bits instead of all or nothing.
setcap
setcap 'cap_net_bind_service=+ep' binary grants exactly one ability, without a root start.
systemd
CapabilityBoundingSet hard-caps which capabilities a service could ever acquire.
Containers
--cap-drop=ALL plus targeted --cap-add minimizes the container's attack surface.