Namespace Theory & Core Types
The isolation primitive — giving each process its own view of the system
What Namespaces Are
Namespaces are a Linux kernel feature that partitions kernel resources so that one set of processes sees one set of resources, while another set of processes sees a completely different set. Each process lives in exactly one namespace of each type.
Key Properties
- Namespaces isolate views, not resources themselves
- Each process belongs to exactly one namespace per type
- Child processes inherit their parent's namespaces by default
- Modern Linux has 8 namespace types
- Each namespace is visible as a file descriptor in
/proc/[pid]/ns/
What They Are NOT
- Not security boundaries on their own — they isolate views, not access
- Not VMs — all namespaces share the same kernel
- Not cgroups — namespaces isolate visibility, cgroups limit usage
- Not containers — a container is namespaces + cgroups + filesystem + security policies combined
Warning
Security warning: A namespace alone does not prevent a process from escaping. A process in a new PID namespace but still in the host mount namespace can read /proc on the host. You need multiple namespaces + seccomp + capabilities + cgroups together for real isolation.
Conceptual Model: Same Kernel, Different Views
PID 1: nginx
Hostname: web-01
Mounts: /app, /etc/nginx
Network: 10.0.0.2/24
PID 1: postgres
Hostname: db-01
Mounts: /var/lib/postgres
Network: 10.0.0.3/24
PID 1: systemd
Hostname: prod-server
Mounts: / (full tree)
Network: 192.168.1.10/24
The Three Syscalls
All namespace operations boil down to three system calls. Everything else (docker run, unshare CLI, ip netns) is a wrapper around these.
clone()
Creates a new process in new namespace(s). Pass CLONE_NEW* flags to specify which namespaces to create.
// Create child in new PID + mount NS
clone(child_fn, stack,
CLONE_NEWPID | CLONE_NEWNS | SIGCHLD,
arg);This is what container runtimes use under the hood.
unshare()
Moves the current process into new namespace(s). No new process is created.
// Current process gets new mount NS
unshare(CLONE_NEWNS);The unshare command-line tool wraps this syscall.
setns()
Joins an existing namespace by opening its fd from /proc/[pid]/ns/xxx.
// Join PID 1234's network NS
int fd = open(
"/proc/1234/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);nsenter and docker exec use this.
unshare CLI Example
# Create new PID + mount namespaces and fork into them
$ sudo unshare --pid --mount --fork bash
# --pid: new PID namespace
# --mount: new mount namespace (needed to remount /proc)
# --fork: fork so the new shell is PID 1 in the new PID NS
# Inside the new namespace, mount a fresh /proc
$ mount -t proc proc /proc
# Now ps only shows processes in this namespace
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 7236 3968 pts/0 S 12:00 0:00 bash
root 2 0.0 0.0 10072 3312 pts/0 R+ 12:00 0:00 ps aux
/proc/[pid]/ns/ — Namespace File Descriptors
Every namespace instance is represented as a pseudo-file (actually a symlink) in /proc/[pid]/ns/. The symlink target includes the namespace type and an inode number that uniquely identifies the namespace instance.
# List all namespace fds for PID 1 (init/systemd)
$ ls -la /proc/1/ns/
lrwxrwxrwx 1 root root 0 Mar 31 12:00 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 pid_for_children -> 'pid_for_children:[4026531836]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Mar 31 12:00 uts -> 'uts:[4026531838]'
Checking if Two Processes Share a Namespace
# Same inode = same namespace
$ readlink /proc/1/ns/net
net:[4026531992]
$ readlink /proc/1234/ns/net
net:[4026531992]
# Same! Both in host network NS
$ readlink /proc/5678/ns/net
net:[4026532456]
# Different! PID 5678 is in a container
nsenter: Joining a Process's Namespaces
# Enter ALL namespaces of PID 1234
$ nsenter -t 1234 -a bash
# Enter only network namespace
$ nsenter -t 1234 -n ip addr
# Enter network + mount namespaces
$ nsenter -t 1234 -n -m bash
# This is what "docker exec" does
# internally (via setns syscall)
Mount Namespace (mnt) — CLONE_NEWNS
The first namespace type ever added to Linux (2.4.19, 2002). It was so new the flag is just CLONE_NEWNS ("new namespace") rather than CLONE_NEWMNT because nobody imagined there'd be more types.
What It Isolates
Each mount namespace has its own mount table — an independent list of mount points. Mounting or unmounting filesystems in one namespace has no effect on other namespaces (assuming private propagation).
- Independent
/,/tmp,/proc,/sys,/dev - Container gets its own rootfs (from the image) without affecting the host
- Bind mounts, tmpfs mounts, overlay mounts — all per-namespace
pivot_root vs chroot
- Changes the apparent root directory for a process
- Original root is still accessible — easy to escape
- A process with
CAP_SYS_CHROOTcan break out by creating a new chroot and using..traversal - Does NOT change the mount table
- Meant for build environments, not security
# Classic chroot escape
$ chroot /newroot /bin/bash
# Process can still access
# host root via fd tricks
- Swaps the root mount — old root becomes a subdirectory
- Old root can be unmounted — truly inaccessible
- Requires a mount namespace (can't affect host mounts)
- This is what container runtimes actually use
- No escape path once old root is unmounted
# pivot_root swaps root mount
$ pivot_root /newroot /newroot/.old
$ umount -l /.old
# Old root is gone. No escape.
Mount Propagation
When namespaces share mount points, mount propagation controls whether mounts in one namespace appear in another. There are four modes:
| Mode | Behavior | Use Case |
|---|---|---|
shared |
Mounts propagate both ways between namespaces | Host systemd mounts, initial setup |
private |
Mounts never propagate — fully independent | Default for containers |
slave |
Mounts propagate one way (host → container, not back) | Container sees new host mounts but can't leak its own |
unbindable |
Like private, but also can't be bind-mounted | Prevent recursive bind mount explosions |
# Check propagation type of a mount
$ findmnt -o TARGET,PROPAGATION
TARGET PROPAGATION
/ shared
/tmp private
/run/user/1000 shared
# Make a mount private
$ mount --make-private /mnt/data
# Make a mount shared
$ mount --make-shared /mnt/data
PID Namespace (pid) — CLONE_NEWPID
PID namespaces give each container its own process ID numbering starting at 1. The first process in a PID namespace becomes PID 1 and has special responsibilities.
Nested PID View
A process has a PID in every ancestor namespace. The host always sees the "real" PID; the container sees its own numbering:
Host view (ps aux on host):
PID COMMAND
1 systemd
856 containerd
12345 nginx: master
12346 nginx: worker
12400 postgres
Container view (ps aux inside):
PID COMMAND
1 nginx: master
2 nginx: worker
PID 1 inside = PID 12345 outside
Container view (ps aux inside):
PID COMMAND
1 postgres
PID 1 inside = PID 12400 outside
The PID 1 Problem: Zombie Reaping
Why PID 1 Matters in Containers
In Linux, PID 1 has a special kernel-level responsibility: it adopts orphaned child processes and must call wait() on them. If it doesn't, zombie processes accumulate.
- On a normal host,
systemd/inithandles this — it's designed for it - In a container, your app (nginx, node, python) is often PID 1 — but most apps don't reap children
- Zombies consume PID space. Enough of them and the container can't spawn new processes
- PID 1 also gets different signal handling:
SIGTERMandSIGINTare not delivered unless the process explicitly registers a handler
# Dockerfile
CMD ["node", "server.js"]
# node is PID 1
# If it spawns child processes
# (e.g., child_process.exec),
# orphans become zombies# Dockerfile
RUN apt-get install -y tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]
# tini is PID 1, reaps zombies
# node runs as PID 2
# Signals are forwarded properlyTip
Docker --init: You can also use docker run --init myimage which injects tini as PID 1 automatically, without modifying the Dockerfile. Kubernetes doesn't have a built-in equivalent — bake tini into the image or use the shareProcessNamespace feature (where the kubelet becomes PID 1).
Demo: PID Namespace in Action
# Create new PID + mount namespaces
$ sudo unshare --pid --fork --mount-proc bash
# --mount-proc remounts /proc so ps works correctly
# We are now PID 1 inside the new namespace
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 7236 3968 pts/0 S 12:00 0:00 bash
root 2 0.0 0.0 10072 3312 pts/0 R+ 12:00 0:00 ps aux
$ echo $$
1
# Meanwhile, on the host, this bash has a normal PID (e.g., 54321)
UTS Namespace (uts) — CLONE_NEWUTS
The simplest namespace type. UTS stands for "UNIX Time-Sharing" — named after the struct utsname in the kernel (returned by the uname() syscall).
What It Isolates
- Hostname (
nodename) — each container has its own hostname - NIS domain name (
domainname) — rarely used in containers - Setting
hostnameinside a container does not affect the host - This is why
docker run --hostname myappworks — it creates a new UTS namespace
# Create a new UTS namespace
$ sudo unshare --uts bash
# Change hostname inside the namespace
$ hostname container-web-01
$ hostname
container-web-01
# Open another terminal on the host
$ hostname
prod-server
# Host hostname is unaffected
Tip
Kubernetes note: Each pod gets its own UTS namespace. The pod's hostname defaults to the pod name. You can override it with spec.hostname and spec.subdomain in the pod spec.
Note
Network namespaces are covered in detail in Net 09 — Namespaces & Virtual Interfaces. That page covers CLONE_NEWNET, veth pairs, bridges, and how container networking is built on top of network namespaces.
Summary: Core Namespace Lifecycle
clone() / unshare()
Create new namespace(s) with CLONE_NEW* flags
Process runs isolated
Own PID tree, mount table, hostname, etc.
/proc/[pid]/ns/*
NS persists as long as any process or fd references it
setns() / nsenter
Other processes can join via fd