<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Indra Gusti Prasetya</title>
    <description>The latest articles on DEV Community by Indra Gusti Prasetya (@indra_gustiprasetya_a80a).</description>
    <link>https://dev.to/indra_gustiprasetya_a80a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3971045%2F76516018-d46d-403b-9d79-239ac1d80baa.png</url>
      <title>DEV Community: Indra Gusti Prasetya</title>
      <link>https://dev.to/indra_gustiprasetya_a80a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/indra_gustiprasetya_a80a"/>
    <language>en</language>
    <item>
      <title>Kubernetes Hardening 2026: RBAC, PSS &amp; Unfixed CVEs</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Sat, 06 Jun 2026 12:45:46 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/kubernetes-hardening-2026-rbac-pss-unfixed-cves-4nni</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/kubernetes-hardening-2026-rbac-pss-unfixed-cves-4nni</guid>
      <description>&lt;p&gt;I have yet to clean up a breached cluster that fell to a clever zero-day. The pattern is always duller: a service account auto-mounted a token nobody scoped, a namespace shipped with no network policy, and a &lt;code&gt;cluster-admin&lt;/code&gt; binding granted years ago for "just this one migration" was never pulled. Kubernetes defaults are tuned for compatibility, so hardening is work you do after install, not something the platform hands you. Below is the checklist I actually run against production clusters in 2026, in roughly the order that buys the most safety per hour. Two things shifted this year: RBAC-driven takeovers tied to bugs like IngressNightmare, and a June 1, 2026 correction from the Kubernetes Security Response Committee that reclassified several CVEs as permanently unfixed, which means your scanners are about to start shouting about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enforce Pod Security Standards at &lt;code&gt;restricted&lt;/code&gt;, but get there through &lt;code&gt;warn&lt;/code&gt; first.&lt;/strong&gt; PodSecurityPolicy is gone; Pod Security Admission (built in since 1.25) replaces it and works per namespace via labels. If you jump straight to &lt;code&gt;enforce: restricted&lt;/code&gt;, you will reject running workloads and get paged at 3am. Apply &lt;code&gt;warn&lt;/code&gt; and &lt;code&gt;audit&lt;/code&gt; first, read what they flag, fix the offending pods, then flip enforce on.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# Observe without breaking anything&lt;/span&gt;
   kubectl label ns payments &lt;span class="se"&gt;\&lt;/span&gt;
     pod-security.kubernetes.io/warn&lt;span class="o"&gt;=&lt;/span&gt;restricted &lt;span class="se"&gt;\&lt;/span&gt;
     pod-security.kubernetes.io/audit&lt;span class="o"&gt;=&lt;/span&gt;restricted
   &lt;span class="c"&gt;# Once the warnings are clean:&lt;/span&gt;
   kubectl label ns payments pod-security.kubernetes.io/enforce&lt;span class="o"&gt;=&lt;/span&gt;restricted &lt;span class="nt"&gt;--overwrite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hunt down every &lt;code&gt;cluster-admin&lt;/code&gt; binding and justify it out loud.&lt;/strong&gt; More than half of the production clusters I have assessed carry at least one RBAC misconfiguration that lets a compromised pod climb to cluster-admin. The fastest win is listing every wildcard and every cluster-admin grant, then asking, per binding, whether that human or workload still needs it.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   kubectl get clusterrolebindings &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="s1"&gt;'.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the &lt;code&gt;escalate&lt;/code&gt;, &lt;code&gt;bind&lt;/code&gt;, and &lt;code&gt;impersonate&lt;/code&gt; verbs especially, plus &lt;code&gt;create&lt;/code&gt; on pods. Any one of them is a quiet path to full takeover, and they rarely look dangerous in a code review.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn off auto-mounted service account tokens.&lt;/strong&gt; Roughly 87% of clusters in audits still mount a token into every pod running under the namespace &lt;code&gt;default&lt;/code&gt; service account. That token is a finished credential sitting in the filesystem, waiting for anyone who lands a shell. Disable it at the service account level and opt workloads back in only when they genuinely call the API.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
   &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
   &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
     &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments&lt;/span&gt;
   &lt;span class="na"&gt;automountServiceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Give every namespace a default-deny NetworkPolicy on the day it is created.&lt;/strong&gt; With no policy in place, every pod can reach every other pod across namespaces, flat and open, and most teams never see it because nothing is logging the traffic. Lay down a deny-all baseline, then add explicit allow rules on top. Bake it into your namespace template so no namespace ever ships without one.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
   &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
   &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;default-deny-all&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;payments&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
   &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
     &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ingress"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Egress"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One gotcha that has bitten me: confirm your CNI actually enforces policy. Calico and Cilium do, but some managed clusters hand you a NetworkPolicy API that silently does nothing because enforcement was never switched on.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat creating or editing Ingress objects as a privileged operation.&lt;/strong&gt; IngressNightmare (CVE-2025-1974, CVSS 9.8) let attackers inject arbitrary NGINX config through the ingress-nginx admission controller and read every secret in that controller's service account scope. Patching the controller matters, but the durable fix is RBAC: stop handing &lt;code&gt;Ingress&lt;/code&gt; create and update rights to every developer namespace by default. Check who actually holds it.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   kubectl auth can-i create ingress &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:dev:builder &lt;span class="nt"&gt;-n&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mitigate the CVEs that will never get a patch.&lt;/strong&gt; On June 1, 2026 the SRC corrected the records for CVE-2020-8561, CVE-2020-8562, and CVE-2021-25740 to show no fixed version. They are architectural trade-offs, not coding bugs, so scanners that used to stay quiet will now alert and you cannot version-bump your way out. The answer is configuration:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2020-8561&lt;/strong&gt; (webhook redirect): run the API server with &lt;code&gt;--profiling=false&lt;/code&gt; and keep audit log verbosity under level 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2021-25740&lt;/strong&gt; (cross-namespace endpoint forwarding): restrict write access to &lt;code&gt;Endpoints&lt;/code&gt; and &lt;code&gt;EndpointSlice&lt;/code&gt; objects via RBAC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2020-8562&lt;/strong&gt; (DNS TOCTOU): run a local DNS cache with an enforced &lt;code&gt;min-cache-ttl&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write these up as accepted-with-mitigation in your risk register. The point is so the next engineer doesn't burn a day chasing a patch that does not exist.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spell out non-root, read-only root filesystem, and dropped capabilities in every securityContext.&lt;/strong&gt; The &lt;code&gt;restricted&lt;/code&gt; PSS profile already requires these, but admission labels get loosened during incidents and stay loosened. Putting the block directly in your manifests means the protection survives a sloppy label change. This single stanza closes most container-escape and privilege-escalation primitives before they start.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
     &lt;span class="na"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
     &lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
     &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALL"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
     &lt;span class="na"&gt;seccompProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RuntimeDefault&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run kube-bench and treat the CIS Benchmark as a backlog, not a one-time report.&lt;/strong&gt; The CIS Kubernetes Benchmark has 200+ checks and you will not clear them in a sprint. Run &lt;code&gt;kube-bench&lt;/code&gt; against the control plane and worker nodes, then triage by blast radius: RBAC, Pod Security, network policy, etcd encryption, and audit logging come first; cosmetic file-permission findings can wait.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   kubectl run kube-bench &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aquasec/kube-bench:latest &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; run &lt;span class="nt"&gt;--targets&lt;/span&gt; node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt etcd secrets at rest, then prove it actually happened.&lt;/strong&gt; By default, Secrets are base64-encoded in etcd, which is encoding, not encryption, and anyone who reads the datastore reads your passwords. Enable an &lt;code&gt;EncryptionConfiguration&lt;/code&gt; with a KMS provider, or at minimum &lt;code&gt;aescbc&lt;/code&gt;, then verify by pulling the raw etcd value and confirming it is ciphertext rather than your credentials in plain sight.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nv"&gt;ETCDCTL_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 etcdctl get /registry/secrets/payments/db-creds | hexdump &lt;span class="nt"&gt;-C&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deploy a runtime scanner and route its findings somewhere a human reads weekly.&lt;/strong&gt; Static hardening starts drifting the moment developers ship the next change. Trivy Operator (or Kubescape) scans workloads, RBAC, and images in-cluster continuously and exposes the results as CRDs you can alert on. The install is the easy part. The discipline is someone reviewing the &lt;code&gt;vulnerabilityreports&lt;/code&gt; and &lt;code&gt;rbacassessmentreports&lt;/code&gt; on a real cadence instead of letting them pile up.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get vulnerabilityreports &lt;span class="nt"&gt;-A&lt;/span&gt;
