<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rbac on Zwindler's Reflection</title><link>https://blog.zwindler.fr/en/tags/rbac/</link><description>Recent content in Rbac on Zwindler's Reflection</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>Licensed under CC BY-SA 4.0</copyright><lastBuildDate>Tue, 19 May 2026 12:00:00 +0200</lastBuildDate><atom:link href="https://blog.zwindler.fr/en/tags/rbac/index.xml" rel="self" type="application/rss+xml"/><item><title>nodes/proxy GET: One Kubernetes permission too many</title><link>https://blog.zwindler.fr/en/2026/05/19/nodes/proxy-get-one-kubernetes-permission-too-many/</link><pubDate>Tue, 19 May 2026 12:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/05/19/nodes/proxy-get-one-kubernetes-permission-too-many/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/05/node_proxy_RCE.webp" alt="Featured image of post nodes/proxy GET: One Kubernetes permission too many" /&gt;&lt;h2 id="tldr"&gt;TL;DR
&lt;/h2&gt;&lt;p&gt;This is a bit old now, but I still wanted to share a quick write-up on the topic.&lt;/p&gt;
&lt;p&gt;Back in January, a cybersecurity researcher reported a Kubernetes flaw that generated quite a buzz. It had been a while since we had a Kube vulnerability that got people talking this much, at least from Denis&amp;rsquo;s memory (yes, I talk about myself in the third person).&lt;/p&gt;
&lt;p&gt;Honestly, it&amp;rsquo;s pretty wild: the &lt;code&gt;nodes/proxy GET&lt;/code&gt; RBAC permission allows any &lt;strong&gt;ServiceAccount&lt;/strong&gt; to execute code inside &lt;strong&gt;any Pod in the cluster&lt;/strong&gt;, without leaving a single trace in the audit logs. That&amp;rsquo;s unfortunate, especially when you have ServiceAccounts named &lt;code&gt;rook-ceph-system&lt;/code&gt; that also happen to have read access to all Secrets in the cluster.&lt;/p&gt;
&lt;p&gt;This article details the issue, how to check if you are vulnerable, the fixes to apply, and the preventive measures you can put in place if you can&amp;rsquo;t patch right away.&lt;/p&gt;
&lt;h2 id="the-problem-websocket--kubelet--un-audited-exec"&gt;The problem: WebSocket + Kubelet = un-audited exec
&lt;/h2&gt;&lt;p&gt;The vulnerability was documented by Graham Helton in &lt;a class="link" href="https://grahamhelton.com/blog/nodes-proxy-rce" target="_blank" rel="noopener"
&gt;this article&lt;/a&gt;. Here is how it works.&lt;/p&gt;
&lt;p&gt;The Kubernetes API exposes a &lt;code&gt;nodes/proxy&lt;/code&gt; subresource that proxies HTTP requests to each node&amp;rsquo;s Kubelet. The Kubelet itself exposes an API on port 10250, specifically the &lt;code&gt;/exec&lt;/code&gt; endpoint which allows executing commands inside a container.&lt;/p&gt;
&lt;p&gt;The issue comes from how the Kubelet handles authorizations for WebSocket connections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;kubectl exec&lt;/code&gt; uses a WebSocket connection, whose handshake is an HTTP &lt;code&gt;GET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The Kubelet maps this initial &lt;code&gt;GET&lt;/code&gt; to the &lt;code&gt;get&lt;/code&gt; RBAC verb&lt;/li&gt;
&lt;li&gt;It checks &lt;code&gt;nodes/proxy GET&lt;/code&gt;, then authorizes the operation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No secondary check&lt;/strong&gt; is performed for the &lt;code&gt;CREATE&lt;/code&gt; verb normally required for &lt;code&gt;/exec&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Result: any ServiceAccount with &lt;code&gt;nodes/proxy GET&lt;/code&gt; can execute commands in any Pod in the cluster, &lt;strong&gt;including system Pods&lt;/strong&gt; (&lt;code&gt;etcd&lt;/code&gt;, &lt;code&gt;kube-apiserver&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Exploitation via websocat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;websocat --insecure &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --header &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --protocol &lt;span class="s2"&gt;&amp;#34;v4.channel.k8s.io&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;wss://&lt;/span&gt;&lt;span class="nv"&gt;$NODE_IP&lt;/span&gt;&lt;span class="s2"&gt;:10250/exec/default/nginx/nginx?output=1&amp;amp;error=1&amp;amp;command=id&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s not all. &lt;strong&gt;Commands executed via this method do not generate any Kubernetes audit logs&lt;/strong&gt; (well, assuming you even collect them 🙈). The access goes directly through the Kubelet, which does not report events back to the API server.&lt;/p&gt;
&lt;p&gt;The official Kubernetes status on this: &lt;strong&gt;Won&amp;rsquo;t Fix&lt;/strong&gt;. It is a &amp;ldquo;design behavior&amp;rdquo; (note the quotes), addressed via a feature gate (&lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2862-fine-grained-kubelet-authz/README.md" target="_blank" rel="noopener"
&gt;KEP-2862&lt;/a&gt;, see below).&lt;/p&gt;
&lt;p&gt;Ouch.&lt;/p&gt;
&lt;h2 id="the-audit-vulnerable-serviceaccounts-on-our-clusters"&gt;The Audit: Vulnerable ServiceAccounts on our clusters
&lt;/h2&gt;&lt;p&gt;In January 2026, following the publication of Graham Helton&amp;rsquo;s article, a lot of people had to urgently audit their clusters. You can either manually audit all your Roles / ClusterRoles, or use a &lt;a class="link" href="https://gist.github.com/grahamhelton/f5c8ce265161990b0847ac05a74e466a" target="_blank" rel="noopener"
&gt;detection script&lt;/a&gt; provided by the researcher.&lt;/p&gt;
&lt;p&gt;As an example, here are three relatively common components that make great candidates for a juicy privilege escalation:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;ClusterRole&lt;/th&gt;
&lt;th&gt;ServiceAccounts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTelemetry Collector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;otel-otelcol-k8sobjects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opentelemetry-collector-daemonset-collector&lt;/code&gt;, &lt;code&gt;opentelemetry-collector-deployment-collector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTelemetry Operator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;otel-operator-resources&lt;/code&gt; / &lt;code&gt;opentelemetry-operator-manager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opentelemetry-operator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rook-Ceph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rook-ceph-global&lt;/code&gt;, &lt;code&gt;rook-ceph-mgr-cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rook-ceph-system&lt;/code&gt;, &lt;code&gt;rook-ceph-mgr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Note: there are many more. Graham Helton added an &amp;ldquo;Appendix: Affected Helm Charts&amp;rdquo; section at the end of his article, referencing AT LEAST 69 affected Helm charts according to him.&lt;/p&gt;
&lt;h3 id="the-critical-case-rook-ceph-system"&gt;The critical case: rook-ceph-system
&lt;/h3&gt;&lt;p&gt;In the official chart, the &lt;code&gt;rook-ceph-system&lt;/code&gt; ServiceAccount combined two particularly dangerous permissions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nodes/proxy GET&lt;/code&gt;&lt;/strong&gt; - the RCE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;secrets GET/LIST/WATCH&lt;/code&gt;&lt;/strong&gt; across the &lt;strong&gt;entire cluster&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Accessible secrets can include LUKS keys for volume encryption, Ceph admin keyrings, dashboard passwords&amp;hellip; this kind of access makes it a prime target for an attacker.&lt;/p&gt;
&lt;p&gt;The attack scenario: a compromise of the &lt;code&gt;rook-ceph-operator&lt;/code&gt; Pod (via CVE, supply chain, or a malicious image) would allow reading all Ceph secrets, and then executing code in any Pod (including &lt;code&gt;etcd&lt;/code&gt;), leading to a full compromise of the cluster and encrypted data.&lt;/p&gt;
&lt;p&gt;To manually check if a ServiceAccount is vulnerable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl auth can-i get nodes --subresource&lt;span class="o"&gt;=&lt;/span&gt;proxy &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --as&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:&amp;lt;namespace&amp;gt;:&amp;lt;serviceaccount&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="examples-of-fixes-to-apply"&gt;Examples of fixes to apply
&lt;/h2&gt;&lt;h3 id="rook-ceph-upstream-fix"&gt;Rook-Ceph: Upstream fix
&lt;/h3&gt;&lt;p&gt;For Rook-Ceph, the fix came from upstream: PR &lt;a class="link" href="https://github.com/rook/rook/pull/16979" target="_blank" rel="noopener"
&gt;rook/rook#16979&lt;/a&gt; removed &lt;code&gt;nodes/proxy&lt;/code&gt; from ClusterRoles. This fix is included in Rook v1.19.1, so updating the affected clusters was enough.&lt;/p&gt;
&lt;p&gt;After updating, checking across all clusters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kubectl get clusterroles rook-ceph-global -o yaml &lt;span class="p"&gt;|&lt;/span&gt; grep -A3 nodes/proxy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -&amp;gt; nothing&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="otel--otel-operator"&gt;OTel / OTel operator
&lt;/h3&gt;&lt;p&gt;For OpenTelemetry, the situation is potentially more complex. If you are using &lt;code&gt;otel-operator&lt;/code&gt; and &lt;strong&gt;OtelCollector&lt;/strong&gt; Custom Resources, you likely have to manage your own RBAC manifests &lt;strong&gt;yourself&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Having had to do it myself, it&amp;rsquo;s quite painful. Depending on your collector type and the receivers you enabled, you need to cross-reference multiple documents on the official OTel and otel-operator websites.&lt;/p&gt;
&lt;p&gt;Upstream merged a conditional approach in &lt;a class="link" href="https://github.com/open-telemetry/opentelemetry-helm-charts/pull/2083" target="_blank" rel="noopener"
&gt;open-telemetry/opentelemetry-helm-charts#2083&lt;/a&gt; based on the Kubernetes version:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Upstream approach (opentelemetry-helm-charts#2083)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- &lt;span class="l"&gt;if semverCompare &amp;#34;&amp;gt;=1.33-0&amp;#34; .Capabilities.KubeVersion.Version }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/pods&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- &lt;span class="l"&gt;else }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{{- &lt;span class="l"&gt;end }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here again, if all your clusters are up to date, you can simply replace &lt;code&gt;nodes/proxy&lt;/code&gt; with &lt;code&gt;nodes/pods&lt;/code&gt; directly (without the condition).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;otel-collector-crb.yaml&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/proxy &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# &amp;lt;- RCE risk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/spec&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/stats&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# After&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# nodes/pods replaces nodes/proxy (RCE risk, see [https://grahamhelton.com/blog/nodes-proxy-rce](https://grahamhelton.com/blog/nodes-proxy-rce))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Requires K8s &amp;gt;= 1.33 (KEP-2862 fine-grained kubelet authz)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/pods&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/spec&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/stats&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;otel-operator-rbac.yaml&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/proxy &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# &amp;lt;- RCE risk&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# After&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# nodes/pods replaces nodes/proxy (RCE risk, see [https://grahamhelton.com/blog/nodes-proxy-rce](https://grahamhelton.com/blog/nodes-proxy-rce))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Requires K8s &amp;gt;= 1.33 (KEP-2862 fine-grained kubelet authz)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;apiGroups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;nodes/pods&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;get&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="preventive-measures"&gt;Preventive Measures
&lt;/h2&gt;&lt;h3 id="kep-2862-fine-grained-kubelet-api-authorization"&gt;KEP-2862: Fine-Grained Kubelet API Authorization
&lt;/h3&gt;&lt;p&gt;As teased earlier, the real long-term solution is &lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2862-fine-grained-kubelet-authz/README.md" target="_blank" rel="noopener"
&gt;KEP-2862&lt;/a&gt; (Fine-Grained Kubelet API Authorization). It introduces granular subresources (&lt;code&gt;nodes/pods&lt;/code&gt;, &lt;code&gt;nodes/metrics&lt;/code&gt;, &lt;code&gt;nodes/stats&lt;/code&gt;, &lt;code&gt;nodes/log&lt;/code&gt;, etc.) allowing precise access without using &lt;code&gt;nodes/proxy&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;K8s Version&lt;/th&gt;
&lt;th&gt;KEP-2862 Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;Alpha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Beta, enabled by default&lt;/strong&gt; - &lt;code&gt;nodes/proxy GET&lt;/code&gt; no longer grants access to &lt;code&gt;/exec&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.36&lt;/td&gt;
&lt;td&gt;GA (locked to enabled)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;But this requires going through EVERY chart in use and checking all deployed manifests now &lt;strong&gt;and in the future&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="ciliumnetworkpolicy-blocking-the-kubelet-port"&gt;CiliumNetworkPolicy: Blocking the Kubelet port
&lt;/h3&gt;&lt;p&gt;While waiting for the K8s upgrade, or as defense-in-depth, you can block access to port 10250 from the affected pods using NetworkPolicies (or CiliumNetworkPolicy if you use Cilium as your CNI plugin).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: This only applies to components that do &lt;em&gt;not&lt;/em&gt; need to access the Kubelet. The OTel collector potentially needs it to gather Kubelet metrics. In that case, you have no choice but to fix the RBAC.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cilium.io/v2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;CiliumNetworkPolicy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;deny-kubelet-api-access&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;namespace&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endpointSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;matchLabels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;app-label&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;egressDeny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;toEntities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;remote-node&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;toPorts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10250&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;TCP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="kyverno-blocking-the-creation-of-new-roles-with-nodesproxy"&gt;Kyverno: Blocking the creation of new Roles with nodes/proxy
&lt;/h3&gt;&lt;p&gt;To prevent any regression (remember, we need to protect ourselves &lt;strong&gt;in the future&lt;/strong&gt;), we can add a Kyverno &lt;code&gt;ClusterPolicy&lt;/code&gt; that rejects the creation or modification of a &lt;code&gt;ClusterRole&lt;/code&gt; or &lt;code&gt;Role&lt;/code&gt; containing &lt;code&gt;nodes/proxy&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Luckily, there are ready-to-use examples on the official Kyverno website:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://kyverno.io/policies/other-vpol/restrict-clusterrole-nodesproxy/restrict-clusterrole-nodesproxy/" target="_blank" rel="noopener"
&gt;validating variant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kyverno.io/policies/other-cel/restrict-clusterrole-nodesproxy/restrict-clusterrole-nodesproxy/" target="_blank" rel="noopener"
&gt;CEL variant&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note : &lt;a class="link" href="https://github.com/kyverno/policies/issues/1492" target="_blank" rel="noopener"
&gt;there probably is a hole in the official Kyverno policy, I opened an issue&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;kyverno.io/v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ClusterPolicy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;restrict-nodes-proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;validationFailureAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Audit &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# switch to Enforce after validation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;deny-nodes-proxy-in-clusterroles&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;kinds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;ClusterRole&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;Role&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;system:kubelet-api-admin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# built-in K8s, unmodifiable&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; nodes/proxy grants RCE capability via Kubelet WebSocket exec.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Use nodes/pods (requires K8s &amp;gt;= 1.33, KEP-2862) instead.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;nodes/proxy&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;AnyIn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{{ request.object.rules[].resources[] }}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Deployment is done in two stages: first in &lt;code&gt;Audit&lt;/code&gt; mode to ensure there are no remaining vulnerable manifests (which you should fix &lt;strong&gt;before&lt;/strong&gt; blocking), then in &lt;code&gt;Enforce&lt;/code&gt; mode to actually block them.&lt;/p&gt;
&lt;h3 id="monitoring-the-audit-log"&gt;Monitoring the Audit Log
&lt;/h3&gt;&lt;p&gt;Even if commands executed &lt;em&gt;via&lt;/em&gt; the Kubelet leave no trace, we can monitor &lt;strong&gt;SubjectAccessReviews&lt;/strong&gt; to detect attempts at enumerating &lt;code&gt;nodes/proxy&lt;/code&gt; permissions.&lt;/p&gt;
&lt;p&gt;The configuration in the Kubernetes audit policy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# audit-policy.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Request&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;create&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;authorization.k8s.io&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;subjectaccessreviews&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then a Prometheus/Alertmanager alert on SARs related to &lt;code&gt;nodes/proxy&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-promql" data-lang="promql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Detect SARs targeting nodes/proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;increase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;apiserver_audit_event_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;verb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;&lt;/span&gt;&lt;span class="s"&gt;create&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;&lt;/span&gt;&lt;span class="s"&gt;subjectaccessreviews&lt;/span&gt;&lt;span class="p"&gt;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="s"&gt;5m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="references"&gt;References
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://grahamhelton.com/blog/nodes-proxy-rce" target="_blank" rel="noopener"
&gt;Kubernetes RCE via nodes/proxy GET&lt;/a&gt; - Graham Helton&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://gist.github.com/grahamhelton/f5c8ce265161990b0847ac05a74e466a" target="_blank" rel="noopener"
&gt;Detection Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2862-fine-grained-kubelet-authz/README.md" target="_blank" rel="noopener"
&gt;KEP-2862 Fine-Grained Kubelet API Authorization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/concepts/security/rbac-good-practices/#access-to-proxy-subresource-of-nodes" target="_blank" rel="noopener"
&gt;Kubernetes RBAC Good Practices - nodes/proxy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/rook/rook/pull/16979" target="_blank" rel="noopener"
&gt;rook/rook#16979&lt;/a&gt; - Fix upstream Rook-Ceph&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/open-telemetry/opentelemetry-helm-charts/pull/2083" target="_blank" rel="noopener"
&gt;open-telemetry/opentelemetry-helm-charts#2083&lt;/a&gt; - Fix upstream OTel&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Kubernetes UserNamespaces: the overhyped GA feature</title><link>https://blog.zwindler.fr/en/2026/04/28/kubernetes-usernamespaces-the-overhyped-ga-feature/</link><pubDate>Tue, 28 Apr 2026 10:00:00 +0200</pubDate><guid>https://blog.zwindler.fr/en/2026/04/28/kubernetes-usernamespaces-the-overhyped-ga-feature/</guid><description>&lt;img src="https://blog.zwindler.fr/2026/04/usernamespaces.webp" alt="Featured image of post Kubernetes UserNamespaces: the overhyped GA feature" /&gt;&lt;h2 id="the-infographic-that-triggered-me"&gt;The infographic that triggered me
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Note: I stumbled upon this GenAI infographic and it was so bad I wrote a post about it. I didn&amp;rsquo;t generate this thing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Over the past few days, LinkedIn has been flooded with the same kind of infographic. Kubernetes 1.36 is out, and one of the most talked-about features is the GA release of &lt;strong&gt;UserNamespaces&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a topic I&amp;rsquo;ve been following since 2018 (talk &lt;a class="link" href="https://blog.zwindler.fr/2018/05/03/recap-du-premier-jour-de-kubecon-europe-2018/" target="_blank" rel="noopener"
&gt;The Route to rootless containers&lt;/a&gt; at KubeCon EU 2018), so I&amp;rsquo;m genuinely glad to see this long journey finally reach the finish line. That said, I&amp;rsquo;m appalled by the way it&amp;rsquo;s being marketed on LinkedIn, apparently by people who have no idea how it actually works — and frankly don&amp;rsquo;t care.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Kubernetes just made root safer. Just add &lt;code&gt;hostUsers: false&lt;/code&gt; to your Pod spec.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The visual: an all-powerful king &amp;ldquo;inside the container&amp;rdquo; and a helpless beggar &amp;ldquo;outside on the host&amp;rdquo;. The promise: &amp;ldquo;No Host Access. No Privilege Escalation. No Lateral Movement. No Node Takeover.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Catchy.&lt;/p&gt;
&lt;p&gt;But presenting it this way is genuinely dangerous, because it obscures entire areas of application and operational security. Selling &lt;code&gt;hostUsers: false&lt;/code&gt; as the universal fix for the &amp;ldquo;root in containers&amp;rdquo; problem is a dramatic oversimplification that will push teams to ignore the real security &lt;strong&gt;priorities&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="what-usernamespaces-actually-do"&gt;What UserNamespaces actually do
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The threat model: container escape&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First, what are we actually talking about? A &lt;strong&gt;container escape&lt;/strong&gt; is when an attacker manages to break out of their container and directly access the host&amp;rsquo;s kernel or filesystem — completely bypassing the normal isolation mechanisms.&lt;/p&gt;
&lt;p&gt;This type of vulnerability is &lt;strong&gt;rare&lt;/strong&gt;, but real-world examples exist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2019-5736" target="_blank" rel="noopener"
&gt;CVE-2019-5736&lt;/a&gt;&lt;/strong&gt; (runc): write to &lt;code&gt;/proc/self/exe&lt;/code&gt; of the host process from inside the container&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2022-0492" target="_blank" rel="noopener"
&gt;CVE-2022-0492&lt;/a&gt;&lt;/strong&gt; (cgroups v1): escape via &lt;code&gt;unshare&lt;/code&gt; in certain configurations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2024-21626" target="_blank" rel="noopener"
&gt;CVE-2024-21626&lt;/a&gt;&lt;/strong&gt; (runc, &amp;ldquo;Leaky Vessels&amp;rdquo;): file descriptor leak to the host working directory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If there&amp;rsquo;s a vulnerability of this type on your node AND a process is compromised AND it runs as root in the container AND it doesn&amp;rsquo;t use UserNamespaces, the attacker gets &lt;strong&gt;root on the host&lt;/strong&gt; — &lt;strong&gt;game over&lt;/strong&gt;. Full access to every file on the node, every secret mounted by other pods, ability to install a rootkit or exfiltrate data from all tenants running on that node.&lt;/p&gt;
&lt;p&gt;It remains possible, but that&amp;rsquo;s a lot of &amp;ldquo;ifs&amp;rdquo;. Either way, this is exactly the scenario UserNamespaces address. They introduce &lt;strong&gt;UID mapping&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UID 0 inside the container is mapped to an unprivileged UID on the host (e.g. 100000, unique per pod)&lt;/li&gt;
&lt;li&gt;If an attacker successfully escapes the container via a kernel exploit, they land as &lt;strong&gt;&lt;code&gt;nobody&lt;/code&gt;&lt;/strong&gt; on the node — the escape succeeds, but the post-escape impact is dramatically reduced&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the &amp;ldquo;Breakouts Lose Impact&amp;rdquo; scenario from the infographic, and on that point, &lt;strong&gt;the infographic is right&lt;/strong&gt;. That&amp;rsquo;s the real value of the feature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edge case: multi-tenancy even with non-root containers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even without root containers, UserNamespaces provide something extra in a &lt;strong&gt;truly multi-tenant&lt;/strong&gt; context (multiple customers on the same cluster). Without UserNamespaces, if two pods from different customers both run with &lt;code&gt;runAsUser: 1000&lt;/code&gt;, they share the same UID 1000 on the node. If one escapes, the attacker can access files from the other pod with the same owner. UserNamespaces, by assigning a unique UID offset per pod, isolates UIDs between pods even when they use the same value inside the container.&lt;/p&gt;
&lt;p&gt;For internal clusters where you control all workloads, this scenario is theoretical. For a multi-tenant SaaS platform or a public build service, it&amp;rsquo;s a real line of defense.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical requirements&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There are a few prerequisites, but most up-to-date clusters should qualify.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux kernel ≥ 5.19&lt;/li&gt;
&lt;li&gt;Compatible runtime (containerd ≥ 1.7, CRI-O ≥ 1.25)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Idmapped mounts&lt;/em&gt; support for persistent volumes (XFS, ext4 — not NFS in all cases)&lt;/li&gt;
&lt;li&gt;Kubernetes ≥ 1.33 (Beta), ≥ 1.36 (GA)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-the-infographic-exaggerates-and-leaves-out"&gt;What the infographic exaggerates (and leaves out)
&lt;/h2&gt;&lt;p&gt;The infographic is right about one specific thing: UserNamespaces reduces the impact of a successful container escape. That&amp;rsquo;s real. The problem is it sells the feature as a universal solution to &amp;ldquo;root in containers&amp;rdquo; — and that&amp;rsquo;s just wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. UID isolation is not application privilege isolation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The infographic promises &amp;ldquo;No Lateral Movement&amp;rdquo;. That&amp;rsquo;s false — completely false.&lt;/p&gt;
&lt;p&gt;A root container with &lt;code&gt;hostUsers: false&lt;/code&gt; can still read the &lt;strong&gt;ServiceAccount Token&lt;/strong&gt; mounted at &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/token&lt;/code&gt;. If that token has broad RBAC permissions (which happens — we may cover this in a future post), the attacker can call the API Server, enumerate cluster resources, and move laterally — all without ever touching the host node.&lt;/p&gt;
&lt;p&gt;UID mapping protects the host. It does not protect the cluster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. A root container is still root inside the container&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Install Anything ✅&amp;rdquo; — it&amp;rsquo;s literally written in the infographic, presented as a feature 😖.&lt;/p&gt;
&lt;p&gt;In a root container (even with UserNS), an attacker who gains control can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;nmap&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;nc&lt;/code&gt; to scan the internal network&lt;/li&gt;
&lt;li&gt;Modify application files, binaries, configurations&lt;/li&gt;
&lt;li&gt;Read all files mounted as volumes&lt;/li&gt;
&lt;li&gt;Persist in the container across restarts if the filesystem is writable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;UserNamespaces removes none of these attack vectors. The ability to install software is a fast track to lateral movement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. It&amp;rsquo;s not that easy, especially for storage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Enabling &lt;code&gt;hostUsers: false&lt;/code&gt; breaks existing storage in most cases.&lt;/p&gt;
&lt;p&gt;Container UID 0 is mapped to UID 100000+ on the host (each container has its own offset). If a persistent volume (NFS, EBS, Ceph RBD) is owned by UID 1000, the root container can&amp;rsquo;t read or write it. The result: counterintuitive &lt;code&gt;Permission Denied&lt;/code&gt; errors that are potentially hard to diagnose, since the application was likely never designed to be root yet have no access to its own files.&lt;/p&gt;
&lt;p&gt;The technical solution exists (&lt;em&gt;idmapped mounts&lt;/em&gt;), but it requires a recent kernel and a compatible filesystem. See the &lt;a class="link" href="https://www.kernel.org/doc/html/latest/filesystems/idmappings.html" target="_blank" rel="noopener"
&gt;official idmapped mounts documentation&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Same story, but for networking&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hostUsers: false&lt;/code&gt; is incompatible with &lt;code&gt;hostNetwork: true&lt;/code&gt;. It&amp;rsquo;s a corner case, but it catches networking workloads (monitoring agents, CNI plugins, etc.).&lt;/p&gt;
&lt;p&gt;Note: that said, running containers with &lt;code&gt;hostNetwork&lt;/code&gt; is &lt;strong&gt;its own security problem&lt;/strong&gt;, so&amp;hellip;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="honest-comparison-userns-vs-the-real-alternatives"&gt;Honest comparison: UserNS vs the real alternatives
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;Attack vector&lt;/th&gt;
&lt;th style="text-align: center"&gt;UserNS (root inside)&lt;/th&gt;
&lt;th style="text-align: center"&gt;Non-root (UID 1000)&lt;/th&gt;
&lt;th style="text-align: center"&gt;Distroless / Scratch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Post-escape impact after successful container escape&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Nobody on host&lt;/td&gt;
&lt;td style="text-align: center"&gt;⚠️ UID 1000 on host&lt;/td&gt;
&lt;td style="text-align: center"&gt;⚠️ UID 1000 on host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;UID isolation between pods (multi-tenant)&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Unique offset per pod&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Shared UID on node&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Shared UID on node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Malware installation inside the container&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Trivial&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Possible&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Near impossible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Write scope in ephemeral container FS&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Full filesystem&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ App directory only&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Near impossible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Lateral movement via SA Token&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Possible&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Possible&lt;/td&gt;
&lt;td style="text-align: center"&gt;⚠️ Potentially difficult&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Operational complexity&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Sometimes high&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Often near zero&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Often low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;Compatibility with existing storage&lt;/td&gt;
&lt;td style="text-align: center"&gt;❌ Sometimes problematic&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Standard&lt;/td&gt;
&lt;td style="text-align: center"&gt;✅ Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Reading the table reveals the true nature of UserNamespaces: it excels on &lt;strong&gt;exactly two rows&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;post-escape impact&lt;/li&gt;
&lt;li&gt;UID isolation in multi-tenant environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On everything else, non-root + distroless does better, or just as well, without the operational complexity. And that &amp;ldquo;everything else&amp;rdquo; — write scope in ephemeral FS, malware installation, lateral movement via SA Token — represents the vast majority of real-world attack vectors, far more common than a container escape. We&amp;rsquo;ll come back to this in the &lt;a class="link" href="#where-to-invest-your-security-budget" &gt;Where to invest your security budget&lt;/a&gt; section.&lt;/p&gt;
&lt;h2 id="real-use-cases"&gt;Real use cases
&lt;/h2&gt;&lt;p&gt;It would be dishonest to dismiss the feature entirely. There are three scenarios where UserNamespaces aren&amp;rsquo;t a lazy option but a genuine technical necessity (with caveats).&lt;/p&gt;
&lt;h3 id="1-build-as-a-service-buildah-rootless-podman"&gt;1. Build-as-a-Service (Buildah, rootless Podman)
&lt;/h3&gt;&lt;p&gt;To build a Docker image, the build engine needs to perform &lt;code&gt;chown&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt; and &lt;code&gt;mknod&lt;/code&gt;. These operations require &lt;code&gt;CAP_CHOWN&lt;/code&gt; and &lt;code&gt;CAP_FOWNER&lt;/code&gt;. Before UserNamespaces, the solution was to run the pod as &lt;code&gt;--privileged&lt;/code&gt; — an obvious open door to the host.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;hostUsers: false&lt;/code&gt;, the build engine believes it&amp;rsquo;s root for its own file manipulation, but it can&amp;rsquo;t touch the host. This is the only case where &amp;ldquo;root inside&amp;rdquo; is a technical constraint rather than technical debt.&lt;/p&gt;
&lt;p&gt;Note: &lt;a class="link" href="https://github.com/GoogleContainerTools/kaniko" target="_blank" rel="noopener"
&gt;Kaniko&lt;/a&gt;, long the go-to for in-cluster builds, has been archived since October 2025 and no longer receives security updates. Buildah or rootless Podman are the active alternatives.&lt;/p&gt;
&lt;p&gt;My take: it can be useful for shared CI/CD platforms (GitLab Runners, Tekton) that refuse privileged pods. But if isolation is critical (public platform, aggressive multi-tenancy), microVMs (Kata Containers, Firecracker) offer far stronger guarantees for an overhead that has become quite manageable.&lt;/p&gt;
&lt;h3 id="2-hostile-multi-tenancy-user-code-platforms"&gt;2. Hostile multi-tenancy (user code platforms)
&lt;/h3&gt;&lt;p&gt;If your business is running code provided by strangers (PaaS, online code editors, public CI/CD), you know upfront that users &lt;em&gt;will&lt;/em&gt; try to escalate their privileges. In this context, UserNS is an extra barrier against kernel 0-days.&lt;/p&gt;
&lt;p&gt;My take: honestly, if the environment is truly &lt;strong&gt;hostile&lt;/strong&gt;, UserNS alone isn&amp;rsquo;t enough. MicroVMs (Kata Containers, Firecracker) provide real hardware isolation and are the right choice here. UserNS can be a complement, not a substitute.&lt;/p&gt;
&lt;h3 id="3-hard-coded-legacy-postfix-dovecot-bind"&gt;3. Hard-coded legacy (Postfix, Dovecot, BIND)
&lt;/h3&gt;&lt;p&gt;Some old UNIX daemons start as root to open a privileged port (&amp;lt; 1024) or read sensitive config files, then drop privileges via &lt;code&gt;setuid()&lt;/code&gt;. This mechanism fails in a classic non-root container.&lt;/p&gt;
&lt;p&gt;UserNamespaces let these processes believe they can make their identity management syscalls, because they are root inside their namespace.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a concrete example written by a colleague (thanks Louis 😘):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Pod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;postfix&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hostUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# UID mapping: root in container → nobody on host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# allowed under PSS Restricted *only* because of hostUsers: false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fsGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;103&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# postfix GID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;postfix&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;postfix:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# same — cf. https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/#integration-with-pod-security-admission-checks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;seccompProfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;RuntimeDefault&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ALL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# drop everything first&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;SETUID &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# privilege drop via setuid() done by postfix itself at startup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;SETGID &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# same for groups&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;CHOWN &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# chown on mail queues at startup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;FOWNER &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# file operations without being the owner&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;FSETID &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# preserve setuid bit after write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;DAC_OVERRIDE &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# MANDATORY: root in UserNS is not &amp;#34;real&amp;#34; root —&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# DAC checks are not automatically bypassed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This manifest illustrates several important things.&lt;/p&gt;
&lt;p&gt;First, making a legacy application actually secure with UserNS is painful and requires compromises — especially around capabilities. This is a far cry from the &amp;ldquo;magic feature that secures root apps&amp;rdquo; the LinkedIn wanna-be influencers imply.&lt;/p&gt;
&lt;p&gt;Then there are some interesting surprises. Normally, the &lt;code&gt;Restricted&lt;/code&gt; Pod Security Standard forbids &lt;code&gt;runAsNonRoot: false&lt;/code&gt;. Kubernetes makes an exception when &lt;code&gt;hostUsers: false&lt;/code&gt; is present. This is documented &lt;a class="link" href="https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/#integration-with-pod-security-admission-checks" target="_blank" rel="noopener"
&gt;here&lt;/a&gt;. Without UserNamespaces, this pod would be rejected by the admission controller.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the &lt;strong&gt;&lt;code&gt;DAC_OVERRIDE&lt;/code&gt; capability&lt;/strong&gt;, which is counterintuitive. Root in a UserNS is not real root from the kernel&amp;rsquo;s perspective for DAC (Discretionary Access Control) checks. When Postfix runs &lt;code&gt;set-permissions&lt;/code&gt; to &lt;code&gt;chown&lt;/code&gt; its queues, the kernel still verifies permissions — and denies them if &lt;code&gt;DAC_OVERRIDE&lt;/code&gt; isn&amp;rsquo;t present. This is exactly the kind of operational surprise that stays invisible until the first production deployment.&lt;/p&gt;
&lt;p&gt;Worth noting: we were still able to keep &lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt; and &lt;code&gt;allowPrivilegeEscalation: false&lt;/code&gt; — legacy doesn&amp;rsquo;t justify throwing everything overboard.&lt;/p&gt;
&lt;p&gt;My take: this is the only use case where UserNS is genuinely acceptable. No untrusted third-party code, no hostile platform — just well-identified legacy with a &lt;strong&gt;migration plan&lt;/strong&gt;. The other two cases are &amp;ldquo;acceptable under conditions&amp;rdquo;; the legacy case is the cleanest of the three.&lt;/p&gt;
&lt;h2 id="some-counterarguments"&gt;Some counterarguments
&lt;/h2&gt;&lt;p&gt;I see you coming with objections, so let&amp;rsquo;s save everyone some time with a quick Q&amp;amp;A:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;It&amp;rsquo;s defense in depth.&amp;rdquo;&lt;/strong&gt;
True — but defense in depth assumes the foundational layers are already in place. If you haven&amp;rsquo;t migrated your images to non-root yet, investing energy in UserNS is putting the cart before the horse. And once you&amp;rsquo;re non-root, the marginal gain of UserNS is negligible compared to the complexity it introduces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;We don&amp;rsquo;t control third-party images.&amp;rdquo;&lt;/strong&gt;
Somewhat weak, in my opinion: if a proprietary vendor&amp;rsquo;s black-box image is hardcoded to run as root, there&amp;rsquo;s a good chance it either genuinely needs it (as is the case for some proprietary security tooling) or it will break with UID mapping (see the storage problem above). UserNS is not a magic wand that makes any third-party image compatible and secure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;It&amp;rsquo;s a centralized safeguard against human error.&amp;rdquo;&lt;/strong&gt;
It&amp;rsquo;s just as easy to forget &lt;code&gt;hostUsers: false&lt;/code&gt; as it is to forget &lt;code&gt;runAsNonRoot: true&lt;/code&gt;. The real centralized solution is &lt;strong&gt;Pod Security Standards&lt;/strong&gt; or an Admission Controller (Kyverno, OPA) that outright rejects root pods. Simpler, more reliable, and it doesn&amp;rsquo;t break storage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;We need it for SOC2/PCI-DSS/&amp;hellip; compliance.&amp;rdquo;&lt;/strong&gt;
If your compliance requires strict tenant isolation, UserNS will likely be deemed insufficient by your auditors. VMs or microVMs remain the gold standard. Using UserNS for compliance means choosing the most complex tool to maintain for a result that remains debatable.&lt;/p&gt;
&lt;h2 id="where-to-invest-your-security-budget"&gt;Where to invest your security budget
&lt;/h2&gt;&lt;p&gt;Setting aside the marketing, here&amp;rsquo;s where effort actually pays off — from highest impact to most niche:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Priority 1 — Non-root images + &lt;code&gt;nobody&lt;/code&gt; (UID 65534)&lt;/strong&gt;
Move images to non-root, ideally using the &lt;code&gt;nobody&lt;/code&gt; user (the least privileged on the system). If an application is compromised under &lt;code&gt;nobody&lt;/code&gt;, the attacker can do almost nothing, even on the container filesystem. Combine with &lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt; and &lt;code&gt;capabilities: drop: [&amp;quot;ALL&amp;quot;]&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runAsUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;65534&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# nobody&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;seccompProfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;RuntimeDefault&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;app&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;my-app:distroless&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;securityContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ALL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Priority 2 — Pod Security Standards (PSS) at &lt;code&gt;Baseline&lt;/code&gt; or &lt;code&gt;Restricted&lt;/code&gt;&lt;/strong&gt;
Block root and privileges without breaking anything at the infra level. It requires having done Priority 1 first, but it&amp;rsquo;s free, standard, and applies cluster-wide via a namespace label (with per-namespace overrides when needed). No more risk of forgetting. Already enabled by default on several Kubernetes distributions (Talos being one, but not the only one).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Priority 3 — MicroVMs (Kata Containers, Firecracker)&lt;/strong&gt;
For truly untrusted workloads. Real hardware isolation, overhead now quite reasonable on recent generations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Priority 4 — UserNamespaces&lt;/strong&gt;
When all else fails. Only for the legitimate cases identified above (builds, legacy, hostile multi-tenancy). This is genuinely the &lt;strong&gt;last&lt;/strong&gt; thing to do.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Kubernetes 1.36 UserNamespaces are the result of a project that officially took five years (KEP-127 dates back to 2021) and has been discussed since nearly the dawn of Kubernetes. For shared build platforms and multi-tenant SaaS running user-provided code, it&amp;rsquo;s a potentially useful building block — particularly to prevent one customer&amp;rsquo;s app from reading another&amp;rsquo;s in the event of a container escape without privilege escalation.&lt;/p&gt;
&lt;p&gt;For everything else — that is to say, 99% of production clusters — that&amp;rsquo;s not where container security starts. And that&amp;rsquo;s precisely the problem with this kind of infographic.&lt;/p&gt;
&lt;p&gt;LinkedIn infographics selling effortless security are dangerous: &amp;ldquo;keep your 800MB root image full of tools, just add &lt;code&gt;hostUsers: false&lt;/code&gt;, and you&amp;rsquo;re protected.&amp;rdquo; That&amp;rsquo;s exactly &lt;strong&gt;the wrong approach&lt;/strong&gt;. Real container security is built in the Dockerfile, not in the PodSpec.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&amp;rsquo;re enabling UserNamespaces to secure an application whose source code you own, you&amp;rsquo;ve probably missed a step in your secure software development lifecycle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="references"&gt;References
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/127-user-namespaces/README.md" target="_blank" rel="noopener"
&gt;KEP-127: Support for User Namespaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/" target="_blank" rel="noopener"
&gt;Kubernetes docs — User Namespaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://kubernetes.io/docs/concepts/security/pod-security-standards/" target="_blank" rel="noopener"
&gt;Pod Security Standards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2019-5736" target="_blank" rel="noopener"
&gt;CVE-2019-5736&lt;/a&gt; — runc: write to host&amp;rsquo;s &lt;code&gt;/proc/self/exe&lt;/code&gt; from inside the container&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2022-0492" target="_blank" rel="noopener"
&gt;CVE-2022-0492&lt;/a&gt; — cgroups v1: escape via &lt;code&gt;unshare&lt;/code&gt;, UserNS helps but &lt;code&gt;runAsNonRoot&lt;/code&gt; does too&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nvd.nist.gov/vuln/detail/CVE-2024-21626" target="_blank" rel="noopener"
&gt;CVE-2024-21626&lt;/a&gt; — runc &amp;ldquo;Leaky Vessels&amp;rdquo;: file descriptor leak to host working directory&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/GoogleContainerTools/distroless" target="_blank" rel="noopener"
&gt;Distroless containers — GoogleContainerTools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>