Skip to main content

User Namespaces Are GA in Kubernetes 1.36: Exactly What It Buys You (and What It Doesn't) for Multi-Tenant and AI-Agent Nodes

11 min readDora NodaDora Noda
Share

For as long as Kubernetes has existed, "root in the container" and "root-adjacent on the node" have been uncomfortably close neighbors. A container-root compromise on a shared node — a bad image, a leaky sidecar, a runc bug — has always been one kernel bug away from host-root. Kubernetes v1.36, released April 23, 2026, finally closes that gap by default: User Namespaces graduated to General Availability.

That's a bigger deal than the changelog entry suggests, because the fix is almost boringly simple. Set one field, hostUsers: false, and the kernel starts lying to your container about who it is — convincingly enough that a root-inside-container process becomes a nobody-on-the-host process, with zero image changes and zero application awareness. But "simple to enable" and "safe to rely on for adversarial workloads" are different claims, and the gap between them matters most for exactly the workload class driving multi-tenant density right now: AI agents executing model-generated code on shared nodes. This post is the concrete version of both halves — the CVE table for what User Namespaces actually neutralizes, the specific place it stops working, and the checklist to run against a Cluster API fleet before you flip it on as a default posture.

What GA Actually Changes

The mechanism is old — Linux has had user namespaces since kernel 3.8 — but making it practical for Kubernetes pods took a decade, because the naive implementation was too slow to ship. Here's the pod spec that turns it on:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: isolated-workload
spec:
  hostUsers: false
  containers:
    - name: app
      image: postgres:17
      securityContext:
        runAsUser: 0

With hostUsers: false, the kubelet allocates a private 65,536-UID range per pod and remaps it. UID 0 inside the container — still genuinely root, with a real root-capable process — lands on some high, unprivileged UID (commonly 100000+) on the host node. CAP_NET_ADMIN, CAP_SYS_ADMIN, and friends still work, but only against resources inside the pod's own namespace. A pod that needs real root to bind a privileged port, run iptables, or chroot during a build keeps working exactly as before; the node just no longer trusts that root the way it used to.

The reason this took until 2026 is storage. Early implementations required a recursive chown of every file in every mounted volume at pod startup, so that on-disk ownership matched the new UID mapping — a multi-minute penalty on any real Postgres volume. The fix was ID-mapped mounts, which landed in Linux 5.12 for block filesystems and were only extended to tmpfs (which the kubelet needs for emptyDir and projected service-account-token volumes) in kernel 6.3. That's the real floor, not the 5.12 everyone quotes from memory. Ownership on disk never changes; the kernel translates UIDs at the mount layer, an O(1) operation regardless of volume size.

The feature's road to GA: alpha in v1.25 (stateless pods only), reworked onto idmap mounts and extended to stateful pods by v1.28–v1.30, enabled by default in v1.33, and GA — stable API, no feature gate required — in v1.36.

What It Buys You: A CVE-by-CVE Accounting

"Reduces blast radius" is the pitch every isolation technology makes. Here's what it concretely means for User Namespaces — four real, high-severity container-escape CVEs from the last several years, and exactly how UID remapping defeats each one:

CVEVulnerability classWhat happens under hostUsers: false
CVE-2019-5736runc binary overwrite via /proc/self/exeEscaping process lands as an unprivileged host UID; the overwrite it depends on requires host-root file permissions it no longer has
CVE-2021-25741subPath symlink traversal to host filesThe pod's remapped UID owns nothing on the host filesystem outside its namespace, so the traversal reaches files it still can't write
CVE-2022-0492cgroup release_agent privilege escalationCAP_SYS_ADMIN is namespaced — writing release_agent no longer reaches a host-executed hook
CVE-2024-21626runc leaked file-descriptor container breakoutThe inherited host fd is owned by an unprivileged UID from the escaping process's point of view, blocking the follow-on host compromise

The pattern across all four: each one is a privilege-escalation bug, not a raw memory-corruption kernel exploit — the payload depends on the escaping process actually mattering once it's on the host side of the boundary. User Namespaces don't patch the underlying bugs; they make the bugs land somewhere worthless.

That's also why this is cheap compared to the alternatives already on every platform team's isolation shortlist. gVisor reimplements the Linux syscall surface in userspace and intercepts every syscall through its Sentry — real protection, but 10–30% I/O overhead and roughly 70% workload compatibility, because uncommon syscalls simply aren't implemented. Kata Containers boots a full microVM per pod — genuine kernel-level isolation, full Linux compatibility, but 150–300ms of VM boot latency and the loss of conveniences like kubectl port-forward. User Namespaces add neither a hypervisor nor a userspace kernel reimplementation: it's UID bookkeeping the kernel already does for every process, so the runtime overhead is close to zero. That's the trade — Kata and gVisor buy you a stronger boundary at a real latency and compatibility cost; User Namespaces buy you a meaningfully higher floor for nearly free.

Where It Stops: The AI-Agent-Sandbox Gap

"Nearly free" is also the tell that it isn't the same guarantee. Two gaps matter enough that they should change what you run on a User-Namespaces-only node.

The OCI-hook execution window. OCI runtime hooks — the mechanism GPU device plugins, CSI drivers, and CDI (Container Device Interface) specs use to inject devices and run setup logic — execute before the user namespace and pivot_root are applied. They run with the container's environment but as host root, because the isolation boundary the hook is supposed to prepare doesn't exist yet when the hook itself runs. That's not a theoretical gap: CVE-2025-23266, dubbed "NVIDIAScape," is exactly this — an LD_PRELOAD payload embedded in a container image that executes during nvidia-ctk's createContainer hook, before namespacing takes effect, giving host-root code execution regardless of whether the pod later drops into an unprivileged UID range. A GPU-scheduling AI workload is the textbook case that hits this path.

The shared kernel. User Namespaces remap identity; they don't remap the kernel. Every pod on the node — remapped or not — still shares one kernel image, one set of syscall handlers, one set of side channels. A genuine kernel 0-day or a speculative-execution side channel crosses the tenant boundary exactly as freely as it did before v1.36. This is the one property gVisor and Kata actually buy that User Namespaces structurally cannot: gVisor interposes a userspace kernel between the pod and the real one; Kata gives each pod its own guest kernel entirely.

Kubernetes' own SIG Apps drew this line explicitly when it shipped the Agent Sandbox project (kubernetes-sigs/agent-sandbox, announced at KubeCon NA 2025) — a purpose-built CRD for exactly the workload this post is about: long-running, stateful, singleton pods that execute autonomously generated code. Its own documentation is blunt about the pairing: "When an AI agent generates and executes code autonomously, security is paramount. The Sandbox custom resource natively supports different runtimes, like gVisor or Kata Containers. This provides the necessary kernel and network isolation required for multi-tenant, untrusted execution." User Namespaces aren't positioned as a substitute for that runtime choice — they're the baseline every Sandbox pod should also have turned on underneath it.

The decision rule this settles into: if every pod on a node is written by a trusted engineering org — even a multi-tenant SaaS where "tenant" means "paying customer running our own image," not "arbitrary code" — User Namespaces alone is a legitimate, cheap, default-on posture. The moment a pod executes model-generated code you didn't review — an AI agent's arbitrary shell commands, a code-interpreter tool call, a customer-submitted script — that pod needs gVisor or Kata (or a node-level wrapper like vNode) on top, not instead of, hostUsers: false.

The Cluster-API Fleet Checklist

Flipping hostUsers: false on for a Cluster-API-provisioned fleet — where node images come from a image-builder template, not a cloud provider's managed node pool with pinned defaults — means checking six things against your actual node image before you rely on it, not after:

1. Kernel 6.3+ on every node image. Ubuntu 22.04 LTS's base kernel is 5.15 — it fails the tmpfs idmap requirement outright and needs the HWE kernel track (6.5+) enabled explicitly. Ubuntu 24.04 LTS ships 6.8 by default and passes. Debian 12 (bookworm) ships 6.1 — also short of the floor. Check uname -r against your image-builder template before assuming; a CAPI fleet mixing node-image versions across MachineDeployments can have some nodes silently ineligible.

2. Container runtime versions. containerd 2.0+ or CRI-O 1.25+, backed by runc 1.2+ or crun 1.9+ (1.13+ recommended). containerd 1.x is unsupported as of v1.36 — if any MachineDeployment still bootstraps an older containerd, the kubelet upgrade itself fails, independent of User Namespaces. cri-dockerd has no support path at all.

3. Filesystem support — and it's not just about CSI drivers. ext4, btrfs, xfs, tmpfs, and overlayfs all support idmap mounts on a sufficiently new kernel. NFS-backed PersistentVolumes are the standout gap: idmap support is inconsistent across NFS server implementations and needs per-driver testing, not an assumption. If your CAPI fleet leans on an NFS-backed CSI driver for shared storage, pilot it explicitly rather than flipping the default fleet-wide.

4. Image compatibility — the part most checklists skip. Two distinct gotchas, both real: first, the per-pod remap range is 0–65535 by default; any file or process UID above that range gets mapped to the kernel's overflow UID (65534) and becomes unreadable — a real failure mode for images that bake in high UIDs. Second, overlayfs specifically needs kernel 5.19+ for idmap support as a snapshotter, separate from the 6.3 tmpfs floor above — without it, containerd will refuse the fast path and fall back to the expensive recursive chown on every container start, silently eating the exact startup-latency win idmap mounts were built to deliver. Test your actual base images, not just a hello-world pod.

5. hostUsers is immutable per pod. There's no live toggle — changing the posture for a running workload means a new pod, which on a CAPI fleet means a MachineDeployment rolling replace, not an in-place flag flip. Budget for it as a rollout, not a config push.

6. CAP_SYS_MODULE stops working. Kernel-module loaders, some GPU driver init paths, and eBPF sideloaders that expect real CAP_SYS_MODULE will break under a remapped namespace — plan a separate, explicitly privileged nodepool or DaemonSet exception for host-introspection tools like node-exporter that need genuine host visibility.

Rolling It Out

The lowest-risk starting point on an existing fleet is a workload that only needs a namespaced capability for its own network namespace — a CNI sidecar, a service mesh proxy, anything that currently runs privileged purely for CAP_NET_ADMIN on itself. That's a one-line spec change with a large blast-radius reduction and none of the storage or GPU-driver edge cases above, which makes it the right first candidate before touching anything stateful.

Bex.co provisions machine fleets via Cluster API and runs every deployed app as its own pod on infrastructure you own — which makes hostUsers: false a natural default floor to build toward on that fleet, the same way any CAPI operator would. It's worth being precise about what that is and isn't: it's a cheap identity-remapping floor, not a substitute for gVisor or Kata on genuinely untrusted, agent-generated code. Star the repo on GitHub if you're building on owned Kubernetes hardware and want the deploy-from-git layer without giving up that control.

User Namespaces GA is a real, overdue floor-raise — four concrete CVE classes neutralized, at close to zero runtime cost, with no image changes required. It is not, and was never designed to be, a kernel-isolation boundary. For the growing share of cluster capacity that's now AI agents executing code nobody reviewed line-by-line, that distinction is the whole point: turn hostUsers: false on everywhere it's compatible, and keep gVisor or Kata in the stack for the pods where "cheap" isn't the property you actually need.


Sources:

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex