By the end of this you will have a worker node provisioned for user namespaces and a Pod whose in-container root (UID 0) maps to a powerless UID on the host, verified by reading /proc/self/uid_map instead of trusting the Pod spec. Kubernetes v1.36 shipped this as GA on 22 April 2026, and the field that turns it on, hostUsers: false, is the easy part. The node prep underneath it is where people lose an afternoon.

The reason this is worth your time over asking a model to "set up user namespaces": the working knowledge here is too new and too underdocumented for that to help. The kernel floor is 6.3, not the 5.12 everyone quotes. The failure mode when you get a dependency wrong is usually silence, not an error. And the same v1.36 upgrade that makes the feature free also deletes containerd 1.x support out from under you.

Prerequisites

  • Kubernetes v1.36+. UserNamespacesSupport is GA and on by default, so no feature gate. On v1.33 to v1.35 the feature works but you may still need the gate enabled.
  • Linux kernel 6.3 or newer on every node. idmap mounts landed in 5.12, but tmpfs idmap support (which kubelet needs for emptyDir and projected volumes) only merged in 6.3. This is the single most-missed requirement.
  • containerd 2.0+ or CRI-O 1.25+, backed by runc 1.2+ or crun 1.9+ (1.13+ recommended).
  • A node filesystem under /var/lib/kubelet/pods/ that supports idmap mounts: btrfs, ext4, xfs, fat, tmpfs, or overlayfs.
  • Root SSH access to a worker node, plus kubectl against the cluster.

Step-by-step

1. Confirm the kernel and runtime actually qualify

uname -r                          # need >= 6.3
containerd --version              # need >= 2.0.0
runc --version                    # need >= 1.2.0  (or: crun --version >= 1.9)
stat -f -c %T /var/lib/kubelet    # expect ext4 / xfs / btrfs / overlayfs

This checks the four things that have to be true before any YAML matters. If uname -r shows something like 5.15, stop here, because no Pod spec will fix a kernel that lacks tmpfs idmap. Cloud vendor LTS images frequently ship 5.x kernels, which is exactly why this feature fails so often in the field.

2. Create the kubelet system user and subordinate ID ranges

kubelet maps each Pod into a slice of host UIDs and GIDs that it reads from /etc/subuid and /etc/subgid, keyed to a user literally named kubelet.

# Create the user if it doesn't exist (no login shell needed)
useradd -r -s /usr/sbin/nologin kubelet 2>/dev/null || true

# Allocate subordinate IDs: 65536 * maxPods. For the default 110 pods:
#   65536 * 110 = 7208960
echo 'kubelet:65536:7208960' >> /etc/subuid
echo 'kubelet:65536:7208960' >> /etc/subgid

The arithmetic has rules the tooling enforces: the first ID must be a multiple of 65536 and at least 65536 (never the 0 to 65535 range), the count must be a multiple of 65536, and the UID and GID ranges must match. Use exactly one line per user. Multiple ranges are not supported, and a second line does not extend the first.

3. Install getsubids so kubelet can read those ranges

# Debian/Ubuntu
apt-get install -y uidmap
# Fedora/RHEL
dnf install -y shadow-utils

# Verify kubelet's allocation is visible:
getsubids kubelet
getsubids -g kubelet

kubelet shells out to getsubids (from shadow-utils) at startup to discover the ranges from step 2. If that binary is missing from PATH, kubelet falls back to running Pods without namespaces and emits no hard error. This is the quietest failure in the whole feature, so install the package before you rely on it.

4. (Optional) Raise the per-Pod ID count

# /var/lib/kubelet/config.yaml  (KubeletConfiguration)
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
userNamespaces:
  idsPerPod: 65536        # default; must be a multiple of 65536

Leave this at the default unless a workload genuinely needs a UID range wider than 65536. If you raise it, redo the /etc/subuid math (idsPerPod × maxPods) or you will exhaust the range partway through your Pod density. Changes apply only to containers created after a kubelet restart:

systemctl restart kubelet

5. Deploy a Pod with user namespaces enabled

apiVersion: v1
kind: Pod
metadata:
  name: userns-demo