kubectl get configauditreports &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.report.summary.criticalCount
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;If you change one habit, make it this: push hardening into namespace creation instead of into a quarterly audit. A namespace template that ships a default-deny NetworkPolicy, &lt;code&gt;automountServiceAccountToken: false&lt;/code&gt;, a scoped developer RoleBinding, and the &lt;code&gt;restricted&lt;/code&gt; Pod Security label means every new team starts from a safe floor rather than an open one. Hardening that relies on people remembering decays between reorgs; hardening that is the default holds. Get the floor right and every other item on this list becomes far easier to enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/security/pod-security-standards/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/security/rbac-good-practices/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/security/rbac-good-practices/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/blog/2026/05/26/reconciling-unfixed-kubernetes-cves/" rel="noopener noreferrer"&gt;https://kubernetes.io/blog/2026/05/26/reconciling-unfixed-kubernetes-cves/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sentinelone.com/blog/ingressnightmare-critical-unauthenticated-rce-vulnerabilities-in-kubernetes-ingress-nginx/" rel="noopener noreferrer"&gt;https://www.sentinelone.com/blog/ingressnightmare-critical-unauthenticated-rce-vulnerabilities-in-kubernetes-ingress-nginx/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tips</category>
    </item>
    <item>
      <title>Kubernetes User Namespaces in 1.36 with hostUsers: false</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Sat, 06 Jun 2026 09:44:54 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/kubernetes-user-namespaces-in-136-with-hostusers-false-gpb</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/kubernetes-user-namespaces-in-136-with-hostusers-false-gpb</guid>
      <description>&lt;p&gt;By the end of this you will have a worker node provisioned for user namespaces and a Pod whose in-container root (UID 0) maps to a powerless UID on the host, verified by reading &lt;code&gt;/proc/self/uid_map&lt;/code&gt; instead of trusting the Pod spec. Kubernetes v1.36 shipped this as GA on 22 April 2026, and the field that turns it on, &lt;code&gt;hostUsers: false&lt;/code&gt;, is the easy part. The node prep underneath it is where people lose an afternoon.&lt;/p&gt;

