01 / What Are Namespaces
Linux namespaces are a kernel feature that partitions global system resources into isolated instances. Each process exists within a set of namespaces, and from that process’s perspective, the resource it observes through a given namespace appears to be the only one on the system. This illusion of isolation is the foundational mechanism behind containers: Docker, Podman, and LXC all ultimately call into the same kernel namespace machinery.
Namespaces were introduced incrementally into the Linux kernel starting with mount namespaces in kernel 2.4.19 (2002) and have been expanded through successive releases. They are not a security boundary by themselves — that role belongs to seccomp, LSMs (SELinux/AppArmor), and capabilities — but they are the primary mechanism for resource visibility isolation. A process inside a PID namespace cannot see processes outside it; a process in a network namespace has its own loopback interface and routing table.
At the kernel level, each namespace type is represented by a distinct struct: struct pid_namespace, struct net, struct mnt_namespace, struct uts_namespace, etc. The task struct (struct task_struct) carries a pointer to struct nsproxy, which holds references to all namespace objects the process currently belongs to. Namespace entry and exit are managed via clone(2), unshare(2), and setns(2).
02 / Namespace Types
Linux currently defines eight namespace types, each isolating a different category of global resource. Understanding which resource each type owns is essential for both container security auditing and kernel exploit research.
PID namespace — Isolates the process ID number space. Processes inside a new PID namespace can have PIDs that overlap with host PIDs. The first process in a new PID namespace always gets PID 1, becoming the init for that namespace. If it dies, all processes in the namespace receive SIGKILL. Network namespace (netns) — Provides an isolated network stack: interfaces, routing tables, firewall rules, and socket namespace. Each netns starts with only a loopback interface. Mount namespace — Isolates the filesystem mount table. Changes to mounts (bind mounts, new filesystems) are invisible outside the namespace. UTS namespace — Isolates hostname and NIS domain name. IPC namespace — Isolates System V IPC objects and POSIX message queues. User namespace — The most powerful and complex type; maps user and group IDs between namespaces, enabling unprivileged containers. Cgroup namespace — Hides the host cgroup hierarchy. Time namespace — (since 5.6) Allows per-namespace clock offsets for CLOCK_MONOTONIC and CLOCK_BOOTTIME.
03 / The unshare() Syscall
unshare(2) allows a process to disassociate parts of its execution context that were previously shared with other processes, without forking. It accepts a bitmask of CLONE_NEW* flags identical to those accepted by clone(2). The corresponding userspace utility, /usr/bin/unshare, wraps this syscall for shell use and is invaluable for container experimentation.
The critical detail about unshare --pid is that the calling process does not change its own PID namespace — only its children will run in the new namespace. This is why --fork is required when unsharing PID namespaces: it forks a child that becomes PID 1 in the new namespace, while the parent remains in the original. The --mount-proc flag additionally bind-mounts a fresh /proc instance reflecting only the new namespace’s processes.
$ sudo unshare --pid --fork --mount-proc bash
[new-ns]# echo $$
1
[new-ns]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 7236 3988 pts/0 S 14:01 0:00 bash
root 6 0.0 0.0 9596 3204 pts/0 R+ 14:01 0:00 ps aux
04 / /proc Namespace Inspection
The /proc filesystem exposes namespace membership for every process through a set of magic symlinks under /proc/<pid>/ns/. Each symlink points to a pseudo-file of the form <ns-type>:[<inode-number>]. Two processes sharing the same namespace will have symlinks pointing to the same inode number — this is how tools like lsns enumerate namespace membership.
$ ls -la /proc/$$/ns
total 0
dr-x--x--x 2 user user 0 Jul 16 14:01 .
dr-xr-xr-x 9 user user 0 Jul 16 14:01 ..
lrwxrwxrwx 1 user user 0 Jul 16 14:01 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 mnt -> mnt:[4026531841]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 net -> net:[4026531840]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 pid -> pid:[4026531836]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 pid_for_children -> pid:[4026531836]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 time -> time:[4026531834]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 user -> user:[4026531837]
lrwxrwxrwx 1 user user 0 Jul 16 14:01 uts -> uts:[4026531838]
$ readlink /proc/1/ns/pid
pid:[4026531836]
An important security note: holding an open file descriptor to any /proc/<pid>/ns/* file keeps the namespace alive even after all processes in it exit. This is exploited by container runtimes to pin namespaces, and by attackers to preserve foothold namespaces after container escape.
05 / PID Namespaces Deep Dive
PID namespaces are hierarchical: a process in a child PID namespace is also visible from ancestor namespaces, with a different PID in each. The kernel maintains a mapping between namespace-local PIDs and their global identifiers. From the host namespace, you can always see all PIDs; from inside a container, you only see PIDs in your own namespace and its descendants.
The kernel struct behind this is struct pid, which contains an array of struct upid entries, one per namespace level the process is visible in. The numbers[0] entry always holds the PID in the innermost namespace; numbers[ns->level] gives the PID as seen from ancestor namespaces. This is why getpid(2) returns different values depending on which namespace the process thinks it’s in.
One critical invariant: the process with namespace-local PID 1 is the namespace init. Signals sent to PID 1 that it doesn’t explicitly handle are silently dropped — just as on the host. If PID 1 exits, the kernel sends SIGKILL to all other processes in that namespace. This behavior is frequently misunderstood by developers writing containerized applications.
06 / Network Namespaces
Network namespaces provide complete isolation of the Linux networking stack. Each namespace has its own: network interfaces (physical or virtual), routing tables, netfilter (iptables/nftables) rule sets, socket table, and UNIX abstract socket namespace. The ip netns tool from iproute2 is the standard interface for managing named network namespaces, which are persisted as bind mounts under /var/run/netns/.
$ sudo ip netns list
myns (id: 0)
$ sudo ip netns add testns
$ sudo ip netns exec testns ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
$ lsns --type net
NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
4026531840 net 215 1 root unassigned /sbin/init
4026532008 net 2 8732 root 0 /run/netns/myns bash
Virtual Ethernet pairs (veth) are the standard mechanism for connecting network namespaces. One end of the pair lives in the container’s namespace, the other in the host or a bridge namespace. The kernel routes packets between them as if they were real Ethernet cables — this is exactly what Docker’s docker0 bridge implements.
07 / Building a Minimal Container
Combining the primitives above, we can hand-roll a minimal container from shell scripts alone — no Docker, no runtimes. The sequence is: create new namespaces, set up a root filesystem, pivot/chroot into it, configure networking, and exec the workload. This exercise concretely demonstrates that containers are not virtual machines — they share the host kernel.
# Step 1 — Enter new namespaces
$ sudo unshare --pid --net --uts --ipc --mount --fork bash
# Step 2 — Set hostname inside UTS namespace
[ns]# hostname mycontainer
# Step 3 — Mount a minimal rootfs (e.g., Alpine extracted to /tmp/rootfs)
[ns]# mount --bind /tmp/rootfs /tmp/rootfs
[ns]# mkdir -p /tmp/rootfs/proc
[ns]# mount -t proc proc /tmp/rootfs/proc
# Step 4 — Chroot into the rootfs
[ns]# chroot /tmp/rootfs /bin/sh
[container]# ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh
4 root 0:00 ps aux
Linux namespaces isolate eight distinct resource domains through a hierarchy of kernel structs referenced by task_struct.nsproxy. clone(), unshare(), and setns() are the three syscall entry points. PID namespace isolation is hierarchical: ancestors always see all descendants with remapped PIDs. Network namespaces provide a fully independent stack; veth pairs bridge them. Containers are processes in namespaces — they share the host kernel at all times.
- Why does
unshare --pidrequire the--forkflag? What happens to the calling process’s PID namespace without it, and what determines which process becomes PID 1 in the new namespace? - A process in a child PID namespace sends
SIGKILLto PID 1. What is the kernel’s behaviour? How does this differ from sending SIGKILL to a non-init process, and what are the container lifecycle implications? - Describe how Docker bridges multiple container network namespaces together. What kernel objects are involved, and how would you trace packets between two containers on the same host using
tcpdumpandip link?