Provision one worker node for user namespaces, deploy a Pod, then read /proc/self/uid_map inside it rather than trusting the Pod spec: in-container root (UID 0) should land on a powerless UID on the host. Kubernetes v1.36 shipped this as GA on 22 April 2026, announced in the v1.36 user namespaces post. Flipping the field that turns it on, hostUsers: false, takes one line. The node prep underneath it is where people lose an afternoon.
TL;DR: You need kernel 6.3 or newer on every node (not the 5.12 everyone quotes, because kubelet needs tmpfs idmap), containerd 2.0+ or CRI-O 1.25+, one kubelet:65536:<65536 × maxPods> line in /etc/subuid and /etc/subgid, and the getsubids binary on PATH. Then set hostUsers: false on the Pod and confirm with kubectl exec <pod> -- cat /proc/self/uid_map. A line reading 0 0 4294967295 means the feature silently did nothing.
Why not hand this to a model and skip the reading? The working knowledge is too new and too thinly documented for that to help. The kernel floor is 6.3. 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.
What do you need before you start?
The Kubernetes user namespaces concept page carries the full support matrix. Here is the version of it that matters on a real node:
- Kubernetes v1.36+.
UserNamespacesSupportis 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
emptyDirand projected volumes) only merged in 6.3. Most stalled rollouts I have seen trace back to this one line. - 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
kubectlagainst the cluster.
Does this node even 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.
Treat a version mismatch here the way you would treat kubelet refusing to start on a cgroup v1 host: a node-image problem to fix in the image, not something to paper over per cluster.
How do you give kubelet its ID ranges?
1. 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.
2. 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 the previous step. If that binary is missing from PATH, kubelet falls back to running Pods without namespaces and emits no hard error. Nothing in this feature fails more quietly, so install the package before you rely on it.
3. (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
How do you turn it on for a Pod?
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
How do you prove the mapping is real?
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
When the node reports a high UID like 65536 for a process that believes it is root, you are done. In practice people skip this node-side check: the Pod starts cleanly, kubectl exec ... id prints uid=0, and everything looks finished.
But if getsubids was missing, that same Pod is running with the host identity map and zero added isolation. Read /proc/self/uid_map. Nothing else tells you the truth, so I treat a green Pod without that check as unverified.
What goes wrong most often?
- Kernel 5.12 is not enough. idmap mounts exist there, but tmpfs idmap support arrived in 6.3, and kubelet needs it for
emptyDirandprojectedvolumes. On an older kernel, Pods with those volumes fail to mount. - Silent fallback when
getsubidsis missing. Nouidmaporshadow-utilspackage means kubelet runs the Pod without namespaces and logs only at high verbosity. Always verify with/proc/self/uid_map, never with the spec. hostPathand 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 withemptyDir, 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.
- Per-Pod range changes need a kubelet restart and only affect new containers. Running Pods keep their old mapping until recreated.
Is hostUsers false a real security boundary?
Partly, and the honest framing matters. 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), and it blunts the same class of bug behind the Fission container escape. The kernel is still shared, though.
An attacker can still do whatever a file's "other" permission bits allow, and a kernel-level exploit is unaffected. Zwindler's read of the GA release lands on the same caution. Run it as defense-in-depth alongside seccomp, Pod Security Standards, and a runtime sensor such as Falco, Tetragon, or Tracee, not as a replacement for any of them.
What is the next move after one node works?
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.
Make it 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, the same way bound ServiceAccount tokens turn credential expiry into a property of the platform. If you would rather have the node image, the policy and the verification rolled out across a fleet without spending the afternoon yourself, say hello.
FAQ
Do I need a feature gate for user namespaces in Kubernetes 1.36? No. UserNamespacesSupport is GA and enabled by default, so there is nothing to switch on at the API server or kubelet. On v1.33 through v1.35 you may still have to enable the gate explicitly.
Why does hostUsers: false do nothing even though the Pod starts? Almost always because getsubids is not on PATH. kubelet reads /etc/subuid through that binary, and when it is missing it runs the Pod with the host identity map and logs nothing at normal verbosity.
Is Linux kernel 5.12 enough for Kubernetes user namespaces? No, despite 5.12 being the version most guides quote. idmap mounts landed there, but tmpfs idmap merged in 6.3, and kubelet needs it for emptyDir and projected volumes.
How many subordinate UIDs should kubelet get in /etc/subuid? Multiply idsPerPod by maxPods: 65536 × 110 gives 7208960, written as one line, kubelet:65536:7208960. The subuid and subgid ranges must match.
Does hostUsers: false work with hostPath volumes? Not usefully. Files owned by UIDs outside the mapped range show up as the overflow ID 65534 and are not writable, so plan on keeping those workloads out of the namespaced set.

Comments
Be the first to comment.