Namespaces from first principles

Build a container by hand with unshare — no Docker.

Advanced16 min · lesson 1 of 25

There is no such thing as a container in the Linux kernel — there is a process, wrapped in namespaces and constrained by cgroups. The fastest way to internalize that is to build one by hand with unshare, no Docker involved. Once you have created a private PID, mount, and network view for an ordinary /bin/sh, the mystique evaporates and, crucially, so does any illusion that the boundary is thick.

terminal
# a "container" with its own PID, mount, UTS, and network namespace — no Docker
$ sudo unshare --pid --mount --uts --net --fork --mount-proc /bin/sh
# inside:
# ps -ef <- only our own processes; sh is PID 1
# hostname box <- our own hostname, host unaffected
# ip addr <- an empty network namespace (just lo, down)

One namespace per resource

Each namespace type virtualizes exactly one global resource. PID gives a process its own process-number space with its own PID 1. MNT gives it a private mount table. NET gives it its own interfaces, routes, and ports. UTS isolates the hostname; IPC isolates shared memory and semaphores; and USER — the one that matters most for security — maps user and group IDs, so root inside can be an unprivileged UID outside. Docker simply creates all of these at once and pivots into a prepared root filesystem.

The namespaces that make a container
isolate what a process SEES
pid
own process tree, own PID 1
mnt
own mount table / rootfs
net
own interfaces, routes, ports
uts / ipc
hostname, shared memory
the security-critical one
user
maps UIDs — container root → unpriv host UID
Docker creates all of these for a process and pivots into a rootfs. Any namespace you DON’T isolate (--pid=host, --net=host) is a hole straight to the host’s.

Namespaces are the visibility half only

A namespace controls what a process can see, not what it can do to what it sees. A process in its own mount namespace still runs with whatever capabilities and UID it was given, against the same kernel. That is why namespaces alone are not a security boundary — they must be paired with capabilities, seccomp, and (ideally) a user namespace. Sharing a namespace with the host deliberately removes even the visibility barrier.

terminal
# entering a running container’s namespaces from the host (how nsenter/docker exec work)
$ pid=$(docker inspect -f '{{.State.Pid}}' web)
$ sudo nsenter -t "$pid" -m -u -n -p -- ps -ef # we are now "inside" web
# the container is just PID $pid on the host wearing five namespaces
The container is a host process in a costume
From the host, docker ps hides it but ps -ef and /proc show the container’s processes as ordinary host PIDs. This is the mental model for everything that follows: hardening is about narrowing what that host process can do, because the kernel it calls into is the same one running everything else on the box.