Namespaces and cgroups
TL;DR: Linux containers are just namespaces (isolation of kernel resource views) + cgroups (resource accounting) + capability/seccomp/LSM filters. Every namespace has a corresponding escape primitive when its assumptions break.
What it is
A namespace gives a process a private view of a kernel resource (PIDs, mounts, network, IPC, UTS, user, cgroup, time). A cgroup enforces accounting and limits (CPU, memory, IO, pids). Together they form the container abstraction — but the kernel is still shared, so any namespace whose isolation has a hole becomes an escape vector.
Preconditions / where it applies
- Code execution inside a namespaced environment (Docker, Podman, LXC, Kubernetes pod, unshare-based sandbox)
- You need to know which namespaces you’re in and which you share with the host to reason about escapes
- Useful introspection:
ls -la /proc/self/ns,readlink /proc/1/ns/*
Technique
The seven (eight) namespaces and their escape primitives:
| NS | Isolates | Escape primitive |
|---|---|---|
mnt | Mount table, root filesystem view | Host bind mounts visible inside; chroot from mounted host disk |
pid | PID numbering | Sharing host PID NS (--pid=host) lets you signal/ptrace host processes |
net | Interfaces, sockets, iptables | --net=host exposes host services; raw socket + CAP_NET_ADMIN |
ipc | SysV IPC, POSIX message queues | Shared IPC = shared shared-memory with host workloads |
uts | hostname, domainname | Mostly cosmetic; not a privesc by itself |
user | UID/GID mapping, caps | Unprivileged userns gives full caps inside — many CVEs (see user-namespace-attacks) |
cgroup | cgroup root | Misset rw cgroup mount + release_agent (v1) = classic escape |
time (5.6+) | CLOCK_MONOTONIC offset | Not a security boundary |
Compare the host vs container view:
1
2
3
4
5
6
7
8
# from inside
ls -la /proc/self/ns
# lrwxrwxrwx 1 root root 0 mnt -> 'mnt:[4026532567]'
# lrwxrwxrwx 1 root root 0 pid -> 'pid:[4026532569]'
# ...
# inode shared with host? then you're NOT isolated for that NS
nsenter -t 1 -m -p ls /host_only_marker
Cgroup essentials:
- v1 has one hierarchy per controller, the
release_agentfile = path the kernel runs when the last task leaves a notify-on-release cgroup → classic escape primitive - v2 unified hierarchy, no
release_agent, escape surface much smaller - Check which you’re on:
stat -fc %T /sys/fs/cgroup/→cgroup2fsmeans v2
Creating namespaces by hand:
1
2
unshare -Urmp --fork --mount-proc bash # user+mount+pid in one shot, no caps needed
nsenter -t <pid> -a # join all NS of another process
Why this matters for offence: every container escape reduces to “find a namespace that isn’t actually isolating”, or “find a kernel bug reachable through a syscall the container’s seccomp profile lets through”. Knowing which NSes a runtime shares (Docker shares the user NS by default — root in container is root on host kernel) is the first triage step inside any container.
Detection and defence
- Default to userns-remap (
/etc/docker/daemon.json"userns-remap": "default") so in-container UID 0 maps to a non-root host UID - Drop CAP_SYS_ADMIN; default seccomp + AppArmor/SELinux profile
- Use cgroup v2; cgroup v1
release_agentescape is well-known - Kubernetes PodSecurity
restrictedprofile: nohostPID,hostNetwork,hostIPC, noprivileged - Audit
unshare(),setns(),clone(CLONE_NEW*)from non-runtime parents
References
- Linux Namespaces overview — authoritative man page
- Aqua — Container escape research — runtime-CVE writeups
- HackTricks — Namespaces — per-NS escape recipes
Related: container-escape-techniques, user-namespace-attacks, linux-capabilities.