Skip to content
Lesson 4 of 19

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, id says uid=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 doesYour simulator does
clone(CLONE_NEWUSER) — a namespace with an empty map; every id is unmapped until writtenNEWNS — new namespace, no ranges; emit the id
a line <inside> <outside> <length> written to uid_map; overlapping ranges are rejected with EINVALMAP <ns> <in> <host> <len>OK, or ERR overlap when the inside range collides
from_kuid() — inside id to host id, when a file is writtenTRANSLATE <ns> <in_id>
make_kuid() — host id to inside id, when a file is read; no range means the overflow idWHO <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.

Up nextUTS NamespaceLinux Namespaces

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…