Processes & Privileges
Linux Capabilities
Splitting root's power into discrete capabilities, and dropping what you do not need.
“Root can do anything” is a security disaster waiting for a bug. If a process needs just one root-only power, say binding to port 443, the traditional model forces you to run the whole thing as UID 0, so any code execution flaw in that process becomes total system compromise. Linux capabilities fix this by carving the monolithic power of root into around forty independent privileges that can be granted and revoked individually. A process can then hold exactly the powers its job requires and drop everything else, which is the practical definition of least privilege on Linux.
The dangerous capabilities
Not all capabilities are equal. A handful are effectively equivalent to full root because they can be leveraged into arbitrary code or file access. Knowing which these are tells you where to look when auditing a service or a container.
| Capability | What it grants | Why it is dangerous |
|---|---|---|
CAP_SYS_ADMIN | A grab bag: mount, pivot_root, many ioctls, and more | So broad it is nearly root; the default “I need everything” capability |
CAP_SYS_PTRACE | Attach to and inspect other processes | Read memory and inject code into other processes, including privileged ones |
CAP_SYS_MODULE | Load and unload kernel modules | Load arbitrary code into the kernel: game over |
CAP_DAC_OVERRIDE | Bypass all file read/write/execute permission checks | Read or modify any file on the system |
CAP_DAC_READ_SEARCH | Bypass read and directory-search checks | Read every file (e.g. /etc/shadow) even if it cannot write |
CAP_SETUID / CAP_SETGID | Change process UIDs/GIDs arbitrarily | Become any user, defeating privilege separation |
CAP_NET_ADMIN | Configure networking, interfaces, firewall | Reconfigure or sniff the network, redirect traffic |
CAP_NET_BIND_SERVICE | Bind to ports below 1024 | Comparatively benign; the poster child for a narrow grant |
The takeaway is that CAP_SYS_ADMIN deserves the same suspicion as running as root outright. In the Kubernetes Security foundation the recommended baseline is to drop: ["ALL"] and add back only the narrow ones like NET_BIND_SERVICE, which is exactly this table applied to pods.
The five capability sets
Each process has several capability sets, and file capabilities interact with them at exec. This is the machinery that makes capabilities inherit predictably.
| Set | Role |
|---|---|
| Permitted | The superset the process is allowed to use |
| Effective | The subset currently active for permission checks |
| Inheritable | Preserved across exec and combined with a file’s inheritable set |
| Bounding | A ceiling: capabilities not in the bounding set can never be regained |
| Ambient | Inheritable capabilities that survive exec of a non-file-capability binary |
The bounding set is the one you use to permanently forbid a capability. Once dropped from the bounding set, a process and all its descendants can never reacquire that capability even via a setuid-root binary. systemd exposes this directly as CapabilityBoundingSet=.
File capabilities: the setuid alternative
Historically, ping was setuid-root just so it could open a raw socket. That is a huge amount of privilege for one narrow need. File capabilities let you attach specific capabilities to a binary instead, so it starts with only what it needs and never sees UID 0.
# Inspect ping's file capability instead of a setuid bit
getcap "$(command -v ping)"
Expected output:
/usr/bin/ping cap_net_raw=ep
The =ep means the capability is in the file’s effective and permitted sets, so ping gains only CAP_NET_RAW and runs as your own UID. Compare a modern minimal service you build yourself.
# Grant a self-built web server only the bind-low-port power
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/mywebd
getcap /usr/local/bin/mywebd
Expected output:
/usr/local/bin/mywebd cap_net_bind_service=ep
That binary can now listen on port 80 while running as an unprivileged user, with no other root power in reach. Note that file capabilities are ignored when NoNewPrivs is set, which ties back to the Process Model page.
Hands-on: audit and drop capabilities
capsh decodes capability masks and can launch a shell with a reduced set so you can test what breaks.
# Decode the effective mask from an earlier /proc reading
capsh --decode=0000000000003400
Expected output:
0x0000000000003400=cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw
Now drop a capability from the bounding set and confirm it cannot be used, simulating a hardened worker.
# Start a shell that can never load kernel modules or admin the network
sudo capsh --drop=cap_sys_module,cap_net_admin --print | grep -E 'Bounding|Current'
Expected output:
Current: cap_chown,cap_dac_override,...,cap_setfcap (no sys_module, no net_admin)
Bounding set =cap_chown,cap_dac_override,... (sys_module and net_admin absent)
For a real daemon you would not hand-roll this; you would let systemd apply it. A minimal drop in a unit file looks like:
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
That gives the service the one power it needs, forbids all others permanently, and blocks setuid escalation in one stanza.
Practical Guidance
- Start from empty and add back. For services and containers, drop all capabilities and grant only the specific ones a feature requires; never start from full and try to subtract.
- Treat
CAP_SYS_ADMIN,CAP_SYS_MODULE, andCAP_SYS_PTRACEas equivalent to root. If a workload asks for them, dig into why before granting. - Replace setuid-root helpers with file capabilities where you can.
cap_net_raw=eponpingis far safer than a full setuid bit. - Use
CapabilityBoundingSet=(not just dropping at runtime) so a capability can never be regained even through a setuid binary. - Audit what is actually held with
getpcaps <pid>or theCapEffline in/proc/<pid>/status, and decode masks withcapsh --decode. Documentation lies; the running mask does not.