Linux Capabilities
debt(d5/e3/b5/t7)
Closest to 'specialist tool catches it' (d5), because trivy, kube-bench, and docker-bench-security flag --privileged, missing capabilities.drop, and over-broad cap grants, but only if you run them as part of the pipeline.
Closest to 'simple parameterised fix' (e3), because the quick_fix is essentially swapping in --cap-drop=ALL --cap-add=NET_BIND_SERVICE or running setcap on a binary — a small, pattern-based change per service rather than one line, but not architectural.
Closest to 'persistent productivity tax' (b5), because capability decisions apply across cli, web, and queue workers, and every new binary/container image invites a recurring 'what caps does this need?' review that slows delivery.
Closest to 'serious trap' (t7), because per the misconception, dropping setuid and running non-root feels sufficient, yet CAP_SYS_ADMIN is nearly full root and the Ambient/Permitted/Inheritable set distinction contradicts the intuitive model of privilege inheritance across execve.
Also Known As
TL;DR
Explanation
Traditional Unix has a binary privilege model: UID 0 (root) can do everything, everyone else is checked against DAC permissions. Linux capabilities, introduced in kernel 2.2 and refined ever since, break root's superpowers into fine-grained units so a program can be granted exactly the privileges it requires and nothing more. Common capabilities include CAP_NET_BIND_SERVICE (bind to ports below 1024), CAP_NET_ADMIN (configure interfaces, iptables, routing), CAP_SYS_ADMIN (a catch-all for mount, sethostname, and many container ops - often called 'the new root'), CAP_SYS_PTRACE (attach to arbitrary processes), CAP_CHOWN (change file ownership), CAP_DAC_OVERRIDE (bypass file permission checks), CAP_SETUID / CAP_SETGID (change identity), and CAP_KILL (signal any process). Each process has five capability sets: Permitted (maximum it may hold), Effective (currently active for permission checks), Inheritable (passed across execve for capability-aware binaries), Bounding (upper limit for the process tree), and Ambient (inherited by non-privileged execs, added in 4.3). File capabilities, stored in extended attributes, let a binary gain specific capabilities on exec without being setuid root - for example, /usr/bin/ping typically has cap_net_raw+ep instead of the historical setuid bit. You inspect capabilities with getcap, setcap, and capsh, and view a process's sets in /proc/<pid>/status. Container runtimes rely heavily on capabilities: Docker drops most by default and keeps a small allowlist, while --cap-add and --cap-drop tune the sandbox. Kubernetes exposes the same knobs via securityContext.capabilities. Combined with namespaces, cgroups, seccomp, and MAC systems like AppArmor or SELinux, capabilities form a core layer of least-privilege defense - but only if you actually drop the ones you don't need. Granting CAP_SYS_ADMIN or leaving the full default set defeats most of the benefit.
Common Misconception
Why It Matters
Common Mistakes
- Granting CAP_SYS_ADMIN because a tool complains - it is nearly equivalent to full root and should be a last resort.
- Keeping Docker's default capability list instead of dropping all and adding only what the workload needs.
- Confusing file capabilities (xattrs on a binary) with process capabilities and expecting shell scripts to inherit them across execve.
- Forgetting the Ambient set, so a capability in Permitted is silently lost when the process execs a non-capability-aware binary.
- Using setuid root for a daemon that only needs CAP_NET_BIND_SERVICE, giving it every other root privilege for free.
Code Examples
# 1. Running a web server as root just to bind port 80:
sudo ./mywebserver --port 80
# Any RCE now runs with full root: mount, modprobe, read /etc/shadow, everything.
# 2. Docker with the default (still generous) capability set,
# then adding SYS_ADMIN because 'something didn't work':
docker run --cap-add=SYS_ADMIN myapp
# CAP_SYS_ADMIN alone allows mount, pivot_root, setns, and dozens of
# operations that let a container escape - this is essentially root.
# 3. Setuid-root binary for a tool that just needs raw sockets:
sudo chown root:root ./myping
sudo chmod u+s ./myping
# Now myping runs as full root instead of holding only CAP_NET_RAW.
# 1. Grant only the one capability the binary needs, via file caps:
sudo setcap 'cap_net_bind_service=+ep' ./mywebserver
./mywebserver --port 80 # runs as unprivileged user, still binds :80
getcap ./mywebserver # ./mywebserver = cap_net_bind_service+ep
# 2. Docker: drop everything, add back only what you need:
docker run --cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
myapp
# 3. Kubernetes securityContext with least privilege:
# securityContext:
# runAsNonRoot: true
# capabilities:
# drop: ["ALL"]
# add: ["NET_BIND_SERVICE"]
# allowPrivilegeEscalation: false
# Inspect a running process's capability sets:
grep Cap /proc/$(pgrep mywebserver)/status
capsh --decode=0000000000000400 # decode a hex cap mask