It took ten years of KEPs, six years of active development, and four releases of beta polishing, but on April 23, 2026, Kubernetes v1.36 "Haru" finally graduated User Namespaces to General Availability. One line of pod spec — hostUsers: false — now changes the single most important sentence in any multi-tenant threat model.
Here is that sentence, before and after, for two tenants packed onto the same bare-metal node:
| Without user namespaces | With hostUsers: false | |
|---|---|---|
| UID of "root" in the container | 0 on the host — the real root | Mapped to an unprivileged host UID (typically 100000+) |
| What a container escape yields | Host root: read every tenant's volumes, secrets, kubelet credentials | An unprivileged process with no special standing on the node |
Granted capabilities (CAP_NET_ADMIN, CAP_SYS_ADMIN) | Valid on the host | Scoped to the namespace; CAP_SYS_MODULE is void for loading kernel modules |
| Blast radius of one compromised app | Every tenant on the node | That tenant's own sandbox |
For anyone running a self-hosted PaaS — packing many customers' build and runtime pods onto shared hardware — this is the biggest structural weakening of "container escape gets you host root" available without paying for a full VM boundary. This post walks through what actually shipped, the named CVEs it defuses, the exact version matrix your fleet must clear to turn it on, and — honestly — the kernel-level attack surface it does not close.
Why It Took Ten Years — and What hostUsers: false Actually Does
Linux has supported user namespaces since kernel 3.8 in 2013. Kubernetes started down this road with KEP-127, shipped alpha support in v1.25 (August 2022), reached beta in v1.30, turned the feature gate on by default in v1.33, and declared GA in v1.36. So why the decade?
Volumes. The naive implementation had to recursively chown every file a pod mounted so that on-disk ownership matched the shifted UIDs — an O(n) walk over potentially millions of files, adding multi-minute startup delays for large persistent volumes. The breakthrough was ID-mapped mounts, merged into Linux 5.12 and extended to tmpfs in 6.3: the kernel translates UIDs and GIDs at mount time, in O(1), with nothing rewritten on disk. From inside the container, files look owned by root; on disk, nothing changed.
With that solved, the mechanics are almost anticlimactic:
apiVersion: v1
kind: Pod
spec:
hostUsers: false
containers:
- name: app
image: myapp:latestNo image changes. No rebuilt binaries. The process inside still believes it is UID 0 and behaves normally — installing packages, binding low ports, writing to /. The kernel simply refuses to let that belief mean anything on the host.
The subtler win is what happens to capabilities. A pod that needs CAP_NET_ADMIN to manage its own network stack historically held a capability that was valid on the host if the process ever escaped. Inside a user namespace, that capability is scoped: it grants administrative power over container-local resources only. CAP_SYS_MODULE — the "load arbitrary code into the kernel" capability — becomes effectively void.
The CVE Ledger: What a Mapped Namespace Neutralizes
This is not theoretical hardening. A string of the most famous container escapes of the past seven years are partially or completely defused when container root is not host root:
- CVE-2019-5736 — the runc escape: a malicious container overwrites the host
runcbinary through/proc/self/exe. Requires the container process to have write access as host root; a mapped UID does not. - CVE-2021-25741 — a symlink race in kubelet
subPathvolume handling that let a pod read or write host paths with the privileges of its host-level UID. - CVE-2022-0492 — the cgroup
release_agentescape: abuse of cgroup-v1 semantics to execute a binary as full host root. Namespaced capabilities close the door. - CVE-2024-21626 — the runc file-descriptor leak (
Leaky Vessels): a working-directory trick giving access to the host filesystem, again with the severity hinging on who the escaping process is on the host.
For a multi-tenant platform, the arithmetic matters more than any single CVE. Without user namespaces, one compromised WordPress plugin in one tenant's pod plus one kernel-adjacent escape equals every tenant's environment variables, mounted secrets, and volumes on that node. With hostUsers: false as the default, the same chain ends at an unprivileged process staring at file permissions it cannot bypass. "Escape = everyone is compromised" becomes "escape = an unprivileged shell," and those are different businesses to be in.
Turning It On Fleet-Wide: the Version Matrix Your Nodes Must Clear
GA in the API server is necessary but not sufficient. User namespaces lean on the newest parts of the container stack, and every layer has a floor:
| Component | Minimum for production use |
|---|---|
| Linux kernel | 6.3+ (idmapped tmpfs — needed for service-account tokens, Secrets, ConfigMaps; 5.12+ is partial) |
| containerd | 2.0+ (1.7 falls back to recursive chown — the slow path) |
| CRI-O | 1.30+ |
| OCI runtime | runc 1.2+ or crun 1.9+ |
| Volume filesystems | idmap-capable: ext4, xfs, btrfs, overlayfs, tmpfs; NFS support varies — test it |
Two gotchas worth pinning to the wall:
- Every filesystem used by the pod's volumes needs idmap-mount support in the kernel — one unsupported NFS mount blocks the pod.
- Some hardened node OSes disable the prerequisite outright: Bottlerocket ships
user.max_user_namespaces=0by default, which silently breaks anything relying on user namespaces (this bites EKS Auto Mode users today).
Here is where a self-hosted fleet has a genuine structural advantage. If you rent managed Kubernetes, you get the kernel your provider ships — and through 2024, the major managed platforms were still rolling out 5.x kernels. If you run your own Cluster API fleet on bare metal — Ubuntu 24.04 with a 6.8 kernel on a Hetzner box, say — the entire matrix above is a node-image decision you own. Build one image with kernel 6.3+, containerd 2.0, and runc 1.2, point your MachineDeployment at it, and Cluster API rolls the fleet node by node with workloads draining ahead of each replacement. There is no ticket to file and no waiting for a cloud's kernel-qualification cycle.
And it applies to both halves of a PaaS's tenant workload. Runtime pods are the obvious case. But build pods are arguably more urgent: a git-push platform executes Dockerfile instructions and buildpack processes that are effectively tenant-supplied code. Rootless BuildKit exists precisely for this — the daemon runs inside a user namespace so a compromised build lands as an unprivileged user — and pod-level hostUsers: false extends the same property to the whole build environment with one field instead of a custom rootless deployment.
What It Does Not Fix: the Kernel Is Still Shared
Now the part vendors' announcement posts tend to whisper. User namespaces remap identity above the kernel. Every container on the node still makes syscalls into the same kernel, and a kernel exploit operates below the layer where UID remapping applies. A working kernel LPE bypasses user namespace protections entirely.
Worse, the mechanism cuts both ways. The same scoping that makes CAP_NET_ADMIN "safe" to grant also exposes kernel subsystems to processes that hold namespaced capabilities — subsystems an unprivileged process could never previously reach. The numbers here deserve to be stated plainly:
- Security researchers at Edera count roughly 40 CVEs over five years that are only reachable when user namespaces are enabled — with kernel operations reachable from an unprivileged context jumping 262% once userns creation is allowed.
- 18 of those 40 sit in nf_tables alone, the kernel's packet-filtering subsystem, reachable via namespaced
CAP_NET_ADMIN. - CVE-2024-1086, a double-free in nf_tables verdict handling, achieved a 99.4% reliable container-escape exploit — with the proof-of-concept publicly available on GitHub.
There is also a gap in when the protection applies. The user namespace wraps the running container process — but OCI hooks (GPU driver injection, some CSI operations) execute as host root before the namespace exists. That window produced CVE-2025-23266 ("NVIDIAScape"), a CVSS 9.0 flaw where a malicious LD_PRELOAD in a container image was inherited by the NVIDIA Container Toolkit's hook running as host root — affecting an estimated 40% of GPU environments. hostUsers: false does nothing for that class of bug.
So the honest statement is narrower than the headline: user namespaces demote the most common escape class — misconfigured privilege and runtime bugs — from "host root" to "unprivileged user." They do not turn a shared kernel into a hypervisor. Harden accordingly: keep seccomp defaults on (RuntimeDefault blocks a large share of the exotic syscall surface, including much of what makes nf_tables reachable), keep tenant pods off host networking, and patch kernels like it matters — because it still does.
The Verdict: When Namespace-per-Tenant on Shared Hardware Is Defensible
Put the two halves together and a clear layered posture falls out for a multi-tenant platform:
hostUsers: falseas the default floor for every tenant pod — runtime and build. It costs one field, requires no image changes, adds no startup latency on containerd 2.0+, and converts the most common escape outcomes into unprivileged access. There is no longer a good reason for a tenant workload to share the host's user namespace.- Seccomp + no-host-network as non-negotiable companions. User namespaces widen the syscall-reachable kernel surface for capability holders; the default seccomp profile narrows it back.
- A VM or sandbox boundary where the threat model actually demands it. Truly adversarial code execution (a public "run any code" product), GPU workloads exposed to hook-path bugs like NVIDIAScape, or compliance regimes that mandate hardware isolation still call for Kata Containers or gVisor on those specific node pools — with the boot-time, memory, and compatibility taxes those bring.
Why is the GA graduation the lever, rather than some new admission policy? Because policy was never the blocker — capability was. You could always write a Kyverno rule demanding hostUsers: false; until the stack underneath could honor it without recursive-chown startup penalties, beta-gate caveats, and runtime gaps, the rule was unenforceable in practice. GA means the API is stable, the feature gate is locked on, containerd 2.0 and crun make it fast, and a fleet can default it rather than pilot it. That is the difference between an isolation story you present in a security review with a straight face and a shared-kernel leap of faith.
For self-hosted platforms, the sequencing is unusually favorable: you control the kernel, the runtime, and the node image, so the version matrix is a weekend of image-building away rather than a year of waiting on a managed provider's roadmap. Namespace-per-tenant on shared hardware — with userns as the floor, seccomp as the fence, and VMs reserved for the workloads that need them — is now a defensible architecture, not an act of faith.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Because a Bex fleet is built on Cluster API with node images you control, clearing the user-namespace version matrix is a node-image update, not a support ticket. Star the repo on GitHub or deploy your first app today.



