Run a workload in a user namespace
By default, UID 0 inside a container is UID 0 on the node, so a process that escapes the container while running as root is root on the host, able to reach other pods and host files. A user namespace breaks that link: it maps the container's user IDs onto a different, unprivileged range of host UIDs. The container still sees itself as root; on the node, that same process is an ordinary user with no privileges.
Take that trade whenever a container runs as root you cannot change, or parses untrusted input, and you want a compromised pod's blast radius kept small. It works on every node, costs almost nothing, and takes a single field.
Turn it on#
Set hostUsers: false on the pod spec. There is nothing to install.
spec:
hostUsers: false # remap container UIDs onto an unprivileged host range
containers:
- name: app
image: your/image
The kubelet hands the pod its own block of 65536 host UIDs and maps the container's 0-65535 onto it. Volume file ownership is translated for you with idmapped mounts, a kernel feature that rewrites ownership between the pod's range and the host's, so files created inside the pod still read correctly from either side. User namespace support is on by default in 1.36, with no feature gate to set.
What root sees versus what the host sees#
The point of the remap is that the two views disagree. Inside, nothing changed; on the node, the same process has been demoted:
| Inside the container | On the node |
|---|---|
| Runs as UID 0, root | Runs as an unprivileged UID the kubelet assigns |
| Creates files owned by root | Creates files owned by that unprivileged user |
Holds capabilities like cap_sys_admin | Those capabilities apply only inside the namespace, powerless against host resources |
| An escape starts as root | An escape lands as a user who owns nothing on the host |
Verify#
Inside, the process still sees root; the kernel's map shows what actually happened:
$ kubectl exec userns-app -- cat /proc/self/uid_map
0 348258304 65536
Read it as: container UID 0 maps to host UID 348258304, for a range of 65536 IDs. The kubelet picks a different range for each pod. A pod without hostUsers: false shows 0 0 4294967295, meaning container root is host root, straight through.
Three tiers, weakest to strongest#
A user namespace is the middle of three boundaries, each stronger than the last:
| Tier | Kernel | Container root on the host | Works on |
|---|---|---|---|
| Standard runtime (default) | shared with host | host root (UID 0) | every node |
+ user namespace (hostUsers: false) | shared with host | unprivileged host user | every node |
Secure runtime (runtimeClassName: secure) | own guest kernel | isolated in a VM | bare metal only |
When the kernel itself must not be shared, step up to Run in the secure runtime .
Where it does not work#
A user namespace cannot be combined with hostNetwork: true, hostPID: true, or hostIPC: true: sharing a host namespace and remapping users are mutually exclusive. It also does not fit a workload that must own host files as a real host UID, such as a node agent, which is why platform DaemonSets that need host access do not use it.
Warning
Do not set hostUsers: false cluster-wide. Platform components need host access and break when forced into a user namespace. Set it per workload, or default it only inside namespaces that hold your own applications.
If a workload needs even finer control over what its remapped root can do, pair it with a security context .
Run virtual machines with KubeVirt
Run full virtual machines next to your containers with KubeVirt on bare-metal nodes, and plan for node churn so a VM disk backed by local NVMe survives.
Run in the secure (Kata) runtime
Run a pod in its own lightweight VM with its own kernel, so a kernel-level escape stays trapped inside the sandbox instead of reaching the host.