Container Security: Why Your Non-Root Container Drops Its Capabilities (SCC, CRI-O, Kernel)
Briefing Summary
A monitoring sidecar runs as non-root and needs ICMP health checks: it requires CAP_NET_RAW. The SecurityContextConstraint (SCC) allows the capability, the PodSpec requests it (capabilities.add: [NET_RAW]), CRI-O starts the container without error — and yet ping fails:
$ ping -c 1 10.0.0.1
ping: Operation not permitted
$ cat /proc/1/status | grep Cap
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000002000
This is not a bug. The scenario, documented by the Red Hat Developer Blog on 5 October 2026, is the collision of three systems that do not talk to each other: SCC admission on the API server, CRI-O translating the CRI request into an OCI spec, and Linux capability semantics in the kernel. Knowing where the SCC ends and the kernel begins is the whole art of debugging container privileges.
1. An SCC Is an Admission Gate, Not a Runtime Engine
An SCC only decides whether a security configuration is allowed into the cluster (permitted capabilities, UID ranges, volume types, privilege escalation, SELinux context). It is evaluated by the OpenShift API server admission controller when the Pod is created. Once the Pod is admitted, the SCC’s job is done: it has no runtime component, does not talk to CRI-O, writes no iptables rules and configures no process capability.
- At cluster scope: SCCs are far more granular than Pod Security Admission (PSA) — PSA works on a coarse 3-tier model (privileged, baseline, restricted) whereas SCCs control field by field.
2. CRI-O: From SCC to OCI Spec, Where It Is Decided
CRI-O has no concept of SCCs: it receives a CreateContainerRequest and translates it into a runtime spec. Simple fields (hostNetwork, hostPID, hostIPC) map directly to namespace modes. Capabilities go through SpecSetPrivileges() and diverge by mode:
- Privileged containers:
SetupPrivileged(true)populates all five sets with every known capability (41 at the time of writing). Note: the PodSpec’sdropCapabilitiesfield is then ignored — the add/drop logic is entirely bypassed. - Non-privileged containers: CRI-O starts from a list and merges in 9 default capabilities (
CHOWN,DAC_OVERRIDE,FSETID,FOWNER,SETGID,SETUID,SETPCAP,NET_BIND_SERVICE,KILL), unless the PodSpec declaresdrop: ALL. This is exactly why thedrop: ALL+add: [one capability]pattern is the production best practice: you restart from a clean, minimal set.
CRI-O then writes the final capability into Bounding, Effective and Permitted — identically whether the container runs as root or not. The runtime outcome, however, will be radically different.
3. Kernel: Five Capability Sets and a UID Transition That Wipes Them
Linux capabilities decompose the superuser model into distinct units spread across five sets, each evaluated differently by the kernel:
- Bounding (CapBnd) — the absolute ceiling of what a process may ever acquire.
- Permitted (CapPrm) — the pool from which effective capabilities can be drawn.
- Effective (CapEff) — what the kernel actually checks at runtime; without
CAP_NET_RAWhere, raw socket creation is denied regardless of what the other sets contain. - Inheritable (CapInh) — capabilities carried across
execve(), only when the binary also carries matching file capabilities. - Ambient (CapAmb) — introduced in Linux 4.3 precisely to solve the non-root problem: they automatically repopulate Permitted and Effective after
execve().
The fatal mechanism: the OCI runtime starts the process as root, applies the sets, and then switches to the target UID (e.g. 1000). During that UID 0 → non-zero transition, the kernel clears CapPrm and CapEff. Only Bounding survives. And CRI-O never sets CapInh or CapAmb.
4. Why CRI-O Deliberately Skips Ambient Capabilities
This is not an oversight: it is a documented security decision in CRI-O’s code. Kubernetes assumes that runAsNonRoot results in capabilities being dropped; enabling ambient capabilities would break that implicit contract. A KEP (2763 – ambient capabilities) does exist in SIG Security to make them selectively supportable — worth tracking, because it would change the game for every non-root sidecar needing fine-grained privilege.
5. The Real Fixes, and What They Cost
- File capabilities (
setcap) — the most targeted approach:setcap 'cap_net_raw=+ep' /usr/bin/pingat image build time. The kernel promotes the capability into Permitted and Effective at exec, even for non-root, as long as it remains in the Bounding set. Cost: you must modify the image; file capabilities are stored as extended attributes, so the build pipeline and storage driver must preserve them — and trust shifts to image provenance. - Running as root — avoids the UID transition. Cost: the most common production SCC (
restricted-v2) forbids it. - Privileged container — populates all five sets, but disables SELinux, AppArmor and seccomp and bypasses the entire add/drop logic: “a sledgehammer for a thumbtack”. Never outside debugging.
The engineering lesson: the SCC, CRI-O and the kernel are three successive layers, each with its own mental model. Faced with CapEff: 0000000000000000 on a non-root process, you are not hunting a bug — you are re-reading the UID transition and capability semantics.
6. Technical Sources & References
- Red Hat Developer Blog — Why your non-root container dropped its capabilities (and how to fix it): developers.redhat.com
- Linux man-pages — capabilities(7): man7.org
- Kubernetes Enhancement Proposal 2763 — Ambient Capabilities: github.com/kubernetes/enhancements
- Red Hat Blog — Managing SCCs in OpenShift: redhat.com
Apply these technologies in production
Explore our intensive 3-month AS200 (DevOps) and AS300 (AI Platform Engineer) curriculums.