Kubernetes Is Learning to Run Without Host Root — Here’s What Changes

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.


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 gitRepo volumes.
  • 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 --user creates 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_namespaces is 0, 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:

  1. The feature gate is Beta and enabled by default. The feature gate table lists KubeletInUserNamespace as true / Beta / 1.37.
  2. Enabling the gate does not make a node rootless. Existing rootful clusters behave exactly as before after the upgrade.
  3. Nodes now report whether they are rootless through a new runningInUserNamespace field in the node’s system info.
  4. 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, and kernel.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:

SettingWhat it changesNode components still host root?
runAsNonRoot: trueThe app inside the container must not run as UID 0Yes
hostUsers: false (Pod user namespaces, GA in v1.36)The Pod gets its own user namespace; container root maps to an unprivileged host UIDYes
KubeletInUserNamespace (rootless node)kubelet, runtime, CNI, and kube-proxy themselves run inside a user namespaceNo

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/subuid and /etc/subgid
  • the KubeletInUserNamespace feature 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=docker against 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 nfs and iscsi do not work. Local volumes like local, hostPath, emptyDir, configMap, secret, and downwardAPI are 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: false Pods.
  • 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

AreaRootful nodeRootless node
kubelet, runtime, CNI, kube-proxyUID 0 on the hostUID 0 inside a user namespace, non-root on the host
Impact of a runtime breakoutHost rootOne unprivileged user account
Attacker can tamper with kernel or boot loaderYesNo
Kernel vulnerabilitiesStill relevantStill relevant
cgroup versionv1 or v2v2 only
Network volumes (NFS, iSCSI)WorkMostly don’t work
CNIBroad supportMust be tested; Flannel VXLAN known to work
NodePort / kubelet portExposed directlyNeeds a port forwarder
hostPID / hostNetworkPhysical hostNode’s namespace
AppArmor / SELinuxNormalOften reduced
Feature maturityStableBeta (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:

  1. Try it on a laptop with kind and rootless Docker.
  2. Inventory privileged workloads and anything using host namespaces or network storage.
  3. Add one rootless node to a non-production cluster, labeled and tainted.
  4. Run your real DaemonSets: CNI, CSI, monitoring, logging, security agents.
  5. Run representative applications, then test storage, DNS, and cross-node traffic.
  6. 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.


References

Avatar photo
David Cao

David is a Cloud & DevOps Enthusiast. He has years of experience as a Linux engineer. He had working experience in AMD, EMC. He likes Linux, Python, bash, and more. He is a technical blogger and a Software Engineer. He enjoys sharing his learning and contributing to open-source.

Articles: 725

Leave a Reply

Your email address will not be published. Required fields are marked *