&lt;p&gt;The reason this is worth your time over asking a model to "set up user namespaces": the working knowledge here is too new and too underdocumented for that to help. The kernel floor is 6.3, not the 5.12 everyone quotes. The failure mode when you get a dependency wrong is usually silence, not an error. And the same v1.36 upgrade that makes the feature free also deletes containerd 1.x support out from under you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes v1.36+&lt;/strong&gt;. &lt;code&gt;UserNamespacesSupport&lt;/code&gt; is GA and on by default, so no feature gate. On v1.33 to v1.35 the feature works but you may still need the gate enabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux kernel 6.3 or newer on every node.&lt;/strong&gt; idmap mounts landed in 5.12, but tmpfs idmap support (which kubelet needs for &lt;code&gt;emptyDir&lt;/code&gt; and projected volumes) only merged in 6.3. This is the single most-missed requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;containerd 2.0+&lt;/strong&gt; or &lt;strong&gt;CRI-O 1.25+&lt;/strong&gt;, backed by &lt;strong&gt;runc 1.2+&lt;/strong&gt; or &lt;strong&gt;crun 1.9+&lt;/strong&gt; (1.13+ recommended).&lt;/li&gt;
&lt;li&gt;A node filesystem under &lt;code&gt;/var/lib/kubelet/pods/&lt;/code&gt; that supports idmap mounts: btrfs, ext4, xfs, fat, tmpfs, or overlayfs.&lt;/li&gt;
&lt;li&gt;Root SSH access to a worker node, plus &lt;code&gt;kubectl&lt;/code&gt; against the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Confirm the kernel and runtime actually qualify
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;                          &lt;span class="c"&gt;# need &amp;gt;= 6.3&lt;/span&gt;
containerd &lt;span class="nt"&gt;--version&lt;/span&gt;              &lt;span class="c"&gt;# need &amp;gt;= 2.0.0&lt;/span&gt;
runc &lt;span class="nt"&gt;--version&lt;/span&gt;                    &lt;span class="c"&gt;# need &amp;gt;= 1.2.0  (or: crun --version &amp;gt;= 1.9)&lt;/span&gt;
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %T /var/lib/kubelet    &lt;span class="c"&gt;# expect ext4 / xfs / btrfs / overlayfs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checks the four things that have to be true before any YAML matters. If &lt;code&gt;uname -r&lt;/code&gt; shows something like &lt;code&gt;5.15&lt;/code&gt;, stop here, because no Pod spec will fix a kernel that lacks tmpfs idmap. Cloud vendor LTS images frequently ship 5.x kernels, which is exactly why this feature fails so often in the field.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create the kubelet system user and subordinate ID ranges
&lt;/h3&gt;

