BlogLinux & scripting

Linux capabilities: dropping root the right way

Split root into fine-grained capabilities, drop everything you don't need, and stop running containers as full root.

Dec 30, 2025·4 min readIntermediate·By the SecOpsLog team · command-tested

Traditional Unix root is binary: uid 0 can do almost everything. Linux capabilities split root into fine-grained privileges — CAP_NET_BIND_SERVICE to bind port 80, CAP_SYS_ADMIN for mount operations, CAP_NET_RAW for raw sockets. A process can hold a subset of caps without full root, and containers should drop every cap they do not need. Running as root 'because the app listens on port 80' stopped being acceptable years ago.

This note shows effective vs permitted vs bounding sets, how to inspect running processes and binaries, and how to configure Docker and systemd to drop caps by default. Start with Linux essentials if user namespaces and ids are rusty; Linux hardening extends this into full host lockdown.

Capability reduction workflow

Inventory what the process actually needs, drop bounding set to minimum, test failure modes, document required caps in the runbook.

1Identify processbinary + startup script2getpcaps / grepCapeffective set today3Map needsbind low port? raw socket?4Drop in Dockercap_drop: ALL + cap_add5Or setcap onbinarycap_net_bind_service+ep6Run as non-rootuidUSER 65532 in Dockerfile7Verify withgetpcapsafter deploy

Inspect effective capabilities

The /proc/PID/status file lists CapEff, CapPrm, CapBnd in hex masks. getpcaps decodes them into human-readable names. Compare a container running --privileged to one with cap_drop: [ALL] — the difference is your attack surface.

File capabilities via setcap cap_net_bind_service+ep /usr/bin/myapp let a non-root user bind port 443 without uid 0. The +ep means effective and permitted sets. Audit file caps during image builds — getcap -r / in CI catches accidental caps left by install scripts.

inspect.sh
# Running process
grep Cap /proc/$(pgrep nginx | head -1)/status
# File capabilities on a binary
getcap /usr/local/bin/myapp
# /usr/local/bin/myapp = cap_net_bind_service+ep

Drop caps in Docker

Default Docker adds a handful of caps beyond what most apps need. Start from cap_drop: [ALL] and add back only NET_BIND_SERVICE if the app must bind ports below 1024 without running as root. Better: listen on 8080 and let the ingress handle 443.

Kubernetes securityContext.capabilities mirrors Docker compose. Pod Security Standards restricted profile drops all caps by default. When an app fails with Operation not permitted, check dmesg for capability denials before reaching for --privileged — the fix is usually one specific cap or a config path permission.

docker-compose.yml
services:
web:
image: myapp:1.2
user: "65532:65532"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
bash — before and after cap droplive
docker run --rm alpine sh -c "grep CapEff /proc/self/status"
CapEff: 0000000000000000 (with cap_drop ALL)
docker run --rm --cap-add NET_BIND_SERVICE alpine \
sh -c "grep CapEff /proc/self/status"
Only NET_BIND_SERVICE bit set if configured
CAP_SYS_ADMIN is nearly root
Some caps grant wide powers — CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, CAP_SYS_PTRACE. `--privileged` in Docker enables effectively all caps plus device access. Treat privileged containers like root shells on the host. Kubernetes `privileged: true` is the same mistake at cluster scale.
Granting caps vs running root
Prefer
Non-root uid + high port
cap_net_bind_service on binary
cap_drop ALL in orchestrator
systemd CapabilityBoundingSet=
Avoid
uid 0 because legacy script
docker --privileged in prod
CAP_SYS_ADMIN for convenience
Unknown cap_add in compose

Where this goes next

Capabilities interact with seccomp, AppArmor, and user namespaces — layer them. For containers, pair cap drops with Docker hardening (rootless mode, read-only rootfs). On bare metal, align with systemd unit directives from Linux hardening and detect cap abuse via Linux detection engineering.

Legacy init scripts that call mount or ip link need caps you would otherwise drop — migrate to systemd units with explicit ExecStartPre and document required caps in the service README. Review caps quarterly when base images update.

Go deeper in a courseLinux essentialsProcesses, users, capabilities, and the filesystem every SecOps engineer needs.View course

Related posts