spec:
  hostUsers: false          # <-- the whole feature
  containers:
  - name: app
    image: busybox:1.36
    command: ["sh", "-c", "sleep 3600"]
    securityContext:
      runAsUser: 0          # root *inside* the container, on purpose

hostUsers: false tells kubelet to place the Pod in its own user namespace. The container believes it runs as UID 0; the host kernel sees an unprivileged UID from the kubelet subuid range. Capabilities like CAP_NET_ADMIN and CAP_SYS_ADMIN become namespace-scoped, so they are real inside the container and meaningless against the host.

kubectl apply -f userns-demo.yaml

Verify it works

Do not trust the field. Prove the mapping by reading the in-container view, then the host view, and confirming they differ.

# Inside the container: claims to be root (UID 0)
kubectl exec userns-demo -- id
# uid=0(root) gid=0(root) ...

# Inside the container: inspect the namespace's UID map
kubectl exec userns-demo -- cat /proc/self/uid_map
#          0     <hostUID>      65536

A uid_map line of 0 <some-large-number> 65536 (for example 0 65536 65536) confirms isolation: container UID 0 begins at host UID 65536, spanning 65536 IDs. The dead giveaway that it is not working is 0 0 4294967295, the identity map, which means the Pod is sharing the host user namespace despite the field being set.

Now confirm from the node itself:

# On the worker node, find the container's real host UID
ps -o pid,uid,cmd -C sleep
# UID column shows ~65536, NOT 0

Seeing a high UID like 65536 for a process that thinks it is root is the win condition. In practice, the step that bites people is skipping this node-side check: the Pod starts cleanly, kubectl exec ... id prints uid=0, and everything looks done, but if getsubids was missing in step 3 the Pod is running with the host identity map and zero added isolation. The /proc/self/uid_map line is the only thing that tells you the truth, so I treat a green Pod with no uid_map check as unverified.

Common pitfalls

  • Kernel 5.12 is not enough. idmap mounts exist there, but tmpfs idmap support arrived in 6.3, and kubelet needs it for emptyDir and projected volumes. On an older kernel, Pods with those volumes fail to mount.
  • Silent fallback when getsubids is missing. No uidmap or shadow-utils package means kubelet runs the Pod without namespaces and logs only at high verbosity. Always verify with /proc/self/uid_map, never with the spec.
  • hostPath and pre-existing volumes defeat it. Files owned by UIDs outside the mapped range appear as the overflow ID 65534 (nobody) and are not writable. User namespaces compose cleanly with emptyDir, projected, and idmap-capable CSI volumes, but not with arbitrary host paths.
  • containerd 1.x will break your upgrade. v1.36 dropped support for it (1.35 was the last release to carry it). If nodes still run 1.x, the kubelet upgrade fails regardless of user namespaces, so move to 2.0+ first.
  • It is mitigation, not a force field. hostUsers: false greatly reduces the blast radius of escapes like CVE-2024-21626 (the runc working-directory and leaked-fd breakout) and CVE-2022-0492 (the cgroups release_agent flaw), but the kernel is still shared. An attacker can still do whatever a file's "other" permission bits allow, and a kernel-level exploit is unaffected. Treat this as defense-in-depth alongside seccomp, Pod Security Standards, and runtime isolation, not a replacement for them.
  • Per-Pod range changes need a kubelet restart and only affect new containers. Running Pods keep their old mapping until recreated.

Wrap-up

You now have a node carrying kubelet subuid and subgid ranges, a runtime and kernel that actually support idmap mounts, and a Pod whose in-container root is a powerless host UID, confirmed through /proc/self/uid_map rather than faith. The highest-leverage next move is to make this the default instead of a per-Pod opt-in: bake the /etc/subuid provisioning and the kernel and runtime version floors into your node image, then write a Kyverno or Validating Admission Policy rule that mutates hostUsers: false onto every workload that does not need host UID identity. That turns a GA checkbox into a baseline the whole cluster inherits.

Sources

  • https://kubernetes.io/blog/2026/04/23/kubernetes-v1-36-userns-ga/
  • https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/
  • https://blog.zwindler.fr/en/2026/04/28/kubernetes-usernamespaces-the-overhyped-ga-feature/