TL;DR — Containers are ordinary Linux processes whose view of the world is filtered by namespaces and whose resource budgets are enforced by cgroups. The seven standard namespaces (mount, PID, network, UTS, IPC, user, and cgroup) define what a process can see; namespaces are the kernel primitive behind every isolation guarantee Docker, containerd, and Kubernetes promise.

Why namespaces, not “magic”

When a teammate first sees a Docker demo, the natural reaction is: “That’s basically a tiny VM.” It isn’t. There is no separate kernel booting up, no hypervisor emulating devices, no hardware-assisted isolation. What you have is /usr/bin/python3 — the exact same binary, the exact same kernel — running with a filtered view of the filesystem, a private process ID space, an isolated network stack, and a synthetic hostname.

The filtering is done by namespaces, a kernel feature that has been steadily expanded since Linux 2.6.19 (the first mount namespace in 2002). The resource budgeting is done by cgroups. Together, they are the entire technical substrate of containerization. If you’ve ever debugged a “container that escaped” CVE, a fork bomb that took down a node, or a hostname collision between two pods — you’ve been debugging a namespace boundary.

This post walks through each namespace primitive, shows what it isolates and what it deliberately doesn’t, then ties it back to how runtimes like containerd and orchestrators like Kubernetes actually wire them together in production.

The seven namespaces and what they actually hide

The kernel exposes namespaces through three system calls: clone(2) (with CLONE_NEW* flags), unshare(2), and setns(2). As of Linux 5.6, the clone3(2) interface is preferred, but the semantics are identical. Each namespace type corresponds to one of seven CLONE_NEW* flags, and each one wraps a different category of kernel state.

Mount namespace (CLONE_NEWNS)

The oldest namespace and the one most likely to surprise you. A mount namespace gives a process its own view of the filesystem mount tree. Inside it, you can mount, umount, and pivot_root without affecting — or being affected by — the host.

This is what makes chroot look quaint. A chrooted process still sees the same mount table; it just has its root dentry changed. A mount-namespaced process can mount an overlay filesystem on top of /, mount a tmpfs over /tmp, or pivot into a freshly unpacked rootfs. The Docker image layers you see in docker inspect are literally bind mounts and overlay mounts wired into a per-container mount namespace.

# Demonstrate the isolation: two shells, two mount tables.
unshare -m bash
mount -t tmpfs none /tmp      # Visible only in this namespace
ls /tmp                      # Empty here
exit                        # Tear down the namespace
mount | grep tmpfs           # Gone — host never saw it

PID namespace (CLONE_NEWPID)

A PID namespace assigns process IDs starting from 1. Inside the namespace, ps, top, and /proc show only the processes that belong to it. To processes outside the namespace, your “PID 1” is just another number — for example, pid 4821.

This has a delightful consequence: every container has its own init. That’s why docker stop sends SIGTERM to PID 1 inside the container and expects the userland init to reap its children. If PID 1 in the container exits before reaping zombies, the container will fill with defunct processes, which is why production images run tini or a real init system.

It also has a sharp edge: a process in a parent PID namespace can see and signal processes in child PID namespaces (with permission), but a child cannot see its parent. That’s the foundation of the “PID 1 reaper” responsibility, formalized in the Docker and Kubernetes PID 1 contract.

Network namespace (CLONE_NEWNET)

