Every CKS candidate learns the mantra “don’t run as root.” It is good advice, and it is also incomplete. A surprising number of real workloads must run as UID 0 inside the container — package managers, legacy daemons, build tools, anything that calls chown on startup — and a runAsNonRoot: true policy simply breaks them. Until recently the only answer was a sandbox runtime or a rewrite.
Linux user namespaces are the third option. With one line in the pod spec, hostUsers: false, a process that is root inside the container becomes an unprivileged, high-numbered user on the node. A container breakout lands the attacker in an account that owns nothing, and most capabilities the container holds stop meaning anything outside the pod. Kubernetes shipped the feature on by default in v1.33 and graduated it to GA in v1.36, which is why it now belongs in any serious CKS preparation.
This article is a focused deep dive into user namespaces as an isolation technique for the Minimize Microservice Vulnerabilities domain (20% of the CKS). It assumes you already know what a securityContext is — the CKAD securityContext guide covers the basics — and that you have met Pod Security Standards and sandboxed runtimes, both of which appear here only where user namespaces interact with them.
The Problem User Namespaces Solve
A container is a process on the node. Without a user namespace, the UID the process runs as inside the container is the same UID the kernel sees on the host. Root in the container is root on the node, and a capability granted to the container (say CAP_SYS_ADMIN or CAP_NET_ADMIN) is a capability that works against the host kernel.
Defence in depth has always tried to narrow that blast radius: drop capabilities, set runAsNonRoot, add a seccomp profile, add AppArmor. Those controls all share a weakness — they constrain what the process may do, but if any of them fails, the process is still privileged.
A user namespace changes the identity itself:
- The process sees itself as UID 0 and holds a full capability set — inside the namespace.
- The kernel maps that UID 0 to some unprivileged host UID, for example 833617920.
- Capabilities held in the namespace are valid only for objects owned by the namespace.
CAP_SYS_MODULEbecomes completely void (no kernel module loading), andCAP_SYS_ADMINis confined to the pod’s own namespace.
The upstream enhancement proposal for this feature lists several HIGH and CRITICAL CVEs that were not exploitable with user namespaces active, including the well-known runc /proc/self/exe overwrite (CVE-2019-5736) and the subPath symlink host-file read (CVE-2021-25741). The exact list matters less than the pattern: the exploit worked because container root was host root, and user namespaces break that assumption.
Exam framing. Pod Security Standards decide whether a pod may ask for privilege. Sandboxes (gVisor, Kata) replace the kernel the pod talks to. User namespaces keep the shared kernel but change who the pod is to that kernel. All three are “isolation techniques” and all three can be layered.
Enabling a User Namespace: hostUsers: false
The pod-level field is spec.hostUsers. Its default is true, meaning the pod shares the host’s user namespace exactly as pods always have. Set it to false to give the pod its own:
apiVersion: v1
kind: Pod
metadata:
name: userns
spec:
hostUsers: false
containers:
- name: shell
image: debian
command: ["sleep", "infinity"]
That is the entire opt-in. There is no annotation, no RuntimeClass, and no change to the container image. Note the field is on the pod, not the container — all containers in the pod, including init and ephemeral containers, share the one user namespace.
What the kubelet does with it
When the pod starts, the kubelet picks a block of host UIDs and GIDs for it and tells the container runtime to create the user namespace with that mapping. Two properties are guaranteed:
- No two pods on the same node share a mapping. Pod A’s root maps to a different host UID than pod B’s root, so even two compromised pods cannot read each other’s files on the node.
- The mapping starts above 65535. The kubelet assumes host users and system files live in 0–65535 and deliberately assigns pods above that, so a pod UID never collides with a real host account or file owner. That non-overlap is precisely what neutralises a bug like CVE-2021-25741: even if the pod can trick its way to a host path, it does not own anything there.
By default each pod gets a block of 65536 IDs, so inside the container UIDs 0–65535 are valid and anything above maps to nothing.
Verifying the Mapping (Do This in the Exam)
A user namespace is invisible unless you look, and the examiners can only grade what is actually in effect. Three checks prove it.
1. Compare the namespace inode inside and outside the pod.
kubectl exec -ti userns -- readlink /proc/self/ns/user
# user:[4026532687]
# on the node
readlink /proc/self/ns/user
# user:[4026531837] <- different inode = different namespace
2. Read the UID map from inside the container.
kubectl exec -ti userns -- cat /proc/self/uid_map
# 0 833617920 65536
The three columns are container UID start, host UID start, and length. Container root (0) is host UID 833617920, and the block is 65536 wide. A pod running in the host user namespace shows 0 0 4294967295 instead — the whole UID space, identity-mapped.
3. Look at the process from the node.
# inside: whoami -> root
# on the node:
ps -eo user,pid,cmd | grep "sleep infinity"
# 833617920 41822 sleep infinity
ps on the host shows the mapped UID, not root. If a task says “confirm the container’s root user is unprivileged on the host,” this is the evidence.
Node Prerequisites: Kernel, Filesystem, Runtime
This is the part candidates skip and then lose time on when a pod sits in ContainerCreating. User namespaces are Linux-only and lean on idmap mounts, a kernel feature that lets one mount present file ownership shifted into a namespace’s ID range. The requirements cascade:
| Layer | Requirement | Why |
|---|---|---|
| Kernel | Linux 6.3 or newer in practice | tmpfs gained idmap mount support in 6.3, and Kubernetes mounts Secrets, projected service-account tokens and emptyDir.medium: Memory on tmpfs |
| Filesystems | Must support idmap mounts: btrfs, ext4, xfs, fat, tmpfs, overlayfs | /var/lib/kubelet/pods and every volume the pod mounts are idmapped |
| OCI runtime | runc ≥ 1.2 or crun ≥ 1.9 (1.13+ recommended) | Older runtimes cannot set up the namespace and mappings |
| CRI runtime | containerd ≥ 2.0 or CRI-O ≥ 1.25 | The CRI must pass the user-namespace request through |
| Kubernetes | Feature gate UserNamespacesSupport: beta off by default 1.30–1.32, on by default from 1.33, GA (locked on) in 1.36 | On a modern cluster nothing needs enabling |
NFS volumes cannot be used in a user-namespaced pod because the Linux NFS client does not support idmap mounts. If a filesystem lacks support, the pod fails with an event like:
Warning Failed kubelet Error: failed to create containerd task: ... failed to fulfil mount request:
failed to set MOUNT_ATTR_IDMAP on /var/lib/kubelet/pods/.../volumes/... invalid argument
(maybe the filesystem used doesn't support idmap mounts on this kernel?)
When you see MOUNT_ATTR_IDMAP ... invalid argument in kubectl describe pod, the diagnosis is immediate: a volume on a filesystem (or a kernel) without idmap support. Fix the storage or drop hostUsers: false for that pod — there is no third option.
Quick checks on a node:
uname -r # 6.3+?
containerd --version # 2.0+?
runc --version # 1.2+?
findmnt -no FSTYPE /var/lib/kubelet # ext4 / xfs / btrfs?
Sizing the ID Range: /etc/subuid, getsubids and idsPerPod
Out of the box the kubelet allocates its own ranges above 65535 and you do not configure anything. Two knobs exist for clusters that need control.
Custom host range via subordinate IDs
The kubelet can honour a subordinate-ID range from /etc/subuid and /etc/subgid, but only under strict conditions:
- A system user literally named
kubeletmust exist (the name is fixed; it is only used to look up the range, not to run the kubelet). - The
getsubidsbinary from shadow-utils must be on the kubelet’sPATH. /etc/subuidand/etc/subgidmust each contain exactly one line forkubelet, and both ranges must be identical.
# /etc/subuid and /etc/subgid
# name:firstID:count
kubelet:65536:7208960
The constraints the kubelet enforces on that line are exam-grade details:
| Rule | Example | Reason |
|---|---|---|
firstID must be a multiple of 65536 and ≥ 65536 | 65536 is the minimum | Pods may never be mapped into 0–65535, where host accounts live |
count must be a multiple of 65536 | 7208960 = 110 × 65536 | Each pod consumes a whole block |
count ≥ 65536 × maxPods | 110 pods × 65536 | Otherwise the kubelet runs out of ranges and pods fail to start |
| UID and GID ranges identical | same line in both files | Simplifies the mapping; other users’ ranges need not align |
| No overlap with any other user’s range | — | Two mappings to the same host IDs would defeat isolation |
Reconfiguring a live node: the kubelet refuses to start if an existing user-namespaced pod uses IDs outside the new range. Drain the node first (kubectl drain — see the node maintenance guide), change the files, restart the kubelet, then uncordon. Remember that DaemonSet pods tolerate the unschedulable taint and will not be evicted by drain.
IDs per pod
From v1.33 the per-pod block size is a KubeletConfiguration field:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
userNamespaces:
idsPerPod: 1048576 # must be a multiple of 65536; default 65536
Raise it only when an image genuinely uses UIDs above 65535. The value applies to containers created after the kubelet restarts; running containers keep their old mapping. (How to find and edit the kubelet’s config file is covered in the static pods and kubelet manifests guide.)
runAsUser, fsGroup and Volumes: What Does Not Change
A common misconception is that user namespaces alter file ownership on volumes. They do not. runAsUser, runAsGroup, fsGroup and supplementalGroups always refer to IDs inside the container, and the idmap mount translates them transparently. A file written by container UID 1000 appears as UID 1000 on the volume, exactly as it would without a user namespace. Consequences:
- You can toggle
hostUserson an existing workload without re-chowning its PersistentVolume. - A user-namespaced pod and a normal pod can share a volume as long as they agree on the in-container IDs.
- This also applies to
hostPath— if the pod is allowed to mount one — so user namespaces do not makehostPathsafe; they only stop the pod’s root from owning host files that it did not already own.
Files owned by IDs outside the pod’s 0–65535 range show up as the kernel’s overflow ID (usually 65534, nobody), and they cannot be modified even by running as 65534. If an image bakes in a UID of, say, 100000, you will hit this and need a larger idsPerPod.
Interaction with Pod Security Admission
Here is the detail most likely to appear in a tricky question. Because container root is not host root once hostUsers: false is set, Kubernetes deliberately relaxes Pod Security Standards for user-namespaced pods:
| Namespace profile | Fields no longer checked when hostUsers: false |
|---|---|
| Baseline and Restricted | runAsNonRoot and runAsUser on the pod and on every container, init container and ephemeral container |
| Baseline only | procMount on every container (Unmasked becomes allowed) |
| Restricted | procMount is still limited to unset or Default |
So a pod with hostUsers: false running as root passes the Restricted profile’s “run as non-root” requirement without setting runAsNonRoot: true. That is by design: the enforced property was always “not privileged on the host,” and a user namespace satisfies it more fundamentally than a UID check. If a scenario says “the Restricted profile rejects our image because it must start as root, and we cannot rebuild it,” the clean answer is a user namespace, not an exemption label. The profiles themselves are explained in the Pod Security Standards guide and enforced step by step in the Minimize Microservice Vulnerabilities walkthrough.
procMount: Masked /proc and When Unmasked Is Allowed
Runtimes following the OCI specification start containers with parts of /proc masked (empty or inaccessible) and other parts read-only, so a process cannot read kernel memory via /proc/kcore, inspect keyrings via /proc/keys, or write to /proc/sys. The masked set includes paths such as /proc/asound, /proc/acpi, /proc/kcore, /proc/keys, /proc/latency_stats, /proc/timer_list, /proc/scsi and /sys/firmware; the read-only set includes /proc/bus, /proc/fs, /proc/irq, /proc/sys and /proc/sysrq-trigger.
The container field securityContext.procMount has two values:
Default(or unset) — the masked/read-only behaviour above. Required by the Baseline and Restricted profiles.Unmasked— a full, writable/proc(and unmasked/sys/firmware). The legitimate use case is running nested containers — a build system or a Podman/Buildah job inside a pod — which needs a real/procto set up its own child namespaces.
spec:
hostUsers: false # mandatory for Unmasked
containers:
- name: builder
image: quay.io/buildah/stable
securityContext:
procMount: Unmasked
The rule tying the two features together: procMount: Unmasked requires hostUsers: false. Kubernetes v1.12 to v1.29 did not enforce that, which is why the feature stayed alpha for years; from v1.30 the API rejects Unmasked without a user namespace, the ProcMountType gate became on by default in 1.33 alongside user namespaces, and both reached GA in 1.36. The security logic is sound — an unmasked /proc is only acceptable when the process reading it is not host root.
Limitations You Must Remember
A user-namespaced pod gives up several other host-sharing options, because sharing the host’s network, PID or IPC namespace while not sharing its user namespace creates inconsistent privilege:
hostUsers: falsecannot be combined withhostNetwork: true,hostPID: trueorhostIPC: true. The API server rejects the pod.- No container in the pod may use
volumeDevices(raw block volumes such as/dev/sda). - NFS volumes are unsupported (no idmap mount support in the NFS client).
- Windows nodes are out of scope entirely.
- The feature does not change what a
privileged: truecontainer can do to the kernel; it changes who the kernel thinks the process is. KeepprivilegedandCAP_SYS_ADMINout of workloads regardless.
For observability, the kubelet exports two counters: started_user_namespaced_pods_total and started_user_namespaced_pods_errors_total. A rising errors counter on one node usually means that node’s kernel or filesystem does not meet the prerequisites while others do — in a mixed cluster, use a node label and nodeSelector so user-namespaced pods land only on capable nodes.
A Worked Exam-Style Task
“The payments namespace enforces the Restricted Pod Security Standard. The image registry.local/ledger:3 must start as root to initialise its data directory and cannot be modified. Deploy it so that it runs, satisfies the profile, and a container escape would not grant root on the node. Prove the isolation.”
apiVersion: v1
kind: Pod
metadata:
name: ledger
namespace: payments
spec:
hostUsers: false
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: ledger
image: registry.local/ledger:3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /var/lib/ledger
volumes:
- name: data
persistentVolumeClaim:
claimName: ledger-data
Why it passes Restricted: hostUsers: false removes the runAsNonRoot/runAsUser checks; the remaining Restricted requirements (seccomp RuntimeDefault, allowPrivilegeEscalation: false, drop ALL capabilities, no host namespaces) are met explicitly. Apply it and prove the isolation:
kubectl -n payments apply -f ledger.yaml
kubectl -n payments get pod ledger # Running
kubectl -n payments exec ledger -- id -u # 0 (root inside)
kubectl -n payments exec ledger -- cat /proc/self/uid_map
# 0 1048576 65536 (root maps to a high host UID)
If it instead sits in ContainerCreating, run kubectl describe pod and look for MOUNT_ATTR_IDMAP — the PVC’s filesystem does not support idmap mounts, and the task cannot be completed on that storage class without changing it.
Where User Namespaces Sit Among CKS Isolation Controls
| Control | Isolates | Changes the kernel? | Typical cost | Exam cue |
|---|---|---|---|---|
| Namespaces + RBAC + NetworkPolicy | API scope, identity, traffic | No | None | ”tenant separation”, “least privilege” |
| Pod Security Standards | What a pod may request | No | None | ”reject privileged pods” |
| seccomp / AppArmor | Which syscalls / files a process may touch | No | Low | ”restrict syscalls”, “profile” |
| User namespaces | Who the process is on the host | No (shared kernel, mapped identity) | Very low | ”must run as root in the container”, “container escape should not be root on the node” |
| RuntimeClass sandbox (gVisor, Kata) | The kernel attack surface itself | Yes (user-space kernel or micro-VM) | Higher (overhead, RuntimeClass setup) | “untrusted code”, “kernel vulnerability” |
They stack. A hardened tenant workload today is hostUsers: false plus Restricted PSS plus a seccomp profile; a workload executing untrusted customer code adds a sandbox on top. The sandboxing guide covers the RuntimeClass path, the system hardening guide covers kernel profiles, and the isolation and multi-tenancy overview places all of them in the broader threat model. Static scanners such as Kubesec and KubeLinter (see the static analysis guide) will still flag a root container even when hostUsers: false is set — treat that finding as “confirm the user namespace is in effect,” not as a false positive to silence blindly.
Common CKS Traps
- Setting
hostUsers: falseon the container. It is a pod-level field. The API server rejects it undercontainers[].securityContext. - Pairing it with
hostNetwork: true. Rejected at admission — a user-namespaced pod cannot share any other host namespace. - Expecting
runAsNonRoot: trueto be required under Restricted. It is not checked for user-namespaced pods. Conversely, adding it anyway is harmless. - Blaming the image for
ContainerCreating. Check events forMOUNT_ATTR_IDMAP; it is the storage or kernel, not the image. - Assuming volume files get re-owned. They do not; in-container IDs are preserved on volumes.
- Using
procMount: UnmaskedwithouthostUsers: false. Rejected since v1.30. - Treating the feature as a sandbox. The kernel is still shared; a kernel exploit still works. User namespaces reduce the impact of a breakout, not the probability of a kernel bug.
- Writing a multi-line
/etc/subuidforkubelet. The kubelet accepts exactly one line, starting at a multiple of 65536 that is ≥ 65536.
Frequently Asked Questions
What does hostUsers: false do in a Kubernetes pod?
It gives the pod its own Linux user namespace. Root inside the containers maps to an unprivileged, high-numbered UID on the node, capabilities are confined to the pod, and the kubelet guarantees each pod on a node gets a distinct, non-overlapping ID range.
Which Kubernetes version enabled user namespaces by default?
The UserNamespacesSupport feature gate was beta but off in v1.30–1.32, on by default from v1.33, and graduated to GA (locked on) in v1.36. The node still needs a capable kernel, filesystem and container runtime.
Why is my user-namespaced pod stuck in ContainerCreating?
Almost always a filesystem without idmap mount support — NFS, or a kernel older than 6.3 for tmpfs. The pod events contain failed to set MOUNT_ATTR_IDMAP ... invalid argument. Move the volume to ext4/xfs/btrfs on a 6.3+ kernel or disable user namespaces for that pod.
Does a user namespace satisfy the Restricted Pod Security Standard for a root container?
Yes. For pods with hostUsers: false, Pod Security Admission does not evaluate runAsNonRoot or runAsUser in the Baseline and Restricted profiles, because container root is not host root. All other Restricted requirements still apply.
Can I use user namespaces with gVisor or Kata Containers?
The user-namespace documentation applies to runtimes that isolate with Linux namespaces (runc, crun under containerd or CRI-O). Kata runs the pod in its own VM kernel, so the host user namespace question largely does not arise; check your sandbox runtime’s documentation for support.
How many UIDs does each pod get?
65536 by default, configurable from v1.33 with userNamespaces.idsPerPod in the KubeletConfiguration (must be a multiple of 65536). Files owned by IDs beyond the range appear as the overflow user 65534 and cannot be modified.
What is procMount: Unmasked and when is it allowed?
It removes the default masking of sensitive /proc paths so nested container tooling can work. It is only valid when hostUsers: false is also set, is allowed by the Baseline profile for user-namespaced pods, and is never allowed by the Restricted profile.
Is this topic really on the CKS?
The Minimize Microservice Vulnerabilities domain (20%) asks candidates to understand and implement isolation techniques such as multi-tenancy and sandboxed containers. User namespaces are a core Kubernetes isolation technique that is now on by default, which makes hostUsers: false and its verification steps fair game.
Conclusion & Next Steps
User namespaces turn “don’t run as root” from a rule you enforce on images into a property the kernel guarantees for you: root inside, nobody outside. For the CKS, know the one-line opt-in, the three verification commands (readlink /proc/self/ns/user, cat /proc/self/uid_map, ps on the node), the prerequisites that make pods stick in ContainerCreating, the Pod Security Admission relaxations, and the incompatibilities with host namespaces and raw block or NFS volumes.
The fastest way to make this stick is to break it on purpose — deploy a user-namespaced pod, read its uid_map, pair it with hostNetwork: true and watch admission reject it, then try an NFS volume and decode the event. Sailor.sh’s Certified Kubernetes Security Specialist (CKS) Mock Exam Bundle gives you a browser-based, exam-style terminal with timed, hands-on security scenarios across all six domains, so you can practise isolation tasks like this one under realistic pressure. To build your own lab first, see the CKS practice environment guide, and use the CKS Study Plan and the CKS Exam Guide 2026 to place this topic within the full curriculum.