For most of its history, Kubernetes has assumed that the important parts of a Linux node run as root.
The kubelet mounts volumes and manages cgroups. The container runtime creates namespaces. CNI plugins build network interfaces and routes. kube-proxy writes iptables or nftables rules. All of that traditionally happens as UID 0 on the host.
That works, but it means a bug in any of those components can hand an attacker full control of the machine.
Kubernetes v1.37 pushes on that assumption. The KubeletInUserNamespace feature gate, often called rootless mode, has graduated to Beta.
According to the official announcement, this lets the node components (kubelet, CRI and OCI runtimes, CNI plugins, and kube-proxy) run as a non-root user on the host by placing them inside a Linux user namespace.
The components still believe they are root. They just aren’t root on the host.
That one distinction is what the rest of this article is about.
Table of Contents
Rootful vs Rootless Nodes
On a traditional node, the picture is simple:
Linux host
├── kubelet (UID 0 on the host)
├── containerd (UID 0 on the host)
├── runc (UID 0 on the host)
└── containers
On a rootless node, a regular user account owns everything, and a user namespace sits in between:
Linux host
└── user "k8s" (UID 1000)
└── user namespace (UID 1000 on the host appears as UID 0 inside)
├── kubelet
├── containerd / runc
├── CNI plugins
├── kube-proxy
└── containers
Inside the namespace, id reports uid=0(root). From the host’s point of view, every one of those processes belongs to UID 1000.
See also: Mastering the Linux Command Line — Your Complete Free Training Guide
Why Bother?
Because node components have a real history of container-breakout bugs. The Kubernetes announcement lists several, including:
- CVE-2022-0811 (“cr8escape”): CRI-O could be tricked into setting arbitrary sysctls such as
kernel.core_pattern, leading to code execution as root on the host. - CVE-2024-10220: the kubelet could be made to run arbitrary commands as root through
gitRepovolumes. - CVE-2025-31133: runc could be tricked into writing to host procfs files such as
/proc/sysrq-trigger.
On a rootful node, a bug like that gives the attacker the host. On a rootless node, the same bug gives them a single unprivileged user account.
The announcement also points out something that gets less attention: an attacker who lands in that account cannot hide by modifying the kernel, boot loader, or firmware. For incident response, that matters a lot.
The Linux Feature Underneath: User Namespaces
You don’t need Kubernetes to see how this works. Try it as a normal user:
unshare --user --map-root-user bash
Inside the new shell:
id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
Now look at how the kernel maps that “root”:
cat /proc/self/uid_map
0 1000 1
Read that line as: UID 0 inside the namespace is UID 1000 outside, for a range of 1 ID.
Try something that needs real root:
cat /etc/shadow
cat: /etc/shadow: Permission denied
Inside the namespace you’re “root”. To the host kernel you’re still UID 1000, so access to host files is still checked against UID 1000.
Leave with exit and you’re back in your normal shell.
A few notes before you rely on this test:
unshare --usercreates only a user namespace. Rootless Kubernetes also needs mount, network, PID, and cgroup setup on top of it.- On newer Ubuntu releases, AppArmor restricts unprivileged user namespaces. If you see
unshare: write failed /proc/self/uid_map: Operation not permitted, check:sysctl kernel.apparmor_restrict_unprivileged_userns - If
user.max_user_namespacesis0, user namespaces are disabled entirely:sysctl user.max_user_namespaces
What Actually Changed in v1.37
The feature isn’t new. It started as an experiment in 2018 and was merged as alpha in Kubernetes v1.22 (KEP-2033). In v1.37:
- The feature gate is Beta and enabled by default. The feature gate table lists
KubeletInUserNamespaceastrue / Beta / 1.37. - Enabling the gate does not make a node rootless. Existing rootful clusters behave exactly as before after the upgrade.
- Nodes now report whether they are rootless through a new
runningInUserNamespacefield in the node’s system info. - Kubernetes CI now runs node conformance tests on a rootless cluster, which is a large part of why the feature earned Beta.
Point 2 surprises people, so here is what the gate actually does. The announcement describes it as “quite boring”: it mainly tells the kubelet to ignore permission errors it hits when running in a user namespace, such as:
- setting sysctls like
vm.overcommit_memory,vm.panic_on_oom,kernel.panic, andkernel.panic_on_oops - opening
/dev/kmsg
It also lets kube-proxy ignore an error when setting RLIMIT_NOFILE.
The user namespace itself has to be created outside Kubernetes by rootless Docker, rootless Podman, rootless nerdctl, RootlessKit, k3s rootless mode, or something similar. Kubernetes doesn’t create it for you.
Rootless Nodes Are Not the Same as Non-Root Pods
These three settings are easy to mix up:
| Setting | What it changes | Node components still host root? |
|---|---|---|
runAsNonRoot: true | The app inside the container must not run as UID 0 | Yes |
hostUsers: false (Pod user namespaces, GA in v1.36) | The Pod gets its own user namespace; container root maps to an unprivileged host UID | Yes |
KubeletInUserNamespace (rootless node) | kubelet, runtime, CNI, and kube-proxy themselves run inside a user namespace | No |
A Pod with runAsNonRoot: true on a normal node looks like this:
Linux host
├── kubelet → root
├── containerd → root
└── Pod → UID 1001
The Pod is non-root. The node is not.
The two user namespace features don’t conflict, and they can be combined. That’s how Kubernetes-in-Kubernetes works without privileged: true: the inner cluster runs as a Pod with hostUsers: false, and its own kubelet runs with KubeletInUserNamespace.
How to Tell Whether a Node Is Rootless
v1.37 exposes this in the node’s status.nodeInfo:
kubectl get nodes \
-o custom-columns=NAME:.metadata.name,ROOTLESS:.status.nodeInfo.runningInUserNamespace
On a kind cluster running in rootless Docker, you would expect something like:
NAME ROOTLESS
kind-control-plane true
On a rootful node, the field is false or missing.
The Kubernetes project suggests using this value to set labels or taints, so workloads that genuinely need host root, such as some CNI plugin installers, don’t get scheduled onto rootless nodes:
kubectl label node kind-control-plane node.example.com/rootless=true
Requirements
The official task page lists these prerequisites:
- cgroup v2 (cgroup v1 is not supported)
- systemd with a user session and cgroup delegation
- sysctl settings appropriate for your distribution
- the unprivileged user listed in
/etc/subuidand/etc/subgid - the
KubeletInUserNamespacefeature gate (already on by default in v1.37)
Check cgroup v2
stat -fc %T /sys/fs/cgroup
cgroup2fs
If you see tmpfs, the host is using cgroup v1 or hybrid mode.
Check which controllers are delegated to your user:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
cpuset cpu io memory pids
If cpu, memory, or pids are missing, resource limits won’t work properly inside the rootless node.
Check subordinate IDs
grep "^$(whoami):" /etc/subuid /etc/subgid
/etc/subuid:alice:100000:65536
/etc/subgid:alice:100000:65536
The format is user:start:count. Here alice can map 65,536 extra IDs, from 100000 to 165535, into her namespaces. Container images often contain files owned by several UIDs, so the runtime needs that range. The single-UID mapping from the unshare test above isn’t enough.
If there’s no entry, add one:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 alice
The Easiest Way to Try It: kind
The Kubernetes project recommends kind with rootless Docker, Podman, or nerdctl. With Docker:
dockerd-rootless-setuptool.sh install
Make sure your Docker CLI is talking to the rootless daemon, not the system one:
docker info --format '{{.SecurityOptions}}'
[name=seccomp,profile=builtin name=rootless name=cgroupns]
name=rootless is the part you want. If it’s missing, check docker context ls or DOCKER_HOST.
Then create the cluster:
kind create cluster
kubectl get nodes -o wide
Depending on the host, you may need extra systemd, kernel module, or sysctl settings. The Docker and kind rootless docs cover them.
Other options:
- minikube:
minikube start --driver=dockeragainst rootless Docker - k3s: has its own experimental rootless mode that doesn’t need rootless Docker
- Usernetes: multi-node rootless clusters connected with Flannel VXLAN
What Breaks, and What Doesn’t
The official caveats are short and specific.
Storage
Most “non-local” volume drivers such as
nfsandiscsido not work. Local volumes likelocal,hostPath,emptyDir,configMap,secret, anddownwardAPIare known to work.
If you rely on NFS, iSCSI, or a network-attached CSI driver, test it before anything else:
kubectl get csidrivers
kubectl get pvc -A
Then create a real Pod that mounts a real PVC and writes to it. A PVC showing Bound doesn’t prove the mount works.
Networking
Some CNI plugins may not work. Flannel (VXLAN) is known to work.
There’s also a detail that catches people. The node’s network namespace isn’t the host’s. It needs its own non-loopback interface, usually provided by slirp4netns, VPNKit, or lxc-user-nic. Ports like the kubelet’s 10250/TCP and any NodePort services have to be forwarded to the host with something like RootlessKit or socat.
Test from inside a Pod:
kubectl run net-test --image=busybox --restart=Never -- sleep 3600
kubectl exec net-test -- ip addr
kubectl exec net-test -- ip route
kubectl exec net-test -- nslookup kubernetes.default
kubectl exec net-test -- wget -qO- -T 5 http://example.com
Also check Pod-to-Pod traffic across nodes, and any NodePort you expose.
kube-proxy
Running kube-proxy in a user namespace means telling it not to set conntrack sysctls it can’t change:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "iptables"
conntrack:
maxPerCore: 0
tcpEstablishedTimeout: 0s
tcpCloseWaitTimeout: 0s
“Host” access means something different
This point is easy to miss. On a rootless node, hostNetwork: true, hostPID: true, and hostPath refer to the node’s namespaces, not the physical machine. A monitoring DaemonSet with hostPID: true sees the processes inside the rootless node, not the real host.
That affects:
- node exporters and monitoring agents
- EDR and runtime security agents
- log collectors reading host paths
- anything that loads kernel modules or touches
/dev
Find the workloads to review:
grep -rnE "privileged: true|hostNetwork: true|hostPID: true|hostIPC: true|hostPath:" ./manifests
Or check what’s already running:
kubectl get pods -A -o json | jq -r '
.items[]
| select(.spec.hostNetwork or .spec.hostPID
or any(.spec.containers[]; .securityContext.privileged == true))
| "\(.metadata.namespace)/\(.metadata.name)"'
LSMs
The documented containerd configuration for rootless mode sets disable_apparmor = true. The docs also note that native overlayfs in a user namespace requires SELinux to be disabled, so fuse-overlayfs is the usual snapshotter. Rootless mode may cost you some of the LSM protection you had before. Weigh that trade-off honestly.
What Rootless Mode Does Not Protect Against
The announcement is explicit:
User namespaces are not effective for mitigating vulnerabilities in the kernel itself.
A kernel bug reachable from inside a user namespace is still a kernel bug. User namespaces actually expose more kernel code to unprivileged users, which is exactly why some distributions restrict them by default.
So rootless mode sits alongside other controls. It doesn’t replace them:
- seccomp to limit the syscalls containers can make
- Pod Security Admission to block privileged Pods
- RBAC to limit who can create workloads
- Pod user namespaces (
hostUsers: false) - kernel patching, which still matters most
Where This Is Actually Useful
The Kubernetes project lists these use cases:
- Production clusters: reduce the impact of container-breakout bugs.
- Shared machines (HPC): users can run Kubernetes without asking for root and without breaking each other’s setups.
- Laptops: a local cluster can’t mess up host iptables rules, such as the ones your VPN depends on.
- AI sandboxes: create a dedicated local user for an AI coding agent and a test cluster, so an agent misled by malicious content on the internet can’t break the host.
- Kubernetes-in-Kubernetes: run nested clusters as
hostUsers: falsePods. - Bootstrapping: run a temporary unprivileged cluster to bootstrap a real one, for example with Cluster API.
The laptop and AI sandbox cases are probably where most people will try this first. Wiping out your host network config with a kind cluster is a very real, very annoying failure, and rootless mode prevents it.
Rootful vs Rootless at a Glance
| Area | Rootful node | Rootless node |
|---|---|---|
| kubelet, runtime, CNI, kube-proxy | UID 0 on the host | UID 0 inside a user namespace, non-root on the host |
| Impact of a runtime breakout | Host root | One unprivileged user account |
| Attacker can tamper with kernel or boot loader | Yes | No |
| Kernel vulnerabilities | Still relevant | Still relevant |
| cgroup version | v1 or v2 | v2 only |
| Network volumes (NFS, iSCSI) | Work | Mostly don’t work |
| CNI | Broad support | Must be tested; Flannel VXLAN known to work |
| NodePort / kubelet port | Exposed directly | Needs a port forwarder |
hostPID / hostNetwork | Physical host | Node’s namespace |
| AppArmor / SELinux | Normal | Often reduced |
| Feature maturity | Stable | Beta (v1.37) |
Should You Use It in Production?
Beta means the feature is well tested and enabled by default, but details may still change before GA. The project says it plans to go GA “depending on feedback and adoption.”
A reasonable path:
- Try it on a laptop with kind and rootless Docker.
- Inventory privileged workloads and anything using host namespaces or network storage.
- Add one rootless node to a non-production cluster, labeled and tainted.
- Run your real DaemonSets: CNI, CSI, monitoring, logging, security agents.
- Run representative applications, then test storage, DNS, and cross-node traffic.
- Write down what broke, and decide from that.
Don’t convert a whole production cluster because the feature is new.
Quick Checklist
# Kernel and cgroup v2
uname -r
stat -fc %T /sys/fs/cgroup # expect: cgroup2fs
# User namespaces allowed?
sysctl user.max_user_namespaces # must be > 0
unshare --user --map-root-user id # expect: uid=0(root)
# Subordinate IDs
grep "^$(whoami):" /etc/subuid /etc/subgid
# Rootless Docker in use?
docker info --format '{{.SecurityOptions}}' # look for name=rootless
# Cluster version and node status
kubectl version
kubectl get nodes \
-o custom-columns=NAME:.metadata.name,ROOTLESS:.status.nodeInfo.runningInUserNamespace
Then test CNI, CSI, DNS, NodePort, monitoring and security agents, and any privileged or host-namespace workloads.
FAQ
Does upgrading to v1.37 make my nodes rootless?
No. The feature gate is on by default, but it doesn’t move the kubelet into a user namespace. Existing rootful nodes don’t change.
Is this the same as runAsNonRoot?
No. runAsNonRoot is about the process inside a container. Rootless mode is about the kubelet, runtime, CNI, and kube-proxy on the node.
Is this the same as hostUsers: false?
No. hostUsers: false gives Pods their own user namespace while the node components stay host root. The two features are separate, and they can be combined.
Does rootless mode stop container escapes?
It limits the damage from escapes that go through node components. It doesn’t protect against kernel vulnerabilities. Use seccomp and other hardening alongside it.
Can I try it on a laptop?
Yes. kind and minikube both support rootless Docker and rootless Podman. kind also supports rootless nerdctl.