This is the workhorse of every container networking stack. A network namespace gives a process its own:

  • Network interfaces (loopback, eth0, veth pairs)
  • Routing table
  • Netfilter / iptables rules
  • Socket tables
  • /proc/net/* files

When you docker run -p 8080:80 nginx, three namespaces are collaborating: the container has a veth interface in its private netns, that veth is bridged into a bridge (or routed via a veth to a host-side cni0 interface) on the host, and the host’s iptables rules in the host netns DNAT the traffic. The packet path crosses the namespace boundary at the veth pair, which is just a virtual patch cable between two network devices. Cloud-native CNIs like Cilium replace this with eBPF but still operate on the same primitives.

UTS namespace (CLONE_NEWUTS)

A short, almost trivial namespace. It isolates two values: the hostname and the domainname, which are stored in kernel struct utsname. This is what lets every Kubernetes pod set metadata.name as its hostname without colliding with siblings. Without it, hostname(1) would return the same string for every container on a node.

IPC namespace (CLONE_NEWIPC)

Isolates System V IPC objects (message queues, semaphores, shared memory segments) and POSIX message queues. Most modern workloads don’t touch them, but the namespace exists for the case where a long-lived legacy app or a database uses shared memory. Without CLONE_NEWIPC, a malicious or buggy container could attach to another container’s shared memory segment by ID.

User namespace (CLONE_NEWUSER)

The most consequential and the most contested. A user namespace maps a range of UIDs/GIDs inside the namespace to a (usually unprivileged) UID/GID outside. The classic pattern: UID 0 inside the container → UID 100000 outside. This is what enables rootless containers, where the entire container runtime — including mount setup — runs as an unprivileged user.

# Rootless in action: mapping UIDs 0-65535 in the namespace to 100000-165535 outside
cat /proc/self/uid_map
         0 100000 65536

User namespaces are also the security-critical primitive behind userns-remap in Docker and rootless Podman. The reason most distributions didn’t enable them by default for years is subtle: a process with CAP_SYS_ADMIN in a user namespace can do things (like mount filesystems) that, outside the namespace, require actual root. As long as the namespace is properly nested, this is fine. When it isn’t, you have a container escape.

Cgroup namespace (CLONE_NEWCGROUP)

The newest of the seven (Linux 4.6) and the one most engineers have never heard of. It virtualizes the cgroup filesystem view itself, so a process sees its own cgroup as /, regardless of where it actually sits in the host’s cgroup hierarchy.

Why does this matter? Because cgroups are how Kubernetes sets resources.limits. The container’s view of /sys/fs/cgroup/ is rooted at its own cgroup directory; the kubelet’s cgroup-adjuster writes to the host’s view at the right relative path. The namespace hides the complexity.

Patterns in production: how runtimes wire namespaces together

A Docker/containerd container is not seven independent primitives — it’s a tightly choreographed setup that the runtime does once, during create, in roughly this order. The exact sequence is documented in the OCI Runtime Spec, and it’s worth reading once.

  1. Create a parent process (the “container init”) via clone() with a stack of CLONE_NEW* flags — NEWNS | NEWPID | NEWNET | NEWUTS | NEWIPC | NEWUSER | (optionally) NEWCGROUP.
  2. Set up cgroups: the runtime writes to /sys/fs/cgroup/.../<container_id>/ to apply memory and CPU limits, and freezes the cgroup’s processes during setup.
  3. Configure the rootfs: pivot_root into the image’s overlay-mounted directory inside the new mount namespace.
  4. Wire up networking: create the veth pair, move one end into the container’s netns, attach the other to the bridge or CNI device on the host, set up iptables/eBPF.
  5. Set hostname via sethostname(2) in the UTS namespace.
  6. Apply seccomp, AppArmor/SELinux profiles, capabilities, and other LSM hooks.
  7. exec(2) the user’s entrypoint, replacing the init helper with the actual workload.

The most important takeaway: all seven namespaces are created in one clone() call, atomically, by the container runtime. They don’t get bolted on later. This is why a partial setup is almost never a real-world condition — either the runtime has done its job, or it hasn’t and the container didn’t start.

Where Kubernetes extends the model

Kubernetes adds three layers of indirection on top:

  • Pod sandbox: All containers in a pod share the same network and IPC namespaces (and UTS, traditionally), but have separate mount and PID namespaces. This is the “sidecar pattern” — the sidecar can localhost:9090 to the main container because they share the netns.
  • cgroup hierarchy: The kubelet creates a cgroup per pod under the QOS class (Burstable, Guaranteed, BestEffort) and per-container sub-cgroups for resource limits.
  • securityContext: YAML fields like runAsUser, runAsNonRoot, privileged, capabilities, seccompProfile, and appArmorProfile are all compiled down to namespace, capability, and LSM syscalls.
# A securityContext that exercises most of the namespace primitives
apiVersion: v1
kind: Pod
metadata:
  name: hardened
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10001
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: myregistry/app:1.2.3
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
      runAsUser: 10001

This YAML, after admission and translation, becomes: a user-namespace mapping, a read-only mount remount, a dropped capability set, and a seccomp filter attached to the container’s PID 1.

Where the seams show: failure modes you should know

Namespaces are powerful, but they’re not magical. The following failure modes are recurring themes in incident reports and CVE databases.

The mount-namespace escape via /proc and /sys

A privileged container, or one with the SYS_ADMIN capability, can often mount filesystems from inside its namespace that reach into the host. The classic example is cgroupfs mounted with release_agent, which Felix Wilhelm’s “Shocker” exploit demonstrated in 2016. Modern kernels have largely closed this; if you run an unprivileged container with default capabilities, this attack doesn’t apply.

The PID namespace + signal leak

A process in a parent PID namespace can send signals to processes in child PID namespaces if it knows the PID. This is by design (the kubelet needs to be able to signal pods), but it also means a compromised host process can kill -9 anything in a container. The mitigation is the standard one: namespace isolation is a security boundary, not a trust boundary. Defense in depth.

Network namespace + host network mode

When you set pod.spec.hostNetwork: true, the pod gets the host’s netns, not a private one. It can see all host interfaces, all ports, all iptables rules. This is occasionally necessary (CNI debugging, certain CNIs), and occasionally a security incident. Audit it.

User namespace + privileged: true

privileged: true is shorthand for “give the container almost all capabilities and disable most namespace-seccomp-isolation.” It is mutually incompatible with the safety user namespaces provide, because privileged containers can re-create their own user namespaces as root. Kubernetes documentation warns about this combination explicitly in the Pods security standards.

Cgroup v1 vs v2 drift

If your node runs cgroup v1 and your workload assumes v2, you’ll see OOMKilled events that don’t match expectations, or memory limits that look like they’re being ignored. The cgroup namespace exists, but the underlying hierarchy doesn’t unify. Modern containerd/Kubelet defaults to v2 — confirm with stat -fc %T /sys/fs/cgroup/.

Practical debugging: three commands you should keep handy

When a container behaves strangely, the namespace primitives give you a small, sharp toolchain for figuring out which boundary is leaking.

# 1. Find every namespace a process belongs to.
ls -l /proc/<pid>/ns/
# lrwxrwxrwx ... cgroup -> cgroup:[4026531835]
# lrwxrwxrwx ... ipc    -> ipc:[4026531839]
# lrwxrwxrwx ... mount  -> mnt:[4026531840]
# ... etc.

# 2. Compare a process's view of /proc to the host's.
ls /proc/ | wc -l
sudo nsenter -t <pid> -m -p ls /proc/ | wc -l

# 3. List all processes in a given PID namespace.
sudo ls /proc/<pid>/task/*/status | xargs grep -l "NSpid"

If two PIDs share an mnt:[...] inode, they’re in the same mount namespace. If they don’t, you’re debugging a real mount boundary.

Key Takeaways

  • Containers are processes with filtered views. Namespaces filter what a process can see; cgroups filter what it can use. Together, they are the entire isolation story.
  • The seven namespaces are created atomically. The OCI runtime spec dictates the order: clone, then cgroup, then rootfs, then netns wiring, then exec. Understanding this sequence explains most “container won’t start” errors.
  • Kubernetes reuses the primitives. Pods share network and IPC namespaces; securityContext compiles to user/mount/cgroup/capability/seccomp syscalls; the kubelet is a careful user of PID-namespace signaling.
  • Privileged is the exception that proves the rule. Most namespace hardening assumes the container is unprivileged. privileged: true opts out of most of it, which is sometimes necessary and always worth auditing.
  • Namespaces are a security boundary, not a trust boundary. A host kernel exploit or a misconfigured capability lets a process cross. Always run with the principle of least capability, even inside containers.

Further Reading