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.

CapabilityWhat it grantsWhy it is dangerous
CAP_SYS_ADMINA grab bag: mount, pivot_root, many ioctls, and moreSo broad it is nearly root; the default “I need everything” capability
CAP_SYS_PTRACEAttach to and inspect other processesRead memory and inject code into other processes, including privileged ones
CAP_SYS_MODULELoad and unload kernel modulesLoad arbitrary code into the kernel: game over
CAP_DAC_OVERRIDEBypass all file read/write/execute permission checksRead or modify any file on the system
CAP_DAC_READ_SEARCHBypass read and directory-search checksRead every file (e.g. /etc/shadow) even if it cannot write
CAP_SETUID / CAP_SETGIDChange process UIDs/GIDs arbitrarilyBecome any user, defeating privilege separation
CAP_NET_ADMINConfigure networking, interfaces, firewallReconfigure or sniff the network, redirect traffic
CAP_NET_BIND_SERVICEBind to ports below 1024Comparatively 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.

SetRole
PermittedThe superset the process is allowed to use
EffectiveThe subset currently active for permission checks
InheritablePreserved across exec and combined with a file’s inheritable set
BoundingA ceiling: capabilities not in the bounding set can never be regained
AmbientInheritable 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

  1. 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.
  2. Treat CAP_SYS_ADMIN, CAP_SYS_MODULE, and CAP_SYS_PTRACE as equivalent to root. If a workload asks for them, dig into why before granting.
  3. Replace setuid-root helpers with file capabilities where you can. cap_net_raw=ep on ping is far safer than a full setuid bit.
  4. Use CapabilityBoundingSet= (not just dropping at runtime) so a capability can never be regained even through a setuid binary.
  5. Audit what is actually held with getpcaps <pid> or the CapEff line in /proc/<pid>/status, and decode masks with capsh --decode. Documentation lies; the running mask does not.