&lt;p&gt;kubelet maps each Pod into a slice of host UIDs and GIDs that it reads from &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt;, keyed to a user literally named &lt;code&gt;kubelet&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the user if it doesn't exist (no login shell needed)&lt;/span&gt;
useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /usr/sbin/nologin kubelet 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Allocate subordinate IDs: 65536 * maxPods. For the default 110 pods:&lt;/span&gt;
&lt;span class="c"&gt;#   65536 * 110 = 7208960&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'kubelet:65536:7208960'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/subuid
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'kubelet:65536:7208960'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/subgid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arithmetic has rules the tooling enforces: the first ID must be a multiple of 65536 and at least 65536 (never the 0 to 65535 range), the count must be a multiple of 65536, and the UID and GID ranges must match. Use exactly one line per user. Multiple ranges are not supported, and a second line does not extend the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Install &lt;code&gt;getsubids&lt;/code&gt; so kubelet can read those ranges
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; uidmap
&lt;span class="c"&gt;# Fedora/RHEL&lt;/span&gt;
dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; shadow-utils

&lt;span class="c"&gt;# Verify kubelet's allocation is visible:&lt;/span&gt;
getsubids kubelet
getsubids &lt;span class="nt"&gt;-g&lt;/span&gt; kubelet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kubelet shells out to &lt;code&gt;getsubids&lt;/code&gt; (from shadow-utils) at startup to discover the ranges from step 2. If that binary is missing from &lt;code&gt;PATH&lt;/code&gt;, kubelet falls back to running Pods &lt;em&gt;without&lt;/em&gt; namespaces and emits no hard error. This is the quietest failure in the whole feature, so install the package before you rely on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. (Optional) Raise the per-Pod ID count
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /var/lib/kubelet/config.yaml  (KubeletConfiguration)&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubelet.config.k8s.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KubeletConfiguration&lt;/span&gt;
&lt;span class="na"&gt;userNamespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;idsPerPod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;65536&lt;/span&gt;        &lt;span class="c1"&gt;# default; must be a multiple of 65536&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave this at the default unless a workload genuinely needs a UID range wider than 65536. If you raise it, redo the &lt;code&gt;/etc/subuid&lt;/code&gt; math (&lt;code&gt;idsPerPod × maxPods&lt;/code&gt;) or you will exhaust the range partway through your Pod density. Changes apply only to containers created after a kubelet restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart kubelet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Deploy a Pod with user namespaces enabled
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;userns-demo&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hostUsers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;          &lt;span class="c1"&gt;# &amp;lt;-- the whole feature&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;busybox:1.36&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runAsUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;          &lt;span class="c1"&gt;# root *inside* the container, on purpose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hostUsers: false&lt;/code&gt; tells kubelet to place the Pod in its own user namespace. The container believes it runs as UID 0; the host kernel sees an unprivileged UID from the kubelet subuid range. Capabilities like &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt; and &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; become namespace-scoped, so they are real inside the container and meaningless against the host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; userns-demo.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verify it works
&lt;/h2&gt;

&lt;p&gt;Do not trust the field. Prove the mapping by reading the in-container view, then the host view, and confirming they differ.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inside the container: claims to be root (UID 0)&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;userns-demo &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="c"&gt;# uid=0(root) gid=0(root) ...&lt;/span&gt;

&lt;span class="c"&gt;# Inside the container: inspect the namespace's UID map&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;userns-demo &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /proc/self/uid_map
&lt;span class="c"&gt;#          0     &amp;lt;hostUID&amp;gt;      65536&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;uid_map&lt;/code&gt; line of &lt;code&gt;0 &amp;lt;some-large-number&amp;gt; 65536&lt;/code&gt; (for example &lt;code&gt;0 65536 65536&lt;/code&gt;) confirms isolation: container UID 0 begins at host UID 65536, spanning 65536 IDs. The dead giveaway that it is &lt;em&gt;not&lt;/em&gt; working is &lt;code&gt;0 0 4294967295&lt;/code&gt;, the identity map, which means the Pod is sharing the host user namespace despite the field being set.&lt;/p&gt;

