Namespaces partition kernel resources so one set of processes sees one set of resources, another sees a different set. They are the isolation layer behind every container runtime; cgroups (the resource-limiting half of containers) are a separate mechanism.
Three syscalls power the entire namespace subsystem.
| Syscall | Signature | Purpose |
|---|---|---|
| clone() | clone(fn, stack, flags, arg) | Create a child process in new namespace(s). Flags are OR'd CLONE_NEW* constants. |
| unshare() | unshare(flags) | Move the calling process into new namespace(s). No fork needed. |
| setns() | setns(fd, nstype) | Join an existing namespace via its /proc fd. This is what nsenter uses. |
C // create child in new PID + UTS namespace int flags = CLONE_NEWPID | CLONE_NEWUTS | SIGCHLD; pid_t pid = clone(child_fn, stack + STACK_SIZE, flags, arg); // move current process into a new network namespace unshare(CLONE_NEWNET); // join an existing namespace int fd = open("/proc/12345/ns/net", O_RDONLY); setns(fd, CLONE_NEWNET);
Enter existing namespaces of a running process. Uses setns() under the hood.
enter by pid # enter all namespaces of PID 12345 $ sudo nsenter --target 12345 --all bash # enter just the network namespace $ sudo nsenter --target 12345 --net bash # enter a Docker container (bypass docker exec) $ PID=$(docker inspect -f '{{.State.Pid}}' mycontainer) $ sudo nsenter --target $PID --all bash # useful when dockerd is stuck but you need into a container
A new net namespace starts with nothing. You build the network from scratch using veth pairs.
veth pair # create a virtual ethernet pair (two ends of a cable) # ip link add veth-host type veth peer name veth-ns # move one end into the namespace # ip link set veth-ns netns myns # assign IPs # ip addr add 10.200.1.1/24 dev veth-host # ip netns exec myns ip addr add 10.200.1.2/24 dev veth-ns # bring up # ip link set veth-host up # ip netns exec myns ip link set veth-ns up # ip netns exec myns ip link set lo up # test # ip netns exec myns ping 10.200.1.1
internet access # enable IP forwarding # echo 1 > /proc/sys/net/ipv4/ip_forward # NAT the namespace traffic # iptables -t nat -A POSTROUTING -s 10.200.1.0/24 -j MASQUERADE # default route inside the namespace # ip netns exec myns ip route add default via 10.200.1.1 # now the namespace can reach the internet # ip netns exec myns curl ifconfig.me
ip netns $ ip netns add myns # create $ ip netns list # list $ ip netns exec myns bash # enter $ ip netns exec myns ip a # run command inside $ ip netns del myns # delete # ip netns creates bind mounts at /var/run/netns/<name> # this keeps the namespace alive even with no processes in it
Every process exposes its namespace memberships as symlinks under /proc/<pid>/ns/. Same inode number = same namespace.
inspection # your own namespaces $ ls -la /proc/$$/ns/ # compare two processes $ readlink /proc/1/ns/pid pid:[4026531836] $ readlink /proc/$$/ns/pid pid:[4026531836] # same inode = same namespace # list all namespaces on the system $ lsns # filter by type $ lsns -t net # a process has TWO PIDs in a PID namespace $ grep NSpid /proc/<pid>/status NSpid: 12345 1 # host PID 12345, container PID 1
The most security-relevant namespace. Maps UIDs between namespaces. Root inside the container is an unprivileged user on the host.
uid_map # /proc/<pid>/uid_map format: # <id_inside> <id_outside> <range> 0 1000 1 # UID 0 inside = UID 1000 outside # "root" in the container is # your unprivileged user on host
rootless # no sudo needed $ unshare --user --map-root-user bash # inside: # whoami root # id uid=0(root) gid=0(root) # but on the host, it's still you
gid_map by hand from an unprivileged process fails with EPERM until you write deny to /proc/<pid>/setgroups first (kernel 3.19+, closes a setgroups(2) privilege trick). unshare --map-root-user does this for you, which is why the examples above just work.
manual mapping # from a second terminal, for an unshared process <pid>: $ echo deny > /proc/<pid>/setgroups # required before gid_map $ echo "0 1000 1" > /proc/<pid>/uid_map $ echo "0 1000 1" > /proc/<pid>/gid_map
A container is just namespaces + pivot_root + cgroups. The sequence:
mount --make-rprivate / so nothing you do propagates back to the host mount namespace.pivot_root requires its new root to be a mount point; a plain directory needs a self bind-mount first.put_old and gets lazily unmounted.bash #!/usr/bin/env bash # minimal container in ~25 lines ROOTFS="/tmp/container-root" mkdir -p "$ROOTFS"/{bin,proc,.old} # install a minimal userland (busybox) cp /path/to/busybox "$ROOTFS/bin/busybox" for cmd in sh ls ps mount umount cat echo; do ln -sf busybox "$ROOTFS/bin/$cmd" done # launch with all namespaces sudo unshare --pid --uts --mount --ipc --net --fork \ /bin/bash -c " hostname container mount --make-rprivate / # no propagation to host mount --bind $ROOTFS $ROOTFS # pivot_root needs a mount point cd $ROOTFS pivot_root . .old mount -t proc proc /proc umount -l /.old exec /bin/sh "
docker run --init (tini) exists is different: a typical application does not reap orphaned zombies or forward signals the way an init should, so long-running containers that spawn children want a tiny real init as PID 1.
Simplified flow of docker run. The namespace and pivot_root work happens inside runc, exactly as in the from-scratch demo, just in Go. Networking is set up by dockerd before the runtime starts.
docker run flow 1. docker CLI --> dockerd (REST API call) 2. dockerd / libnetwork prepares networking (CNM model): - veth pair, docker0 bridge attach, iptables NAT rules 3. dockerd --> containerd (gRPC) --> containerd-shim-runc-v2 --> runc 4. runc (OCI runtime): a. clone(CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWNS | ...) b. In child: - Set up cgroups - Set up rootfs (overlay mounts), mount /proc /sys /dev - pivot_root - Set hostname, join the prepared network namespace - Drop capabilities - Set seccomp filters - Set AppArmor/SELinux labels - execve(container entrypoint) 5. Container process running, fully isolated
Mounts can propagate between namespaces. This is often the source of subtle container bugs.
| Mode | Behavior | Use Case |
|---|---|---|
| private | Changes stay in this namespace only | Default for containers |
| shared | Changes propagate to all peer mounts | Host mounts visible in containers |
| slave | Receives propagation but doesn't send | One-way host-to-container sync |
| unbindable | Can't be bind-mounted at all | Prevent mount namespace escapes |
check propagation $ findmnt -o TARGET,PROPAGATION # make a mount private (no propagation) # mount --make-private /mnt/data # make a mount shared (propagate everywhere) # mount --make-shared /mnt/data # recursively make everything private (container startup) # mount --make-rprivate /
no network # run a process with zero network access $ sudo unshare --net -- bash -c ' curl google.com # fails: no interfaces ping 8.8.8.8 # fails: no route ' # useful for sandboxing builds, running untrusted code, etc.
hostname $ sudo unshare --uts -- bash -c ' hostname devbox exec my-app ' # my-app sees hostname "devbox" # host hostname is untouched
rootless # no sudo needed $ unshare --user --map-root-user --pid --fork --mount-proc bash # you're now "root" with isolated PIDs # whoami root # ps aux PID USER COMMAND 1 root bash
docker namespaces $ docker run -d --name test alpine sleep 3600 $ PID=$(docker inspect -f '{{.State.Pid}}' test) # compare container vs host namespaces $ for ns in /proc/$PID/ns/*; do type=$(basename $ns) cnt=$(readlink $ns) host=$(readlink /proc/1/ns/$type) [[ "$cnt" != "$host" ]] && echo "ISOLATED: $type" done ISOLATED: cgroup ISOLATED: ipc ISOLATED: mnt ISOLATED: net ISOLATED: pid ISOLATED: uts # note: default docker does NOT use user or time namespaces
rescue # enter a container when docker exec hangs or dockerd is dead # containerd v2 shim keeps the pid here: $ PID=$(cat /run/containerd/io.containerd.runtime.v2.task/moby/<container-id>/init.pid) # or just find it: ps --forest, pgrep -f <entrypoint> $ sudo nsenter --target $PID --all bash # you're inside the container, full shell # works even if the docker daemon is completely dead
/proc/<pid>/ns/* | Namespace symlinks |
/proc/<pid>/uid_map | UID mapping |
/proc/<pid>/gid_map | GID mapping |
/proc/<pid>/setgroups | Must be "deny" before gid_map |
/proc/<pid>/status | NSpid, NStgid, etc. |
/var/run/netns/ | Named net namespaces |
unshare | Create new namespaces |
nsenter | Enter existing namespaces |
lsns | List namespaces |
ip netns | Manage net namespaces |
findmnt | Mount propagation info |
pstree | PID namespace trees |
$ man 7 namespaces # overview $ man 7 user_namespaces # UID mapping details $ man 7 pid_namespaces # PID 1 semantics $ man 7 cgroups # resource limits (the other half of containers) $ man 2 unshare # syscall $ man 2 clone # syscall $ man 2 setns # syscall $ man 2 pivot_root # the mount-point requirement lives here