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.
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.
Inventory what the process actually needs, drop bounding set to minimum, test failure modes, document required caps in the runbook.
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.
# Running processgrep Cap /proc/$(pgrep nginx | head -1)/status# File capabilities on a binarygetcap /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.
services:web:image: myapp:1.2user: "65532:65532"cap_drop:- ALLcap_add:- NET_BIND_SERVICEsecurity_opt:- no-new-privileges:true
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 configuredWhere 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.