&lt;p&gt;Now confirm from the node itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the worker node, find the container's real host UID&lt;/span&gt;
ps &lt;span class="nt"&gt;-o&lt;/span&gt; pid,uid,cmd &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;
&lt;span class="c"&gt;# UID column shows ~65536, NOT 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing a high UID like &lt;code&gt;65536&lt;/code&gt; for a process that thinks it is root is the win condition. In practice, the step that bites people is skipping this node-side check: the Pod starts cleanly, &lt;code&gt;kubectl exec ... id&lt;/code&gt; prints &lt;code&gt;uid=0&lt;/code&gt;, and everything looks done, but if &lt;code&gt;getsubids&lt;/code&gt; was missing in step 3 the Pod is running with the host identity map and zero added isolation. The &lt;code&gt;/proc/self/uid_map&lt;/code&gt; line is the only thing that tells you the truth, so I treat a green Pod with no uid_map check as unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kernel 5.12 is not enough.&lt;/strong&gt; idmap mounts exist there, but tmpfs idmap support arrived in 6.3, and kubelet needs it for &lt;code&gt;emptyDir&lt;/code&gt; and &lt;code&gt;projected&lt;/code&gt; volumes. On an older kernel, Pods with those volumes fail to mount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent fallback when &lt;code&gt;getsubids&lt;/code&gt; is missing.&lt;/strong&gt; No &lt;code&gt;uidmap&lt;/code&gt; or &lt;code&gt;shadow-utils&lt;/code&gt; package means kubelet runs the Pod without namespaces and logs only at high verbosity. Always verify with &lt;code&gt;/proc/self/uid_map&lt;/code&gt;, never with the spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hostPath&lt;/code&gt; and pre-existing volumes defeat it.&lt;/strong&gt; Files owned by UIDs outside the mapped range appear as the overflow ID 65534 (&lt;code&gt;nobody&lt;/code&gt;) and are not writable. User namespaces compose cleanly with &lt;code&gt;emptyDir&lt;/code&gt;, projected, and idmap-capable CSI volumes, but not with arbitrary host paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;containerd 1.x will break your upgrade.&lt;/strong&gt; v1.36 dropped support for it (1.35 was the last release to carry it). If nodes still run 1.x, the kubelet upgrade fails regardless of user namespaces, so move to 2.0+ first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is mitigation, not a force field.&lt;/strong&gt; &lt;code&gt;hostUsers: false&lt;/code&gt; greatly reduces the blast radius of escapes like CVE-2024-21626 (the runc working-directory and leaked-fd breakout) and CVE-2022-0492 (the cgroups &lt;code&gt;release_agent&lt;/code&gt; flaw), but the kernel is still shared. An attacker can still do whatever a file's "other" permission bits allow, and a kernel-level exploit is unaffected. Treat this as defense-in-depth alongside seccomp, Pod Security Standards, and runtime isolation, not a replacement for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-Pod range changes need a kubelet restart&lt;/strong&gt; and only affect new containers. Running Pods keep their old mapping until recreated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;You now have a node carrying kubelet subuid and subgid ranges, a runtime and kernel that actually support idmap mounts, and a Pod whose in-container root is a powerless host UID, confirmed through &lt;code&gt;/proc/self/uid_map&lt;/code&gt; rather than faith. The highest-leverage next move is to make this the default instead of a per-Pod opt-in: bake the &lt;code&gt;/etc/subuid&lt;/code&gt; provisioning and the kernel and runtime version floors into your node image, then write a Kyverno or Validating Admission Policy rule that mutates &lt;code&gt;hostUsers: false&lt;/code&gt; onto every workload that does not need host UID identity. That turns a GA checkbox into a baseline the whole cluster inherits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/blog/2026/04/23/kubernetes-v1-36-userns-ga/" rel="noopener noreferrer"&gt;https://kubernetes.io/blog/2026/04/23/kubernetes-v1-36-userns-ga/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.zwindler.fr/en/2026/04/28/kubernetes-usernamespaces-the-overhyped-ga-feature/" rel="noopener noreferrer"&gt;https://blog.zwindler.fr/en/2026/04/28/kubernetes-usernamespaces-the-overhyped-ga-feature/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
