Step 1 of 5 · Reading · ~4 min
Read
Linux Namespaces
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.)
Why the exercise is a simulator
Writing /proc/<pid>/uid_map needs a privileged writer, and the sandbox has none. But the user namespace is the one namespace whose whole substance is arithmetic on a small table — there is no device, no filesystem, no scheduler behind it. Reimplementing the table is reimplementing the feature. When your TRANSLATE is right, you have written the check the kernel performs on every file the container creates.
Your exercise: User Namespace Translator
| The kernel does | Your simulator does |
|---|---|
clone(CLONE_NEWUSER) — a namespace with an empty map; every id is unmapped until written | NEWNS — new namespace, no ranges; emit the id |
a line <inside> <outside> <length> written to uid_map; overlapping ranges are rejected with EINVAL | MAP <ns> <in> <host> <len> — OK, or ERR overlap when the inside range collides |
from_kuid() — inside id to host id, when a file is written | TRANSLATE <ns> <in_id> |
make_kuid() — host id to inside id, when a file is read; no range means the overflow id | WHO <ns> <host_id>, unmapped when nothing matches |
The arithmetic is four tokens long and every mistake in it lives at the edges: outside = outside_start + (id − inside_start), valid only while inside_start ≤ id < inside_start + length. MAP 1 0 100000 65536 maps 65535 and does not map 65536; a <= there is the difference between a correct mapping and one that silently hands the container a host uid it was never granted. The overlap check has the same shape — two ranges collide unless one ends before the other begins.
Note the two directions are not symmetric in what they read: TRANSLATE matches on the inside column, WHO on the host column, and the same table serves both. That is why one file has a single owner inside and a single, different owner outside, with nothing storing the pair.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…