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
// KEY TAKEAWAYS

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.

// TOOLS USED
unshare nsenter ip netns lsns /proc
// PRACTICE QUESTIONS
  1. Why does unshare --pid require the --fork flag? What happens to the calling process’s PID namespace without it, and what determines which process becomes PID 1 in the new namespace?
  2. A process in a child PID namespace sends SIGKILL to 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?
  3. 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 tcpdump and ip link?