Reading — step 1 of 5
Read
User Namespace
The fourth lie is the boldest: who you are. Inside the container, the process runs as root — UID 0, full privileges, can install packages, bind low ports, chown anything. On the host, that same process is UID 100000: an unprivileged nobody. The user namespace is a translation table for identity, and it's the piece that made containers acceptable to security teams.
The mapping
A user namespace (CLONE_NEWUSER) comes with a UID map — literal ranges written to /proc/<pid>/uid_map:
0 100000 65536
Read: "UIDs 0–65535 inside this namespace correspond to host UIDs 100000–165535." (A GID map does the same for groups.) Every identity check crossing the boundary gets translated:
- Inside,
idsaysuid=0(root)— the process genuinely experiences root. - The process creates a file; inside it's owned by root; the host inode records owner 100000.
- The process tries something host-privileged (loading a kernel module, rebooting): the kernel checks its host identity — UID 100000, unprivileged — denied.
The elegant part: "root" inside the namespace is real, but its privileges are scoped to namespaced resources. Root-in-a-user-namespace can mount filesystems in its own mount namespace and configure interfaces in its own network namespace — full admin of its own bubble, tourist everywhere else.
Why this matters more than the other lies
Without user namespaces, container root is host root — same UID 0, contained only by the other namespaces' walls. Any container-escape bug (a kernel vulnerability, a leaked mount) hands the attacker root on the host. It works — most of the world ran this way for years — but it means every container is one bug away from owning the machine.
With user namespaces, escape lands you as UID 100000: no home, no sudo, no permissions, blast radius of a guest account. This is rootless containers — the daemon and the containers run unprivileged (Podman's default, Docker's rootless mode), and even "root inside" is an accounting fiction the host never honors. Defense in depth, implemented as arithmetic.
The mapping also solves mundane problems: two containers both writing as "root" to their volumes get distinct host owners (100000 vs 200000 with disjoint maps) — accidental cross-container file access dies at the inode permission check. (And the classic bind-mount permission headache — host files owned by your laptop UID looking like nobody inside — is exactly this translation running in reverse; now you can debug it from first principles.)
Your exercise: User Namespace Translator
Implement the mapping engine: parse map entries (inside_start outside_start length, possibly several per namespace), translate inside→outside and outside→inside, and answer "unmapped" honestly when a UID falls in no range (real kernels surface unmapped IDs as the overflow UID 65534, nobody — the spec will tell you its convention). The details graders love: range arithmetic is outside = outside_start + (uid - inside_start) only when inside_start ≤ uid < inside_start + length — off-by-one at the range's end is the whole difficulty; and nested namespaces translate step by step, one map at a time, exactly like the kernel. Small algorithm, big consequence: it's the difference between "root escaped a container" and "an unprivileged process is loose."
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…