<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Podo Stack]]></title><description><![CDATA[Tools that survived production. Weekly curation]]></description><link>https://podostack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!K687!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F647baa21-c6c0-4d23-bcf0-ddf3a7a641ed_500x500.png</url><title>Podo Stack</title><link>https://podostack.com</link></image><generator>Substack</generator><lastBuildDate>Sun, 20 Sep 2026 08:18:02 GMT</lastBuildDate><atom:link href="https://podostack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Ilia]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[podostack@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[podostack@substack.com]]></itunes:email><itunes:name><![CDATA[Ilia Gusev]]></itunes:name></itunes:owner><itunes:author><![CDATA[Ilia Gusev]]></itunes:author><googleplay:owner><![CDATA[podostack@substack.com]]></googleplay:owner><googleplay:email><![CDATA[podostack@substack.com]]></googleplay:email><googleplay:author><![CDATA[Ilia Gusev]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Bitnami, one year later: hardened base images went free]]></title><description><![CDATA[bitnamilegacy, Docker Hardened Images under Apache 2.0, Chainguard Catalog Starter, VEX-filtered CVE counts, stale Helm chart defaults]]></description><link>https://podostack.com/p/bitnami-hardened-images-year-later</link><guid isPermaLink="false">https://podostack.com/p/bitnami-hardened-images-year-later</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Fri, 18 Sep 2026 14:03:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!v6rS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v6rS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v6rS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!v6rS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!v6rS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!v6rS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v6rS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!v6rS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!v6rS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!v6rS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!v6rS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffc61ad6b-6394-463b-9777-e84aabfd1bec_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This week I ran helm show values on the Apache Airflow chart, version 1.22.0, released in June. The bundled PostgreSQL points at bitnamilegacy/postgresql, tag 16.1.0-debian-11-r15. The image labels say it was built on November 30, 2023, on Debian 11, and Debian 11 left LTS on August 31.</p><p>That image is there on purpose. A year ago Bitnami shut most of its free catalog and said running it cost too much. Eleven weeks later Docker gave its hardened images away under Apache 2.0, and by August 2026 a startup that matched that price had announced its shutdown.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://podostack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Podo Stack is free. Subscribe to get every issue and deep dive by email.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>September 29, 2025: the tags disappeared</h2><p>Broadcom posted the notice on July 16, 2025, in bitnami/containers issue #83267. On August 28 Bitnami stopped publishing Debian-based images and OCI charts to Docker Hub, then ran brownouts through September.</p><p>The cut came on September 29. That morning someone filed an issue because docker.io/bitnami/kafka had no tags at all. The maintainer replied that Kafka wasn't in the trial set: about 40 images and 10 charts stayed free, latest tag only, for development. Versioned tags were frozen in docker.io/bitnamilegacy. Production moved to Bitnami Secure Images, rebuilt on VMware's Photon OS, with no public price; press coverage put it at $50,000 a year and up.</p><p>The reasoning came in a Broadcom community post on August 18: "Bitnami has been the Jenkins of the internet for many years, but this has become unsustainable. Operating a build pipeline and OCI registry for the general public is very expensive."</p><p>AWS dropped its 317 Bitnami repositories from ECR Public on June 10, 2026. Today docker.io/bitnami/postgresql has no version tags, only latest, which is PostgreSQL 18.6 on Photon OS 5.0:</p><pre><code>$ docker pull bitnami/postgresql:16.4.0
Error response from daemon: manifest for bitnami/postgresql:16.4.0 not found: manifest unknown: manifest unknown</code></pre><h3>Links</h3><ul><li><p><a href="https://github.com/bitnami/containers/issues/83267">bitnami/containers #83267: changes to the Bitnami catalog</a></p></li><li><p><a href="https://community.broadcom.com/tanzu/blogs/beltran-rueda-borrego/2025/08/18/how-to-prepare-for-the-bitnami-changes-coming-soon">Broadcom: how to prepare for the Bitnami changes</a></p></li><li><p><a href="https://aws.amazon.com/blogs/containers/bitnami-image-removal-from-ecr-public/">AWS: Bitnami image removal from ECR Public</a></p></li><li><p><a href="https://hub.docker.com/u/bitnamilegacy">Bitnami Legacy on Docker Hub</a></p></li></ul><h2>December 17, 2025: Docker gave its images away</h2><p>Docker launched Docker Hardened Images on May 19, 2025, as a paid product with a 7-day patch window for critical and high CVEs. When Bitnami users went shopping that August, Docker called DHI "affordable and transparent", which still meant a trial and a sales conversation.</p><p>On December 17 more than 1,000 images and Helm charts went free under Apache 2.0, served from dhi.io. A Docker account is the entry ticket; an anonymous token request to dhi.io comes back Unauthorized. Unlike Bitnami's leftovers, the free tier keeps versioned tags such as python:3.13. Select and Enterprise hold the SLA and the FIPS and STIG variants, and Enterprise alone gets five years of Extended Lifecycle Support past upstream end of life. The public definitions repo held 595 image definitions and 96 charts when I counted this week.</p><p>One developer quoted by DevClass wasn't sold: "It's free for now, just like registries were 'free' and docker desktop was free... until they weren't."</p><h3>Links</h3><ul><li><p><a href="https://www.docker.com/blog/docker-hardened-images-for-every-developer/">Docker: Hardened Images for everyone</a></p></li><li><p><a href="https://docs.docker.com/dhi/">DHI docs: Community, Select, Enterprise</a></p></li><li><p><a href="https://github.com/docker-hardened-images/catalog">GitHub: docker-hardened-images/catalog</a></p></li><li><p><a href="https://devclass.com/2025/12/18/docker-hardened-images-now-free-devs-give-cautious-welcome/">DevClass: devs give cautious welcome</a></p></li></ul><h2>2026: everyone priced against zero</h2><p>Chainguard had the most to lose and moved the least. Its free images at cgr.dev/chainguard need no login but ship as latest and latest-dev only, a set it trimmed in November 2024. Anonymously, python:latest resolves for me and python:3.12 returns 404. In December CEO Dan Lorenc told TechTarget he'd be "pretty cautious" about Docker's offer. On March 17, 2026, Chainguard launched Catalog Starter: five images of your choice, all supported versions, production use allowed, no CVE SLA, corporate email required.</p><p>Red Hat followed on May 12 with Red Hat Hardened Images, a no-cost catalog of more than 45 images.</p><p>Minimus went furthest. On June 23 it opened its whole catalog, every major and minor version, no registration. By late August its homepage said the reg.mini.dev registry switches off on October 22. Echo bought the technology on August 27 and told TechTarget it would keep the platform running. Chris Hughes of Noma Security told the same outlet: "Everyone kind of raced to 'out-free' each other."</p><h3>Links</h3><ul><li><p><a href="https://www.chainguard.dev/unchained/introducing-chainguard-catalog-starter">Chainguard: introducing Catalog Starter</a></p></li><li><p><a href="https://edu.chainguard.dev/chainguard/containers/concepts/container-categories/">Chainguard Academy: container categories and the free tier</a></p></li><li><p><a href="https://www.redhat.com/en/about/press-releases/red-hat-hardened-images-accelerates-cloud-native-development-and-zero-cve-strategies">Red Hat Hardened Images GA</a></p></li><li><p><a href="https://www.techtarget.com/it-infrastructure/news/366649818/Rival-buyout-aids-customers-after-DevSecOps-firm-shutters">TechTarget: rival buyout after Minimus shutters</a></p></li></ul><h2>What "near-zero CVEs" counts</h2><p>Every catalog above advertises near-zero CVEs, so I went looking for the denominator. Docker's number is what a scanner reports after applying Docker's own VEX statements. A CVE Docker judges unexploitable gets <code>not_affected</code> with a justification, and a backported fix without a version bump gets <code>not_affected</code> plus <code>inline_mitigations_already_exist</code>. Docker's docs warn that scanners without VEX support report more. The Grype example in those same docs, VEX applied, still shows a High perl CVE marked won't fix.</p><p>Chainguard publishes its triage as security advisories in wolfi-dev/advisories and secdb feeds that scanners consume. Its docs say plainly that a CVE count depends on build age, and show an October 2024 jre digest scanning dirty. The paid SLA clock starts once a qualifying patch exists: seven days for critical, fourteen for everything else.</p><p>The honest reading of zero is a fresh build scanned by a tool that accepts the vendor's triage. Pin that digest for six months and you run a six-month-old image with the same badge.</p><h3>Links</h3><ul><li><p><a href="https://docs.docker.com/dhi/explore/security-concepts/vex/">DHI docs: VEX statuses and justifications</a></p></li><li><p><a href="https://docs.docker.com/dhi/explore/scanner-integrations/">DHI docs: scanner integrations</a></p></li><li><p><a href="https://edu.chainguard.dev/chainguard/containers/concepts/zerocve/">Chainguard: low-to-no CVEs, and how to check</a></p></li><li><p><a href="https://www.chainguard.dev/legal/cve-policy">Chainguard CVE remediation policy</a></p></li></ul><h2>What teams did with their charts</h2><p>On September 18, 2025, mid-brownout, Airflow merged "Temporary fix to Bitnami psql chart licensing issues", which swapped in the official postgres:13 image, a major-version downgrade from 16. In January 2026 a follow-up put back the exact Bitnami image, now from the archive: "It's likely users of the provided postgres moved to this image anyways." Mastodon pointed its Bitnami subchart images at bitnamilegacy in October 2025. Superset did the same on May 4, 2026, "until we can switch over to Valkey and vanilla Postgres."</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!a0hX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!a0hX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 424w, https://substackcdn.com/image/fetch/$s_!a0hX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 848w, https://substackcdn.com/image/fetch/$s_!a0hX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!a0hX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!a0hX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png" width="1832" height="1008" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1008,&quot;width&quot;:1832,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!a0hX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 424w, https://substackcdn.com/image/fetch/$s_!a0hX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 848w, https://substackcdn.com/image/fetch/$s_!a0hX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!a0hX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf70a7f2-deca-473f-b79a-cc06fa59dc77_1832x1008.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>GitLab split it. The company's infrastructure team parked its Redis caches on bitnamilegacy/redis as a short-term fix, while the product chart removed bundled Bitnami PostgreSQL and Redis in GitLab 19.0 on May 21, 2026, "with no replacement", and the migration guide points at CloudNativePG and Valkey. Langfuse's chart 2.0.0, out August 18, replaced Bitnami Redis with the official Valkey chart and Bitnami Postgres with groundhog2k/postgres. <a href="https://podostack.com/p/valkey-one-year-fork-default">The Valkey issue</a> covered where that chart came from.</p><p>The advice from both ends of the removal was mirroring. A Bitnami maintainer told one user to copy an image "to your own registry before the final removal." AWS, retiring its own copies, wrote: "Never point production workloads directly at a public registry you don't own." A mirror with digest pins trades a tag that vanishes for a digest that never changes, and someone still has to move the pin.</p><h3>Links</h3><ul><li><p><a href="https://github.com/apache/airflow/pull/55820">apache/airflow #55820</a> and <a href="https://github.com/apache/airflow/pull/61156">#61156</a></p></li><li><p><a href="https://github.com/apache/superset/pull/39839">apache/superset #39839</a></p></li><li><p><a href="https://docs.gitlab.com/charts/installation/migration/bundled_chart_migration/">GitLab: migrate from the bundled Redis, PostgreSQL, and MinIO charts</a></p></li><li><p><a href="https://github.com/langfuse/langfuse-k8s/pull/388">langfuse/langfuse-k8s #388</a></p></li></ul><h2>The recipe was never the expensive part</h2><p>Bitnami's Dockerfiles still sit in bitnami/containers under Apache 2.0, with commits from this week. Anyone can build them. Broadcom's complaint was about running "a build pipeline and OCI registry for the general public", and the vendors doing that for free in 2026 have something else to sell next to it. Docker has subscriptions and Red Hat has RHEL; Minimus had only the images.</p><p>Until somebody opens the next PR, Airflow's chart will keep pulling a Postgres build from November 2023.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://podostack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://podostack.com/subscribe?"><span>Subscribe now</span></a></p><p>Questions? Feedback? Reply to this email. I actually read them.</p><ul><li><p>Ilia</p></li></ul>]]></content:encoded></item><item><title><![CDATA[subPath: the ConfigMap update 7 of our 12 pods never saw]]></title><description><![CDATA[subPath bind mounts, ..data symlink swaps, kubelet sync period, immutable ConfigMaps, Reloader]]></description><link>https://podostack.com/p/configmap-subpath-never-updates</link><guid isPermaLink="false">https://podostack.com/p/configmap-subpath-never-updates</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Wed, 16 Sep 2026 14:03:40 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!7JM6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7JM6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7JM6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!7JM6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!7JM6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!7JM6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7JM6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/acc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7JM6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!7JM6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!7JM6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!7JM6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc09751-e1aa-4982-9275-1d37a265e4ef_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>On Tuesday, September 1, we raised one number in the checkout-api ConfigMap. upstream_timeout went from 2s to 5s, because a payment provider's p99 had crept up to 3.1 seconds. Twelve replicas. The service has had hot reload since 2024, so nobody restarted anything. Argo CD showed Synced, and the ConfigMap in the cluster said 5s.</p><p>By Wednesday morning timeout errors were down from about 410 an hour to about 240. I took that as the provider still being slow and moved on.</p><p>On Friday a node pool upgrade drained every node the service ran on. All twelve pods came back elsewhere, and timeout errors fell to 9 an hour. Three days of a config change that had "rolled out" and mostly hadn't.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://podostack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Podo Stack is free. Subscribe to get every issue and deep dive by email.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Five pods had crashed, and those five got the new value</h2><p>On Monday I split the error graph per pod. From Tuesday night on, seven pods produced every timeout and five produced none. Those five all had a restart count of 1 or 2, OOMKilled on Tuesday night by a memory limit set too tight months earlier. The seven that never restarted had been reading 2s all along.</p><p>Then I opened the chart. It mounted the ConfigMap with <code>subPath: app.yaml</code> at /etc/checkout/app.yaml, because the image ships other files in /etc/checkout that a directory mount would hide. The docs cover this in one note: "A container using a ConfigMap as a subPath volume mount will not receive ConfigMap updates." I'd read that page before. It hadn't stuck.</p><h3>Links</h3><ul><li><p><a href="https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically">Kubernetes docs: Mounted ConfigMaps are updated automatically</a></p></li><li><p><a href="https://kubernetes.io/docs/concepts/storage/volumes/#using-subpath">Kubernetes docs: Using subPath</a></p></li></ul><h2>What the kubelet writes into a ConfigMap volume</h2><p>I reproduced it in staging: a throwaway pod mounting the same ConfigMap as a directory, next to a checkout-api pod with the subPath mount. I bumped the timeout from 5s to 7s. 71 seconds later the probe pod saw 7s, and the app pod still read 5s.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lj1_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lj1_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 424w, https://substackcdn.com/image/fetch/$s_!lj1_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 848w, https://substackcdn.com/image/fetch/$s_!lj1_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!lj1_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lj1_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png" width="1832" height="1008" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1008,&quot;width&quot;:1832,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lj1_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 424w, https://substackcdn.com/image/fetch/$s_!lj1_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 848w, https://substackcdn.com/image/fetch/$s_!lj1_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!lj1_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52564796-cf57-440d-8131-7947ef2558a4_1832x1008.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><pre><code>$ kubectl -n stg exec cfg-probe -- ls -la /cfg
total 12
drwxrwxrwx 3 root root 4096 Sep  7 10:14 .
drwxr-xr-x 1 root root 4096 Sep  7 10:12 ..
drwxr-xr-x 2 root root 4096 Sep  7 10:14 ..2026_09_07_10_14_31.3920571846
lrwxrwxrwx 1 root root   32 Sep  7 10:14 ..data -&gt; ..2026_09_07_10_14_31.3920571846
lrwxrwxrwx 1 root root   15 Sep  7 10:12 app.yaml -&gt; ..data/app.yaml</code></pre><p>Nothing in that directory is a plain file. app.yaml links to <code>..data/app.yaml</code>, and ..data links to a timestamped directory holding the real bytes. On an update, the kubelet's AtomicWriter writes the new payload into a fresh timestamped directory, points a ..data_tmp symlink at it and renames that over ..data, so a reader never catches half a file. Then the old directory gets deleted.</p><p>A subPath mount never looks at ..data again. At container start the kubelet runs the path through filepath.EvalSymlinks, lands on the real file inside the timestamped directory and bind-mounts that one file into the container. The swap moves ..data later, and the bind mount keeps holding the old file - deleted from the volume, still readable through the mount. A container restart makes the kubelet resolve the path again, see a different inode and remount (PR #89629, in since v1.19), which is why our OOMKilled pods were the lucky ones. Env vars from a ConfigMap are also read once, at start.</p><p>Our 71 seconds fit the documented formula: kubelet sync period plus cache propagation delay. Defaults are Watch for <code>configMapAndSecretChangeDetectionStrategy</code> and 1m for <code>syncFrequency</code>, and the pod worker requeues each pod with a 0.5 jitter factor, so a running pod gets re-synced roughly every 60 to 90 seconds.</p><h3>Links</h3><ul><li><p><a href="https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/util/atomic_writer.go">kubelet: atomic_writer.go</a></p></li><li><p><a href="https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/util/subpath/subpath_linux.go">kubelet: subpath_linux.go</a></p></li><li><p><a href="https://github.com/kubernetes/kubernetes/pull/89629">kubernetes/kubernetes PR #89629: subpath ConfigMap mount on container restart</a></p></li><li><p><a href="https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/">Kubelet configuration reference (v1beta1)</a></p></li></ul><h2>A CVE lived in the same resolve-then-mount step</h2><p>I only found this one in the issue tracker while reading up on subpath_linux.go. CVE-2021-25741 was a symlink exchange in that code path: a container's subPath mounts could reach files outside the volume, host filesystem included, which gave hostPath-like access on clusters that had banned hostPath. It was rated High and fixed in v1.22.2, v1.21.5, v1.20.11 and v1.19.15, the second subPath symlink CVE after CVE-2017-1002101. Today's kubelet opens the subpath safely and bind-mounts <code>/proc/&lt;kubelet pid&gt;/fd/&lt;fd&gt;</code> instead of a path the container could swap.</p><h3>Links</h3><ul><li><p><a href="https://github.com/kubernetes/kubernetes/issues/104980">kubernetes/kubernetes #104980: CVE-2021-25741</a></p></li><li><p><a href="https://github.com/kubernetes/kubernetes/issues/60813">kubernetes/kubernetes #60813: CVE-2017-1002101</a></p></li></ul><h2>What we changed</h2><p>checkout-api lost its subPath. The ConfigMap now mounts as a directory at /etc/checkout/conf.d/, with the config flag pointing there. Its hot reload is viper's WatchConfig, which watches the parent directory and re-checks the resolved symlink target on every event (the source comment names "k8s ConfigMap replacement"). Three edits in staging gave three reloads.</p><p>The ledger service was worse off. Its hand-rolled reload put an fsnotify watch on the file path, and inotify follows symlinks, so the watch sat on the real file inside the timestamped directory. The first swap deleted that directory and the watch with it, and no second update ever arrived. We moved the watch to the directory and re-read on any event touching ..data, which is what the AtomicWriter comment suggests.</p><p>Services with no reload got a <code>checksum/config</code> annotation on the pod template, so a ConfigMap edit changes the template and the Deployment rolls normally. Our two third-party charts went under Stakater's Reloader (10.4k stars, v1.4.22 on September 9) with reloader.stakater.com/auto set to "true".</p><p>ConfigMaps from Kustomize's configMapGenerator already carry a content-hash suffix, so those got <code>immutable: true</code>. The docs sell it on performance: watches on immutable objects get closed, which counts at tens of thousands of ConfigMap-to-pod mounts (<a href="https://podostack.com/p/k8s-api-server-watch-cache">more on what watches cost the apiserver</a>). We're nowhere near that. I wanted the next in-place edit to fail at apply time, loudly.</p><h3>The honest part</h3><p>Hot reload through a directory mount skips the rollout. When I pushed a bad 50ms timeout on purpose to staging, all four replicas were serving with it 80 seconds later. A checksum rollout would have gone pod by pod under maxUnavailable. That route is slower and noisier, and I trust it more for anything that can break request handling.</p><h3>Links</h3><ul><li><p><a href="https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments">Helm: Automatically roll Deployments</a></p></li><li><p><a href="https://github.com/stakater/Reloader">GitHub: stakater/Reloader</a></p></li><li><p><a href="https://kubernetes.io/docs/concepts/configuration/configmap/#configmap-immutable">Kubernetes docs: Immutable ConfigMaps</a></p></li><li><p><a href="https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well">fsnotify: watching a file doesn't work well</a></p></li></ul><h2>Where it landed</h2><p>On September 10 we changed upstream_timeout again. The per-pod error graph moved as one line this time, and all twelve pods logged the reload within 88 seconds of the Argo CD sync.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://podostack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://podostack.com/subscribe?"><span>Subscribe now</span></a></p><p>Questions? Feedback? Reply to this email. I actually read them.</p><ul><li><p>Ilia</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Issue #035 - HolmesGPT, Job retries, and the IfNotPresent hole from 2015]]></title><description><![CDATA[An incident agent that is read-only until someone says yes, one retry budget for a thousand shards, the tenancy tool that changes your namespace count, and the kubelet fix for pod B]]></description><link>https://podostack.com/p/issue-035-holmesgpt-job-retries-private-image</link><guid isPermaLink="false">https://podostack.com/p/issue-035-holmesgpt-job-retries-private-image</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Tue, 15 Sep 2026 14:03:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6nPx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6nPx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6nPx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!6nPx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!6nPx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!6nPx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6nPx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6nPx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!6nPx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!6nPx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!6nPx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b89cfb0-769c-4b6e-9879-9b164b20251a_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Capsule's API description says a tenant quota's hard limit "is never crossed." The controller behind it sums usage on a reconcile loop and rewrites each namespace's limit to whatever headroom is left, so between two passes every namespace sees the same leftover. I only found that because I stopped at the sentence and opened the code.</p><p>It happened again in all four blocks this week. Grab bag, round three.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://podostack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Podo Stack is free. Subscribe to get every issue and deep dive by email.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>&#128640; Sandbox Watch: HolmesGPT</h2><h3>What it is</h3><p>An open-source agent for incident investigation. You ask "why is ledger crashlooping?", it queries your cluster and observability stack, then writes up a root cause. Robusta started it in May 2024 and donated it to the CNCF, where the Sandbox vote passed on October 8, 2025. The repo moved from robusta-dev to its own HolmesGPT org. Apache 2.0, about 3,350 stars, release 0.41.0 on September 8.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tTpz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tTpz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 424w, https://substackcdn.com/image/fetch/$s_!tTpz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 848w, https://substackcdn.com/image/fetch/$s_!tTpz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 1272w, https://substackcdn.com/image/fetch/$s_!tTpz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tTpz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png" width="1832" height="1322" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1322,&quot;width&quot;:1832,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tTpz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 424w, https://substackcdn.com/image/fetch/$s_!tTpz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 848w, https://substackcdn.com/image/fetch/$s_!tTpz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 1272w, https://substackcdn.com/image/fetch/$s_!tTpz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16dd2ce6-906f-4f9f-9af8-759c29e52119_1832x1322.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>How it works</h3><p>An agentic loop, where the model reads each tool result before choosing its next call. Built-in toolsets reach Kubernetes, Prometheus, Loki, Tempo, Datadog, ArgoCD and dozens more, plus MCP servers. Runbooks are now called skills, markdown procedures synced from git. Models go through LiteLLM, with OpenAI as the default and Anthropic, Bedrock or Ollama a flag away. It ships as a CLI, a Helm-deployed HTTP API, a k9s plugin, or a Slack bot via Robusta.</p><h3>The honest part</h3><p>"By design, HolmesGPT has read-only access," says the README. I went looking for where that gets enforced. The Kubernetes toolset skips secrets, and the Helm chart's ClusterRole grants none. A bash toolset ships enabled too, though, with "kubectl get" in its core allowlist and an empty default deny list. On the CLI it runs under your kubeconfig, so a secret read passes whenever your RBAC permits it. Anything off the list stops at a "Do you want to proceed?" prompt, which <code>--bash-always-allow</code> removes. Since February a Remediation MCP server can restart, scale, drain and patch, each change human-approved. That's the policy question from <a href="https://podostack.com/p/issue-034-gonzo-cloudevents-container-boundary">issue #034</a>, inside an SRE tool.</p><p>Every log line the loop pulls lands in the model's context, off your network with a hosted provider. Ollama support is labeled experimental, with tool calling that "may produce inconsistent results."</p><p>Accuracy, from the project's own August 5 benchmark: seven models passed between 71% and 94% of 63 runs, and five scored 50% on tests tagged "hard". A wrong answer prints under the same "AI:" header as a right one.</p><h3>Where I'd use it</h3><p>k8sgpt, Sandbox since December 2023, finds problems with Go analyzers and calls an LLM only for <code>--explain</code>. HolmesGPT lets the model choose what to query, so it reaches further and guesses more. I'd take it for the first ten minutes at 3 a.m., read its conclusion as a list of places to look, and keep <code>--bash-always-allow</code> away from any admin kubeconfig.</p><h3>Links</h3><ul><li><p><a href="https://github.com/HolmesGPT/holmesgpt">GitHub: HolmesGPT/holmesgpt</a></p></li><li><p><a href="https://holmesgpt.dev/latest/data-sources/builtin-toolsets/bash/">HolmesGPT docs: Bash toolset allowlists and approval</a></p></li><li><p><a href="https://holmesgpt.dev/latest/development/evaluations/latest-results/">HolmesGPT benchmark results</a></p></li><li><p><a href="https://github.com/k8sgpt-ai/k8sgpt">GitHub: k8sgpt-ai/k8sgpt</a></p></li></ul><div><hr></div><h2>&#128142; The Hidden Gem: backoffLimitPerIndex</h2><h3>What it is</h3><p>A retry budget per index for Indexed Jobs. <code>backoffLimitPerIndex</code> and <code>maxFailedIndexes</code> went GA in Kubernetes 1.33 and have been on by default since 1.29. Around them the Job API kept growing: podFailurePolicy reached GA in 1.31, successPolicy in 1.33, podReplacementPolicy in 1.34, managedBy in 1.35. The retry knob most people remember is still the original one, <code>backoffLimit</code>, default 6.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!btY0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!btY0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 424w, https://substackcdn.com/image/fetch/$s_!btY0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 848w, https://substackcdn.com/image/fetch/$s_!btY0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 1272w, https://substackcdn.com/image/fetch/$s_!btY0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!btY0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png" width="1832" height="1188" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1188,&quot;width&quot;:1832,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!btY0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 424w, https://substackcdn.com/image/fetch/$s_!btY0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 848w, https://substackcdn.com/image/fetch/$s_!btY0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 1272w, https://substackcdn.com/image/fetch/$s_!btY0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa352a5a8-2b70-49d5-a60c-93ae3dcc1889_1832x1188.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>One budget for a thousand shards</h3><p>Picture a 1000-shard Indexed Job with that setting. The controller compares the 6 against status.failed for the whole Job, a counter that doesn't reset when other shards succeed, and fails the Job on the seventh failed pod from any index. Three crashes on shard 7 plus four pods caught in a node drain will do it. Running pods get deleted, unstarted indexes never get a pod, and nothing retries the Job afterwards.</p><p>Drains count immediately. Without a pod failure policy, a pod is treated as failed the moment it gets a deletionTimestamp, even if it would have exited 0 inside its grace period.</p><p>The docs say retry backoff (10s, doubling) caps at six minutes. I checked pkg/controller/job, and the cap constant has been <code>10 * time.Minute</code> since 1.28.</p><h3>What per-index changes</h3><p>Each index gets its own failure counter and its own backoff clock. A shard that burns through its budget lands in <code>status.failedIndexes</code> while the other 999 keep going. Once failed indexes go past maxFailedIndexes, the controller terminates the Job. Under that cap it still ends Failed, reason FailedIndexes, but with 997 shards done and three index numbers to rerun.</p><p>An Ignore rule on the <code>DisruptionTarget</code> pod condition keeps evictions and preemptions off the count, and FailIndex gives up on an index at once on an exit code reserved for bad input.</p><pre><code>apiVersion: batch/v1
kind: Job
metadata:
  name: reindex-shards
spec:
  completionMode: Indexed
  completions: 1000
  parallelism: 50
  # no backoffLimit here: with backoffLimitPerIndex set it defaults to 2147483647
  backoffLimitPerIndex: 2      # third failed pod marks this index failed
  maxFailedIndexes: 20         # 21st failed index terminates the Job
  podFailurePolicy:
    rules:
    - action: Ignore           # drain, preemption, taint eviction, node pressure
      onPodConditions:
      - type: DisruptionTarget
    - action: FailIndex        # bad input for this shard, retrying won't help
      onExitCodes:
        containerName: worker
        operator: In
        values: [42]
  template:
    spec:
      restartPolicy: Never     # required by both backoffLimitPerIndex and podFailurePolicy
      containers:
      - name: worker
        image: registry.example.com/reindex:1.8.2
        args: ["--shard=$(JOB_COMPLETION_INDEX)"]</code></pre><h3>Where it still bites</h3><p>Leftover backoffLimit in the manifest still wins. The controller checks the Job-wide limit first, so a chart that templates <code>backoffLimit: 6</code> onto every Job brings the shared budget back.</p><p>Ignoring DisruptionTarget also swallows kubelet node-pressure evictions. If a shard pushes its node into memory pressure, it gets rescheduled with no retry limit unless activeDeadlineSeconds is set. An OOM kill against the container's own memory limit still counts.</p><p>kubectl describe job prints Completed Indexes and nothing for failed ones; those live only in status.failedIndexes.</p><p>Version lag is small. The oldest EKS release still in extended support is 1.31, and upstream every field above is on by default there. managedBy is the exception, alpha and off until 1.32.</p><h3>Links</h3><ul><li><p><a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/#backoff-limit-per-index">Kubernetes docs: Jobs - backoff limit per index</a></p></li><li><p><a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-apps/3850-backoff-limits-per-index-for-indexed-jobs">KEP-3850: Backoff Limits Per Index For Indexed Jobs</a></p></li><li><p><a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-apps/3329-retriable-and-non-retriable-failures">KEP-3329: Retriable and non-retriable Pod failures for Jobs</a></p></li><li><p><a href="https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/job_controller.go">pkg/controller/job/job_controller.go</a></p></li><li><p><a href="https://podostack.com/p/cronjob-100-missed-schedules-fixed-in-123">Podo Stack: CronJob - the 100-missed-schedules bug was fixed in 1.23</a></p></li></ul><div><hr></div><h2>&#9876;&#65039; The Showdown: Only one of these tenancy tools changes your namespace count</h2><h3>The setup</h3><p>I went to pull the Hierarchical Namespace Controller into this comparison and found a read-only repo under kubernetes-retired. SIG Auth voted in February 2025 to archive it "due to a lack of maintainers and adopters".</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1xyJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1xyJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 424w, https://substackcdn.com/image/fetch/$s_!1xyJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 848w, https://substackcdn.com/image/fetch/$s_!1xyJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 1272w, https://substackcdn.com/image/fetch/$s_!1xyJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1xyJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png" width="1832" height="964" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:964,&quot;width&quot;:1832,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1xyJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 424w, https://substackcdn.com/image/fetch/$s_!1xyJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 848w, https://substackcdn.com/image/fetch/$s_!1xyJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 1272w, https://substackcdn.com/image/fetch/$s_!1xyJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e03ac73-5b6e-4d27-8feb-356158211abc_1832x964.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>How each one draws the line</h3><p><strong>Accurate</strong> copies objects down a tree. Annotate a Secret or Role with <code>accurate.cybozu.com/propagate</code> and it lands in the sub-namespaces below. Budgets are out of scope. The sample config even lists ResourceQuota as a propagatable kind, and a copied quota gives each child the full limit. v1.9.0 in June was mostly Renovate bumps. 79 stars.</p><p><strong>Capsule</strong> is the governance layer: a Tenant owns a capped set of namespaces and pins allowed registries and storage classes. It has sat in CNCF Sandbox since December 2022 and has 2,175 stars; v0.14.5 shipped last Friday. The tenant quota's API description promises the hard limit "is never crossed", so I read the controller. The legacy <code>spec.resourceQuotas</code> is a reconcile loop that sums usage and rewrites each namespace's hard to the leftover headroom. Between passes, every namespace sees the same leftover. v0.14.0 (August 21) added GlobalResourceQuota, which reserves usage atomically at admission, and the old quota system is now deprecated.</p><p>With <strong>vCluster</strong>, the tenant stops being a namespace. Each one gets its own API server, OSS under Apache 2.0. Embedded etcd and Private Nodes need the Free tier, which validates its license against vCluster Platform. v0.37.1 landed yesterday, 11,298 stars.</p><h3>The #023 cost angle</h3><p>In <a href="https://podostack.com/p/issue-023-namespaces-cost-at-scale">#023</a> the bill scaled with namespace count. Capsule and Accurate leave that count where it was, and Accurate inflates the object count on top: one Secret propagated over 40 sub-namespaces is 41 Secrets in every cluster-wide Secret informer.</p><p>vCluster moves the line. Tenant namespaces and CRDs stay in the virtual control plane; the syncer pushes pods, services, endpoints and PVCs, plus only the ConfigMaps and Secrets pods reference, into one host namespace per tenant.</p><p>The honest part: pods still run on host nodes, so pod-watching DaemonSets see every one of them. And the bill moves into control planes. The chart default asks for 200m CPU and 256Mi, but the sizing guide recommends 4 CPU and 8 GiB per replica, three replicas, for production. Fifty tenants at that profile is 600 CPU in limits.</p><h3>My read</h3><p><strong>Trusted teams sharing Secrets and RBAC</strong></p><ul><li><p>Accurate. Opt-in copying, nothing more.</p></li></ul><p><strong>Internal platform with real per-team budgets</strong></p><ul><li><p>Capsule 0.14+, shared limits in GlobalResourceQuota.</p></li></ul><p><strong>Namespace count already hurting, or tenants need their own CRDs</strong></p><ul><li><p>vCluster, after pricing the control planes.</p></li></ul><p><strong>Still on HNC</strong></p><ul><li><p>Archived since April 2025. The archive thread points at Accurate.</p></li></ul><h3>Links</h3><ul><li><p><a href="https://projectcapsule.dev/docs/resource-management/globalresourcequota/">Capsule: GlobalResourceQuota</a></p></li><li><p><a href="https://github.com/cybozu-go/accurate">GitHub: cybozu-go/accurate</a></p></li><li><p><a href="https://www.vcluster.com/docs/vcluster/deploy/control-plane/sizing-and-performance">vCluster: control plane sizing and performance</a></p></li><li><p><a href="https://github.com/kubernetes/org/issues/5484">kubernetes/org #5484: archiving HNC</a></p></li><li><p><a href="https://podostack.com/p/issue-023-namespaces-cost-at-scale">Podo Stack #023: Namespaces aren't free</a></p></li></ul><div><hr></div><h2>&#128110; The Policy: IfNotPresent let pod B run an image it had no right to pull</h2><h3>Pod B never showed a credential</h3><p>Pod A in team-a runs registry.example.com/private/app:1.4.2 with an imagePullSecret. Later the scheduler drops pod B from team-b onto the same node - same image, no imagePullSecrets, default <code>IfNotPresent</code>. The kubelet asks the runtime whether the image is on disk, hears yes, and logs "Container image ... already present on machine". No registry call, so no credential check. kubernetes/kubernetes#18787 reported this in December 2015 and closed in April 2025.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UUKf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UUKf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 424w, https://substackcdn.com/image/fetch/$s_!UUKf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 848w, https://substackcdn.com/image/fetch/$s_!UUKf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 1272w, https://substackcdn.com/image/fetch/$s_!UUKf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UUKf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png" width="1832" height="1188" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1188,&quot;width&quot;:1832,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UUKf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 424w, https://substackcdn.com/image/fetch/$s_!UUKf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 848w, https://substackcdn.com/image/fetch/$s_!UUKf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 1272w, https://substackcdn.com/image/fetch/$s_!UUKf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3100f8f-5ffa-44a4-8db2-61287788ce4d_1832x1188.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The archive note behind this block forced Always through a Kyverno ClusterPolicy and promised "no limitations." Neither half holds up in 2026.</p><h3>What the fixes cost</h3><p><strong>AlwaysPullImages.</strong> An admission plugin, off by default, that sets Always on every Pod and rejects anything else. For an unchanged image the pull is a manifest check measured in kilobytes, per KEP-2535. The bill is availability: the kubelet re-checks on every container start, CrashLoopBackOff restarts included, so a registry outage stops pods whose bytes already sit on the node.</p><p><strong>KEP-2535, inside the kubelet.</strong> The <code>KubeletEnsureSecretPulledImages</code> gate went alpha in v1.33 and flipped to beta, on by default, in v1.35. v1.37 still ships it as beta. Every kubelet pull leaves a record under <code>/var/lib/kubelet/image_manager/</code> with the secret's namespace, name, UID and credential hash, kept across restarts. Pod B brings no matching credential, so the kubelet pulls for real and the private registry refuses it. A pod reusing a known secret skips the registry.</p><p>The policy kind is on its way out too: Kyverno 1.19 deprecated ClusterPolicy on August 20, and 1.20 removes it.</p><h3>The manifests</h3><p>For 1.35+ nodes whose kubelet config you own:</p><pre><code>apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
# NeverVerify | NeverVerifyPreloadedImages (default)
# NeverVerifyAllowlistedImages | AlwaysVerify
imagePullCredentialsVerificationPolicy: NeverVerifyAllowlistedImages
preloadedImagesVerificationAllowlist:
  - registry.k8s.io/*   # public only: allowlisted images skip verification entirely</code></pre><p>For nodes you can't configure, or older than 1.35, force Always for the private prefix and nothing else. MutatingAdmissionPolicy went GA in v1.36, so there's no webhook to run:</p><pre><code>apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
  name: private-images-always-pull
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE"]
      resources: ["pods"]
  failurePolicy: Fail
  reinvocationPolicy: IfNeeded
  mutations:
  - patchType: ApplyConfiguration
    applyConfiguration:
      expression: &gt;
        Object{
          spec: Object.spec{
            containers: object.spec.containers
              .filter(c, c.image.startsWith("registry.example.com/"))
              .map(c, Object.spec.containers{name: c.name, imagePullPolicy: "Always"})
          }
        }
  - patchType: ApplyConfiguration
    applyConfiguration:
      expression: &gt;
        Object{
          spec: Object.spec{
            initContainers: object.spec.?initContainers.orValue([])
              .filter(c, c.image.startsWith("registry.example.com/"))
              .map(c, Object.spec.initContainers{name: c.name, imagePullPolicy: "Always"})
          }
        }
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
  name: private-images-always-pull
spec:
  policyName: private-images-always-pull</code></pre><p>I ran it against a v1.37.0 kube-apiserver: both private containers came back Always, and an nginx:1.27 sidecar kept IfNotPresent. Kyverno 1.18+ takes the same mutations block in a policies.kyverno.io/v1 MutatingPolicy.</p><h3>What stays open</h3><p>Under the default policy, any image the kubelet didn't pull itself counts as preloaded and stays open to every pod on the node. That includes crictl-based warmers like the DaemonSet in <a href="https://podostack.com/p/image-preload-operator-zero-cold-start">#020</a>, and everything already on disk when the gate first turned on. An in-place upgrade from 1.34 leaves a node full of those. <code>AlwaysVerify</code> closes the gap and puts the registry back in front of them.</p><p>GKE's node system config has no field for this policy; EKS AL2023 nodes take raw KubeletConfiguration through nodeadm. Records don't expire either: revoke a registry token and pods referencing that same secret keep starting the cached image until image GC removes it.</p><h3>Links</h3><ul><li><p><a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2535-ensure-secret-pulled-images">KEP-2535: Ensure Secret Pulled Images</a></p></li><li><p><a href="https://kubernetes.io/docs/concepts/containers/images/#ensureimagepullcredentialverification">Kubernetes docs: Ensure image pull credential verification</a></p></li><li><p><a href="https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages">Kubernetes docs: AlwaysPullImages admission controller</a></p></li><li><p><a href="https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/">Kubernetes docs: Mutating Admission Policy</a></p></li><li><p><a href="https://kyverno.io/docs/policy-types/overview/">Kyverno: policy types and the ClusterPolicy deprecation</a></p></li></ul><div><hr></div><p>Issue #18787 went up on GitHub in December 2015 and was closed in April 2025. The default that shuts the gap shipped with v1.35 that December, ten years after the report. It covers images the kubelet pulled itself, so a crictl preloader like the one in <a href="https://podostack.com/p/image-preload-operator-zero-cold-start">#020</a> still leaves pod B a way in.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://podostack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://podostack.com/subscribe?"><span>Subscribe now</span></a></p><p>Questions? Feedback? Reply to this email. I actually read them.</p><ul><li><p>Ilia</p></li></ul>]]></content:encoded></item><item><title><![CDATA[The rolling update myth: your frontend is talking to next week's backend]]></title><description><![CDATA[version skew, session stickiness, deployment pinning, Gateway API, skew protection]]></description><link>https://podostack.com/p/version-skew-deployment-pinning</link><guid isPermaLink="false">https://podostack.com/p/version-skew-deployment-pinning</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Fri, 11 Sep 2026 14:02:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!yjK6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yjK6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yjK6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!yjK6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!yjK6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!yjK6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yjK6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yjK6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!yjK6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!yjK6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!yjK6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd88f27-2048-41b2-8eb2-a89dffad1adb_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A user loads your app at 2:47pm. Their browser caches the JavaScript bundle - React Server Component payloads, API client code, the works. At 2:52pm, your rolling update finishes: new backend pods are live, old ones are gone. The user, still on the same tab, clicks something. Their cached frontend, built for deployment N, calls an API shaped for deployment N+1.</p><p>Sometimes that's a 500. Sometimes it's a silent RSC hydration failure. Other times it's a contract mismatch that only shows up as "weird" data three clicks later. Nobody on call connects it to the deploy, because the deploy finished five minutes ago and "succeeded."</p><p>Rolling updates are safe for the backend. They say nothing about whether the client talking to that backend is the version you think it is.</p><h2>Why this is worse in full-stack frameworks</h2><p>Any API can drift out of sync with an old client for a few seconds during a rollout - that's not new. What's new is how tightly coupled frontend and backend have become in frameworks like Next.js, Remix, SvelteKit, and Nuxt: shared types, server components whose payload shape <em>is</em> the contract, code that assumes the two sides were built together because, until a rolling update happens, they were.</p><p>And the window isn't seconds anymore. A user's session - and the JS bundle their browser is holding onto - can outlive your rolling update by minutes, hours, sometimes days, depending on caching and how long they leave the tab open. "The rollout only takes two minutes" doesn't help if the client that matters loaded ten minutes before it started.</p><h2>What's actually going on: version skew</h2><p>This has a name - version skew - and once you know to look for it, the fix is controlling which backend version a given client session actually talks to, for as long as that session lives, not shipping rollouts faster.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!37UK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!37UK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 424w, https://substackcdn.com/image/fetch/$s_!37UK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 848w, https://substackcdn.com/image/fetch/$s_!37UK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 1272w, https://substackcdn.com/image/fetch/$s_!37UK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!37UK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png" width="1752" height="874" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:874,&quot;width&quot;:1752,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!37UK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 424w, https://substackcdn.com/image/fetch/$s_!37UK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 848w, https://substackcdn.com/image/fetch/$s_!37UK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 1272w, https://substackcdn.com/image/fetch/$s_!37UK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbac70eb9-ebd9-4e1b-8865-e4c64dd747ae_1752x874.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Vercel shipped this first, for Next.js specifically: Skew Protection. A deployment ID gets attached to the client, carried via a cookie (<code>__vdpl</code>), and every subsequent request from that session gets routed back to the exact deployment that originally served it - not whatever's currently live. The session stays pinned to one version until its own natural end, regardless of how many deploys happen underneath it.</p><h3>Links</h3><ul><li><p><a href="https://vercel.com/blog/version-skew-protection">Vercel: Introducing Skew Protection</a></p></li><li><p><a href="https://vercel.com/docs/skew-protection">Vercel: Skew Protection docs</a></p></li></ul><h2>Doing it on plain Kubernetes</h2><p>Vercel's version is a managed platform feature. The concept ports to vanilla Kubernetes without needing that platform, using pieces you likely already have: a session cookie that carries a deployment identifier, Gateway API routing rules that read that cookie and send the request to the matching backend version, and a small state machine tracking each deployment as Active, Draining, or Expired so you know when it's finally safe to tear an old version down.</p><p>None of this replaces your rollout strategy - canary, blue/green, whatever you're already doing. Skew protection sits on top of it. The rollout question is "how do I move traffic to the new version safely." The skew question is a separate one: "once a specific user's session started on version N, does it stay on version N until that session naturally ends."</p><h2>The trade-off that makes this non-trivial</h2><p>Pinning sessions to versions means old versions have to keep running, potentially for as long as your longest-lived session - which means running N versions of your backend simultaneously instead of one, with memory, replica count, and infrastructure cost scaling with N. Observability gets messier too: every metric now needs a version label, or your dashboards quietly average together three different backends and tell you nothing useful.</p><p>It's also not a fit for everything. A clean backend API with no shared types and no client-side caching to speak of mostly doesn't have this problem in the first place - skew protection is solving a specific coupling, not a general Kubernetes deployment concern.</p><h2>When to reach for this</h2><p>Anywhere you don't control the client cache - mobile apps that don't force-refresh, CDN edge caching, long-lived SPA sessions - rolling updates alone aren't enough, and the fix looks more like blue/green scoped to the session than a faster rollout. It's the failure mode that falls squarely between the frontend team and the platform team, which is usually why it ships unfixed: each side assumes the other one owns it.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><p>- Ilia</p>]]></content:encoded></item><item><title><![CDATA[The scheduler that starts 7 of your 8 GPU pods, then bills you for all 8]]></title><description><![CDATA[partial scheduling, gang scheduling, Kueue ClusterQueue/LocalQueue, topology-aware placement]]></description><link>https://podostack.com/p/kueue-gang-scheduling</link><guid isPermaLink="false">https://podostack.com/p/kueue-gang-scheduling</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Wed, 09 Sep 2026 14:02:52 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!LaiI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LaiI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LaiI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!LaiI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!LaiI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!LaiI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LaiI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LaiI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!LaiI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!LaiI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!LaiI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fead44f97-0379-4635-9f10-6efb1d12e843_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A distributed training job asks for eight GPU pods. Seven schedule within seconds. The eighth sits in Pending - one node short of capacity somewhere in the cluster - and stays there. Nothing crashes. Nothing errors. The training process on all seven running pods just hangs, because the all-reduce step that synchronizes gradients across the job is waiting for a peer that never showed up.</p><p>Seven GPUs, burning money, doing nothing, for however long it takes someone to notice and kill the job.</p><p>The default Kubernetes scheduler did exactly what it was built to do: schedule pods, one at a time, as capacity allows. That's precisely the problem.</p><h2>Why this isn't a <code>podAffinity</code> problem</h2><p>The instinct is to reach for <code>podAffinity</code> or <code>topologySpreadConstraints</code> to keep the job's pods close together. Neither one touches the actual failure. Both operate per-pod, evaluated independently as each pod is scheduled - they can influence <em>where</em> a pod lands, but nothing about them makes eight pods land <em>together or not at all</em>. The scheduler still admits each pod as its own decision, and a job that needs all eight can still end up with seven.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ieIg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ieIg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 424w, https://substackcdn.com/image/fetch/$s_!ieIg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 848w, https://substackcdn.com/image/fetch/$s_!ieIg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 1272w, https://substackcdn.com/image/fetch/$s_!ieIg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ieIg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png" width="1792" height="1232" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1232,&quot;width&quot;:1792,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ieIg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 424w, https://substackcdn.com/image/fetch/$s_!ieIg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 848w, https://substackcdn.com/image/fetch/$s_!ieIg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 1272w, https://substackcdn.com/image/fetch/$s_!ieIg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf280352-d613-4a1b-9270-3905a31278b2_1792x1232.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Distributed training, MPI jobs, Spark executors that all need to see each other before work starts - none of these have a "partial success" mode. A job that gets 7 of 8 pods isn't 87% done. It's 0% done, at full cost.</p><h2>Gang scheduling: the actual fix</h2><p>Gang scheduling flips the admission decision from per-pod to per-job: either every pod in the group gets scheduled together, or none of them do, and the ones that would've scheduled successfully get held back too. HPC batch schedulers have handled this exact problem for decades; Kubernetes borrowed the concept late, and it shows up now as "all-or-nothing" semantics in a handful of projects: Kueue, Volcano, Apache YuniKorn.</p><p>Kueue is the one worth knowing first: an official Kubernetes SIG-Scheduling project (<code>kubernetes-sigs/kueue</code>), not some third-party fork of the scheduler, currently at v0.13.4 and shipping regularly.</p><h3>Links</h3><ul><li><p><a href="https://kueue.sigs.k8s.io/docs/concepts/all_or_nothing/">Kueue: All-or-nothing scheduling</a></p></li><li><p><a href="https://github.com/kubernetes-sigs/kueue">GitHub: kubernetes-sigs/kueue</a></p></li></ul><h2>How Kueue actually enforces it</h2><p>Kueue sits in front of the default scheduler as an admission layer, rather than replacing it. Jobs go into a <code>LocalQueue</code>, a namespace-scoped object your team submits work to, which references a <code>ClusterQueue</code> that owns the actual quota and fair-sharing rules across the cluster. Kueue holds a job back until it's confident the whole group can be admitted, then releases all of its pods to the underlying scheduler together.</p><p>The enforcement mechanism is <code>waitForPodsReady</code>: a cluster-wide, timeout-based check. Once Kueue admits a workload, it watches until every pod in it reports ready. If the timeout passes before that happens - because a node disappeared, or capacity got contended by something else - Kueue evicts the whole workload and requeues it, rather than leaving it half-running and silently burning resources. That's the mechanism that turns "7 of 8, hanging forever" into "0 of 8, retried automatically."</p><h3>Links</h3><ul><li><p><a href="https://kueue.sigs.k8s.io/docs/tasks/manage/setup_wait_for_pods_ready/">Kueue: setup all-or-nothing with ready Pods</a></p></li></ul><h2>Topology awareness: gang scheduling isn't enough on its own</h2><p>Getting all eight pods scheduled <em>somewhere</em> solves the all-or-nothing problem. It doesn't solve the next one: distributed training over NVLink or 400G Ethernet is latency-sensitive enough that <em>where</em> those eight pods land relative to each other changes throughput by a lot, not a little.</p><p>Kueue's topology-aware placement understands a zone-over-rack-over-host hierarchy and bin-packs a job's pods onto adjacent nodes rather than scattering them wherever capacity happens to be free. Gang scheduling gets you eight pods running together. Topology awareness gets you eight pods running together <em>fast</em>.</p><h2>This isn't only an ML problem</h2><p>The specific example is GPU training because that's where the pain is loudest right now, but the underlying failure mode - a job whose pods have to arrive together to do useful work at all - applies to any tightly-coupled distributed system: MPI jobs, Spark executor pools, anything with a synchronization barrier before real work starts. If your workload has a moment early on where every worker waits for every other worker, the default scheduler's per-pod admission model is working against you, not for you.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><p>- Ilia</p>]]></content:encoded></item><item><title><![CDATA[Issue #034 - Gonzo, CloudEvents, and the container that held while the policy didn't]]></title><description><![CDATA[A k9s-inspired log viewer, an event envelope that stops at the envelope, Kubernetes Dashboard's official pick for what comes next, and five sandbox escapes with no broken boundary]]></description><link>https://podostack.com/p/issue-034-gonzo-cloudevents-container-boundary</link><guid isPermaLink="false">https://podostack.com/p/issue-034-gonzo-cloudevents-container-boundary</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Tue, 08 Sep 2026 14:01:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!aeU2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aeU2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aeU2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!aeU2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!aeU2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!aeU2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aeU2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aeU2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!aeU2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!aeU2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!aeU2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bee2be2-d77a-45f5-b357-ad966a3cf176_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Kubernetes Dashboard is archived. Lens is a subscription. Between those two, one project ended up as the answer to both - which made this week's Showdown block write itself. That's the shape underneath all four of these: something that looked settled - a boundary, a standard, a free tool, a security model - turning out to hold less than advertised.</p><p>Grab bag, round two.</p><div><hr></div><h2>&#128640; Sandbox Watch: Gonzo</h2><h3>What it is</h3><p>A terminal log viewer built specifically for OTLP, from ControlTheory, a small observability startup. It surfaced publicly around KubeCon 2025 USA, MIT licensed, sitting around 2,800 GitHub stars now.</p><h3>How it works</h3><p>Built on Bubble Tea, Charm's TUI framework - not the tview/tcell stack k9s runs on. "Inspired by k9s" means the 2x2 grid layout and vim-style keybindings, not shared code. It runs its own OTLP receiver - gRPC on 4317, HTTP on 4318 - so logs land directly, no Loki or Elasticsearch sitting in between. It also auto-detects JSON, logfmt, and plain text, with prebuilt integrations for Kubernetes pod logs, Docker, syslog, and a long list of platforms: Vercel, Supabase, Railway, Cloudflare Workers, Netlify, Fly.io, Render, AWS CloudWatch.</p><h3>The honest part</h3><p>A Show HN commenter reported 5,000 log lines taking over six minutes to process on their machine - "so slow" was the exact complaint. Worth checking against your own log volume before you commit to it. There's also a fixed bug about returning terminal control to the parent process "(e.g. k9s)" on quit, which tells you people are already running this as a k9s plugin rather than standalone.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5gwa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5gwa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 424w, https://substackcdn.com/image/fetch/$s_!5gwa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 848w, https://substackcdn.com/image/fetch/$s_!5gwa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 1272w, https://substackcdn.com/image/fetch/$s_!5gwa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5gwa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png" width="1792" height="874" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:874,&quot;width&quot;:1792,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5gwa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 424w, https://substackcdn.com/image/fetch/$s_!5gwa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 848w, https://substackcdn.com/image/fetch/$s_!5gwa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 1272w, https://substackcdn.com/image/fetch/$s_!5gwa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c82ba0f-1a46-41ff-94ac-b48a14795150_1792x874.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And it isn't purely a community project. Gonzo shares a blog and a company with ControlTheory's paid product, an AI-focused observability platform pitched right alongside it. That doesn't make the free tool worse - it's still MIT, still yours to run - but it's worth knowing you're also looking at a funnel.</p><h3>Links</h3><ul><li><p><a href="https://github.com/control-theory/gonzo">GitHub: control-theory/gonzo</a></p></li><li><p><a href="https://controltheory.com/gonzo">Gonzo</a></p></li></ul><div><hr></div><h2>&#128142; The Hidden Gem: CloudEvents</h2><h3>What it is</h3><p>The CNCF's standard envelope for events - not a message bus, not a delivery protocol, just an agreed shape for "here's what happened." Spec v1.0.2, Graduated in the CNCF in January 2024, health score rated "Excellent." Google, Microsoft, IBM, Red Hat, VMware, SAP, and Serverless Inc. all had a hand in it - a vendor consortium, not one inventor.</p><h3>The envelope</h3><p>Four required fields: <code>id</code>, <code>source</code>, <code>specversion</code>, <code>type</code>. A handful of optional ones: <code>datacontenttype</code>, <code>dataschema</code>, <code>subject</code>, <code>time</code>. That's genuinely it - small enough to hold in your head, which is the whole point.</p><h3>Who actually uses it</h3><p>Knative Eventing is the deepest integration - Broker and Trigger are built directly around CloudEvents. Dapr wraps pub/sub messages in a CloudEvents envelope by default. AWS EventBridge and Azure Event Grid both support it too, but as an opt-in mode through API Destinations or a schema setting - not what you get by default.</p><h3>Where it stops helping</h3><p>The spec doesn't touch routing or delivery guarantees - that's left entirely to whatever's actually moving the event around. And <code>dataschema</code> is optional, which means the envelope is standardized but the payload inside it usually isn't. A consumer still can't validate a CloudEvent's actual data without a separate, out-of-band agreement on what that data looks like. CloudEvents solves "what shape is this event," not "what does it mean" or "will it arrive."</p><h3>Links</h3><ul><li><p><a href="https://cloudevents.io/">CloudEvents</a></p></li><li><p><a href="https://github.com/cloudevents/spec">GitHub: cloudevents/spec</a></p></li></ul><div><hr></div><h2>&#9876;&#65039; The Showdown: Lens went paid, Kubernetes Dashboard went dark, one project answers both</h2><h3>The setup</h3><p>Mirantis has been tightening Lens's licensing since 2023 - marketplace extensions gated, multi-cluster limited, an Enterprise tier added in 2024. Since October 1, 2024, even the free Personal plan lost team features - shared clusters and Spaces now need a Pro or Enterprise subscription.</p><h3>The one everyone still recommends, and shouldn't</h3><p>OpenLens - the open-source fork people reach for reflexively - is dead. Last release was v6.5.2 in June 2023. No cease-and-desist, no drama: once Mirantis stopped shipping an open core to fork from, contributions simply stopped. If a "just use OpenLens" comment is more than a year old, don't trust it.</p><h3>What's actually alive</h3><p>FreeLens is the real successor - a fresh community fork started in 2024/2025, sitting around 5,300 stars, cross-platform, no proprietary licensing anywhere in it. If you want the original Lens desktop experience without a subscription, this is it now, not OpenLens.</p><h3>The other half of the story</h3><p>Kubernetes Dashboard - the official web UI - was archived on January 21, 2026. The repo is read-only. And the project's own farewell note points people toward Headlamp.</p><p>Headlamp has been CNCF Sandbox since May 2023, with Microsoft engineers (formerly Kinvolk) actively maintaining it, a monthly release cadence, and both web and desktop builds behind a plugin system. It's the one project that both halves of this story - the paywalled desktop app and the abandoned official dashboard - now point toward.</p><h3>My read</h3><p>Terminal-first: k9s, untouched by any of this. Want the original Lens desktop feel for free: FreeLens. Want the option positioned to be the default going forward, with a real team behind it and a plugin ecosystem: Headlamp. Kubernetes Dashboard has dropped out of the running entirely - it's the cautionary footnote now, not a fourth option.</p><h3>Links</h3><ul><li><p><a href="https://github.com/freelensapp/freelens">GitHub: freelensapp/freelens</a></p></li><li><p><a href="https://www.cncf.io/projects/headlamp/">Headlamp on CNCF</a></p></li><li><p><a href="https://github.com/kubernetes/dashboard">GitHub: kubernetes/dashboard</a></p></li></ul><div><hr></div><h2>&#128110; The Policy: the container isn't the boundary that matters</h2><h3>The framework, and where it comes from</h3><p>"Boundary, Policy, Lifecycle" as three separate questions for AI-agent sandboxing isn't an industry standard - it's from a single blog post by Luis Cardoso, a malware-analysis researcher, published in January. Worth saying plainly, because backlog notes and secondhand summaries have a way of making a good personal framework sound like consensus. It isn't, yet. But it's a genuinely useful way to cut the problem, and this year handed it a lot of supporting evidence.</p><h3>What it says</h3><p>Three separate questions, and most sandbox security effort only answers the first. Boundary: can the code physically escape the container, VM, or process? Policy: even fully inside the boundary, what is the agent actually allowed to touch - credentials, network egress, the host filesystem through a mount? Lifecycle: what happens to that access after the task that needed it ends? Most engineering attention goes to boundary. Most real incidents turn out to be policy.</p><h3>The week that proved it</h3><p>In July, security researchers documented a string of sandbox escapes across Cursor, Codex CLI, Gemini CLI, Antigravity, and Claude Code - written up as "the week of sandbox escapes." None of them were a broken container boundary. Every one had the same shape: an agent, still fully inside its sandbox, writes a file the boundary was never designed to police - a <code>.vscode</code> task config, a <code>.claude</code> hook, a modified Python virtualenv, git metadata under a nonstandard directory name that slips past path-based rules. Something outside the sandbox later reads and executes that file, unsandboxed. CVE-2026-35603 and CVE-2026-48124 came out of that wave. Anthropic fixed its case quickly; the write-ups describe the other vendors moving slower.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MXv5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MXv5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 424w, https://substackcdn.com/image/fetch/$s_!MXv5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 848w, https://substackcdn.com/image/fetch/$s_!MXv5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 1272w, https://substackcdn.com/image/fetch/$s_!MXv5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MXv5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png" width="1752" height="964" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:964,&quot;width&quot;:1752,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MXv5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 424w, https://substackcdn.com/image/fetch/$s_!MXv5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 848w, https://substackcdn.com/image/fetch/$s_!MXv5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 1272w, https://substackcdn.com/image/fetch/$s_!MXv5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43bcdfbd-c481-44cf-929a-b13d6bee8056_1752x964.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Why the framing matters</h3><p>The container held in every one of these. Nobody broke out. The failure sat entirely in the second question - policy - which most teams never separately audit, because "we sandboxed it" already feels like the finished sentence.</p><h3>What to actually check</h3><p>Not "can the agent escape." Ask what it can still write to, reach over the network, or leave behind after the task ends, even while fully contained. That's three separate audits, not one.</p><h3>Links</h3><ul><li><p><a href="https://www.luiscardoso.dev/blog/sandboxes-for-ai">Luis Cardoso: Sandboxes for AI</a></p></li><li><p><a href="https://www.pillar.security/blog/the-week-of-sandbox-escapes">Pillar Security: The Week of Sandbox Escapes</a></p></li></ul><div><hr></div><p>Four different kinds of "that wasn't actually solid." A dashboard that just stopped existing. A standard that only settles the envelope, not the substance. A free tool that's also a sales funnel, which is fine as long as you know it going in. And a sandbox that held perfectly while the policy around it didn't.</p><p>The last one is worth sitting with longer than a headline allows. Five different AI coding tools hit the identical failure shape in the same month, and the container was never the part that broke.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><p>- Ilia</p>]]></content:encoded></item><item><title><![CDATA[The Flux Kustomization that adopted a namespace it never should have touched]]></title><description><![CDATA[path scope, targetNamespace, prune: true, atomic paths, .fluxignore]]></description><link>https://podostack.com/p/flux-kustomization-scope-bleed</link><guid isPermaLink="false">https://podostack.com/p/flux-kustomization-scope-bleed</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Fri, 04 Sep 2026 14:01:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RlHr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RlHr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RlHr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!RlHr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!RlHr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!RlHr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RlHr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RlHr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!RlHr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!RlHr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!RlHr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbaf814f-16b7-4970-b0e2-a1a32d430a5d_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Nobody touched the production Redis namespace that week. That was the whole problem with the postmortem: every line of the incident had a completely different name in it, <code>valkey-test</code>, and it still ended with resources gone from <code>redis-system</code>. Four unrelated decisions, each one reasonable on its own, lined up into a chain that let a Kustomization adopt a namespace it had never been told to manage, then delete it again on schedule, exactly as configured.</p><p>Nothing here is a Flux bug. Every step is the tool doing precisely what its config says. That's what makes it worth walking through slowly: four defaults compounding, not a single obvious mistake.</p><h2>The four ingredients</h2><p>The production Redis Kustomization watched <code>path: ./kubernetes/</code>, not <code>./kubernetes/production/redis-system/</code> - the whole tree above it, and not the directory that actually belonged to it. Flux Kustomization doesn't ask which manifests under that path belong to it, either. It takes all of them recursively, by default. Then there's <code>targetNamespace</code>, which doesn't filter what Flux picks up - it force-rewrites <code>metadata.namespace</code> on every namespaced resource it finds, regardless of what namespace the manifest author actually wrote. And <code>prune: true</code>, turned on for exactly the reason you'd turn it on: keep the cluster from accumulating dead resources nobody remembers creating.</p><p>Individually these are all sane defaults. <code>path</code> has to point somewhere. Recursion is what makes a directory of manifests useful instead of requiring one Kustomization per file. <code>targetNamespace</code> exists so you can reuse the same manifests across environments. <code>prune</code> is the thing that makes GitOps actually mean "the cluster matches Git," instead of "the cluster is a superset of Git that only grows."</p><h2>How the adoption actually happened</h2><p>Someone added a <code>kubernetes/test/</code> directory for a Valkey experiment, unrelated to the Redis rollout, just organized under the same parent tree. The Redis Kustomization's next reconciliation walked <code>./kubernetes/</code>, found the new directory, and pulled every manifest in it into its own managed set, including a namespace manifest for <code>valkey-test</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8tqw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8tqw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 424w, https://substackcdn.com/image/fetch/$s_!8tqw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 848w, https://substackcdn.com/image/fetch/$s_!8tqw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 1272w, https://substackcdn.com/image/fetch/$s_!8tqw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8tqw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png" width="1832" height="1054" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1054,&quot;width&quot;:1832,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8tqw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 424w, https://substackcdn.com/image/fetch/$s_!8tqw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 848w, https://substackcdn.com/image/fetch/$s_!8tqw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 1272w, https://substackcdn.com/image/fetch/$s_!8tqw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff76ed5ea-381a-4190-8a2b-7ffcbae09e54_1832x1054.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>targetNamespace: redis-system</code> then did exactly what it's documented to do. For cluster-scoped resources like the <code>Namespace</code> object itself, the field is a no-op: Flux just created <code>valkey-test</code> as specified, a stray but harmless namespace. For anything namespaced sitting in that same test directory (a Deployment, a Secret), <code>targetNamespace</code> would have silently rewritten its namespace from <code>valkey-test</code> to <code>redis-system</code>, moving a test workload's manifests into the production namespace without anyone editing a single YAML file to say so.</p><p>Weeks later, someone cleaned up by renaming <code>kubernetes/test/</code> to <code>kubernetes-test/</code>, moving it outside the watched path, which reads as the responsible thing to do. On the next reconciliation, Flux compared its desired state (Git, where the adopted resources no longer appeared under the watched path) against actual state (the cluster, where they still existed, because Flux had created them). <code>prune: true</code> did what it's for: deleted everything it had adopted and could no longer see in Git. <code>Namespace/valkey-test</code>, gone. Anything else it had silently claimed, gone with it.</p><h3>Links</h3><ul><li><p><a href="https://fluxcd.io/flux/components/kustomize/kustomizations/">Flux Kustomization API reference</a></p></li></ul><h2>The version where this isn't recoverable</h2><p>What actually happened was close to the safe end of this failure mode: Flux deleted only what it had wrongly created itself, and the incident resolved as a strange namespace appearing and disappearing.</p><p>The version that doesn't resolve cleanly needs one more coincidence: a filename collision. If the wandering test directory had contained a <code>secret.yaml</code> that happened to share a name with a real production secret already managed under <code>redis-system</code>, one of two things happens instead. Either Flux's apply overwrites the real secret's contents with the test manifest's, silently, on the next reconcile, with no alert. Or, worse, the later prune reads the production secret as something <em>it</em> adopted and no longer sees in Git, and deletes the real one.</p><p><code>prune: true</code> doesn't know the difference between a resource it should own and a resource it accidentally started owning. It only knows what's in Git and what's in the cluster under its watch. If the <code>path</code> is wide enough to accidentally include something it shouldn't, prune becomes, in the plainest sense, a weapon pointed at your own cluster by a rule that's doing exactly what it was told.</p><h2>What actually closes the gap</h2><ul><li><p>Never point a Kustomization's <code>path</code> at a parent directory that might one day contain something unrelated. <code>./kubernetes/</code> is the mistake, <code>./kubernetes/production/redis-system/</code> is the fix: the deepest, most specific directory that contains only that Kustomization's own manifests and nothing else, ever.</p></li><li><p>If a shared parent path is genuinely unavoidable, a <code>.fluxignore</code> file at that path (same syntax as <code>.gitignore</code>) excludes subtrees like <code>test/</code>, <code>staging/</code>, or <code>*-test/</code> from being swept up at all.</p></li><li><p><code>targetNamespace</code> is a global override with no per-resource awareness, so stop reaching for it by default. A <code>kustomization.yaml</code> with an explicit <code>namespace:</code> field in the directory itself gives you the same result with none of the blast radius, because it only ever touches manifests that are already scoped to that directory.</p></li><li><p><code>spec.wait: true</code> makes Flux wait for resources to report ready before considering the reconcile complete, which surfaces adoption-shaped surprises sooner, before they've had a chance to sit quietly for weeks.</p></li><li><p>A single YAML change won't close this gap on its own. Treat "do any two Kustomizations' paths overlap" as a question worth asking on a recurring basis, the same way you'd audit IAM policies for overlap: the failure mode here is two good configs that were never checked against each other, not a bad config in isolation.</p></li></ul><h3>Links</h3><ul><li><p><a href="https://fluxcd.io/flux/components/kustomize/kustomizations/">Flux Kustomization API reference</a></p></li><li><p><a href="https://fluxcd.io/flux/components/source/gitrepositories/#ignore">Flux: .sourceignore and path exclusion</a></p></li></ul><p>Questions? Feedback? Reply to this email. I actually read them.</p><p>- Ilia</p>]]></content:encoded></item><item><title><![CDATA[The 25% of your node that's gone before the first pod lands]]></title><description><![CDATA[kube-reserved, eviction thresholds, pause containers, DaemonSet overhead, hosted control plane fees]]></description><link>https://podostack.com/p/k8s-hidden-cost-overhead</link><guid isPermaLink="false">https://podostack.com/p/k8s-hidden-cost-overhead</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Wed, 02 Sep 2026 14:03:06 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cQxz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cQxz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cQxz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!cQxz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!cQxz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!cQxz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cQxz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cQxz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!cQxz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!cQxz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!cQxz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd98f89b5-1bf8-40e0-86bf-cb591dc11859_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Somebody on my team once sized a node pool off the instance label. c5.4xlarge, 16 vCPU, done, that's what goes in the capacity plan. Then we actually looked at what <code>kubectl describe node</code> reported as allocatable, and it wasn't 16. It was closer to 15.5 on paper and noticeably less than that once the DaemonSets landed. Nobody had lied to us. We'd just never asked the node what it thought "capacity" meant.</p><p>That gap, between the number on the instance and the number a pod can actually schedule into, is usually 20-30% on a real cluster, and almost none of it shows up on a <code>kubectl top</code> dashboard. It's not a bug. Every layer taking a slice has a legitimate reason to exist. It's just that nobody sums the slices before doing capacity planning, so the plan is wrong by a fifth to a third before the first workload lands.</p><p>Here's where it actually goes.</p><h2>kube-reserved and system-reserved</h2><p>Every managed Kubernetes offering carves out CPU and memory for the kubelet, the container runtime, and the OS itself before your pods ever see it. The three big ones do it with three different formulas, and none of them show up on the labels on the instance type.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lNex!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lNex!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 424w, https://substackcdn.com/image/fetch/$s_!lNex!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 848w, https://substackcdn.com/image/fetch/$s_!lNex!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 1272w, https://substackcdn.com/image/fetch/$s_!lNex!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lNex!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png" width="1792" height="1412" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1412,&quot;width&quot;:1792,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lNex!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 424w, https://substackcdn.com/image/fetch/$s_!lNex!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 848w, https://substackcdn.com/image/fetch/$s_!lNex!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 1272w, https://substackcdn.com/image/fetch/$s_!lNex!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd64dcb24-c633-4c80-b35d-2b1a8780c8d2_1792x1412.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>AKS is the most transparent about the math: kube-reserved is the lesser of <code>20 MB &#215; max-pods + 50 MB</code> or 25% of total memory. An 8 GB node running 30 pods reserves 650 MB before anything else happens. GKE publishes reference numbers too: a 2 vCPU / 7.5 GB node reserves roughly 70m CPU and 1,736 Mi memory as kube-reserved on its own. EKS doesn't publish an exact formula at all; the common community convention baked into most bootstrap scripts is <code>memory=0.3Gi, ephemeral-storage=1Gi</code> for system-reserved, which is a convention people copied into their tooling, not a number AWS commits to.</p><p>Three providers, three different answers to "how much of this node is actually mine," and the honest answer is you have to ask the node, not the instance catalog.</p><h3>Links</h3><ul><li><p><a href="https://learn.microsoft.com/en-us/azure/aks/node-resource-reservations">AKS node resource reservations</a></p></li><li><p><a href="https://github.com/awslabs/amazon-eks-ami/issues/318">EKS AMI: kube/system-reserved should be enabled by default</a></p></li></ul><h2>Eviction thresholds, and the version cliff nobody warns you about</h2><p>Reserved capacity is the visible tax. Eviction thresholds are the one nobody budgets for: they carve out headroom that never shows up as "used" on any graph, because it's specifically the memory the kubelet refuses to let you use, held in reserve for the moment things go sideways.</p><p>GKE's default <code>memory.available</code> hard eviction threshold is 100Mi. AKS matches that on 1.29 and later. AKS <em>before</em> 1.29 defaulted to 750Mi, seven and a half times more headroom carved out of every single node, silently, as part of a version bump most teams treat as routine. If you fleet-upgraded across that boundary and your capacity math didn't move, it was wrong the whole time before and it's differently wrong now. EKS's commonly configured value in community bootstrap tooling sits around 200Mi, in between the other two, and again: not a published AWS default, a convention.</p><p>None of these thresholds are wrong to have. A node with zero eviction headroom OOM-kills workloads instead of gracefully shedding the least important pod first. The reservation itself is fine. What trips people up is that "allocatable" on the node object already accounts for kube-reserved, while the eviction threshold sits on top of <em>that</em>, invisible to anything reading the allocatable field. Most capacity math stops at allocatable and never goes further.</p><h3>Links</h3><ul><li><p><a href="https://learn.microsoft.com/en-us/azure/aks/node-resource-reservations">Node resource reservations in AKS</a></p></li></ul><h2>Pause containers: small, until you're at scale</h2><p>This one is genuinely tiny per pod: a pause container holding the shared network and IPC namespace for everything else in the pod runs around 1 MB of resident memory. Nobody should reorganize a sprint around it.</p><p>But it's 1 MB times every pod on every node, all the time, for the lifetime of the cluster, and it's invisible in the same way the eviction threshold is: real memory the OS has committed, attributed to a container nobody thinks about when they're reading a namespace's usage graph. At 1,000 pods that's roughly a gigabyte of RAM doing nothing but holding namespaces open, spread across your fleet in amounts too small to alert on and too persistent to ever go away.</p><p>No budget breaks on it alone. What it does is quietly turn "sum of container requests" into a slightly wrong answer to "how much memory does this cluster actually need" - permanently, by a margin that grows with pod count.</p><h2>DaemonSets: the capacity your FinOps dashboard calls "infrastructure"</h2><p>This is the one that actually moves budgets, and it's the one most cost dashboards get structurally wrong.</p><p>CNI, CSI, the log shipper, the node-level monitoring agent, whatever security or service-mesh sidecar-injector runs as a DaemonSet - every one of them takes a CPU and memory reservation on <em>every node in the cluster</em>, multiplied by node count, for the entire life of the fleet. This cost scales with your node count, not your workloads, which means it moves in lockstep with your <em>other</em> costs, whatever's already driving the bill.</p><p>The reservation itself isn't what costs money. Most FinOps tooling attributes CPU and memory by namespace or by workload label, and DaemonSets frequently live in a <code>kube-system</code> or platform namespace that gets bucketed as "infrastructure" and never allocated back to the teams whose CNI policies, log volume, or security scanning drove the agent's resource needs in the first place - that's where the money actually goes missing. Five or six DaemonSets on a hundred-node cluster is a real, attributable cost that shows up on nobody's per-team invoice.</p><p>If your FinOps dashboard has a line called "infrastructure" that's bigger than any single team's line, that's usually where it's hiding.</p><h2>Hosted control plane fees: the one that's actually on your bill</h2><p>Everything above is invisible capacity tax. This one, at least, is a real line item - it's just inconsistently visible depending on who's cutting your check.</p><p>As of this year, EKS and GKE both charge $0.10 per cluster per hour for the control plane while your Kubernetes version is in standard support - about $73/month, though GKE offsets that with a $74.40 monthly credit per billing account that covers exactly one zonal or Autopilot cluster. AKS's free tier charges nothing for the control plane at all; the $0.10/hour tier buys you a 99.95% uptime SLA you don't get for free. And EKS has a second number worth knowing before you need it: once a cluster's Kubernetes version rolls into extended support, that $0.10/hour becomes $0.60/hour - six times the base rate, for the exact same control plane, as the penalty for not upgrading.</p><p>On bare metal, none of this exists as a line item, because the control plane is your hardware and your amortization schedule, which is precisely why it's easy to under-budget for it when you migrate the other direction.</p><h3>Links</h3><ul><li><p><a href="https://learn.microsoft.com/en-us/azure/aks/node-resource-reservations">AKS node resource reservations</a></p></li></ul><h2>What to actually do with this</h2><p><code>kubectl top</code> was never lying to you about usage. It was answering a question you weren't asking - "how much is running" instead of "how much of this node's sticker capacity did I actually get to use." Those are different numbers, and the gap between them is kube-reserved plus the eviction threshold plus pause-container overhead plus your DaemonSet count, none of which show up in the same place.</p><p>The concrete version: read <code>kubectl describe node</code> and compare <code>Capacity</code> to <code>Allocatable</code> yourself, on your actual node type, on your actual provider - don't trust the instance catalog. Then subtract your DaemonSet requests times node count before you tell finance what a node pool costs per unit of real workload capacity. That number, not the instance label, is the one your capacity plan should be built on.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><p>- Ilia</p>]]></content:encoded></item><item><title><![CDATA[Issue #033 - TOON, zot, and the RCE Kubernetes calls working as intended]]></title><description><![CDATA[A token format that beats JSON by half, a registry that ditched the database, the Probe CRD gap nobody's closed, and a WebSocket handshake that skips the real check]]></description><link>https://podostack.com/p/issue-033-toon-zot-nodes-proxy-working-as-intended</link><guid isPermaLink="false">https://podostack.com/p/issue-033-toon-zot-nodes-proxy-working-as-intended</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Tue, 01 Sep 2026 14:03:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!GHPf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GHPf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GHPf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!GHPf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!GHPf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!GHPf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GHPf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GHPf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!GHPf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!GHPf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!GHPf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb699a8b2-1862-4da0-a28f-50dbce7840e4_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Five deep-dive weeks in a row, and I promised the next open week would break the pattern. What landed in it doesn't share a vendor or a language, but it shares a shape: each of these four is a boundary someone drew somewhere you wouldn't expect. How much structure an LLM really needs. How much a registry needs to run at all. Where Prometheus Operator's declarative reach quietly stops. And what Kubernetes itself is willing to call a bug.</p><p>Grab bag's back.</p><div><hr></div><h2>&#128640; Sandbox Watch: TOON</h2><h3>What it is</h3><p>A serialization format built specifically for LLM context, not for storage or wire transfer. Token-Oriented Object Notation - TOON for short - shipped in October 2025 from a single maintainer, Johann Schopplich, MIT licensed. It hit Hacker News and picked up over 2,000 stars in three days. It's sitting past 25,000 now.</p><h3>How it works</h3><p>Not a compression trick. TOON borrows YAML-style indentation for nested objects, then switches to a CSV-like table layout the moment it sees an array of uniform objects - which is most of what actually gets shoved into a prompt: query results, API responses, tool-call output. Each array gets an explicit <code>[N]</code> row count and a <code>{fields}</code> header up front. That's not for humans. It's a guardrail the model uses to check its own parsing as it goes, instead of guessing where one object ends and the next begins from indentation alone.</p><h3>The numbers, and where they stop working</h3><p>The project's own benchmark: 46.3% fewer tokens than formatted JSON, at 70.1% accuracy on retrieval tasks. An independent run across four models - Claude Haiku, Gemini Flash, GPT-5 Nano, Grok - found 39.6% fewer tokens, with accuracy that came out <em>higher</em> than JSON's, 73.9% against 69.7%.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8csh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8csh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 424w, https://substackcdn.com/image/fetch/$s_!8csh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 848w, https://substackcdn.com/image/fetch/$s_!8csh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 1272w, https://substackcdn.com/image/fetch/$s_!8csh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8csh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png" width="1592" height="696" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:696,&quot;width&quot;:1592,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8csh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 424w, https://substackcdn.com/image/fetch/$s_!8csh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 848w, https://substackcdn.com/image/fetch/$s_!8csh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 1272w, https://substackcdn.com/image/fetch/$s_!8csh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6120655f-f38b-45d5-8d4a-9b1ab4a8867d_1592x696.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The authors are upfront about where it loses, too. Deeply nested or non-uniform data - minified JSON often wins outright. Semi-uniform arrays, somewhere around 40-60% field consistency, and the savings mostly evaporate. Purely tabular data - plain CSV is still smaller, TOON's explicit lengths and field lists cost you 5-10% over that. And on local or quantized models, some setups parse JSON faster in wall-clock time even while burning more tokens to do it.</p><h3>When to reach for it</h3><p>Not as a database format, not as an API contract - the authors call it a translation layer for LLM input, and that's the honest framing. If you're stuffing a hundred rows of uniform records into a context window before a completion call, it's worth trying. If your payload is one deeply nested object, don't bother.</p><h3>Links</h3><ul><li><p><a href="https://github.com/toon-format/toon">GitHub: toon-format/toon</a></p></li><li><p><a href="https://github.com/toon-format/spec">TOON spec</a></p></li><li><p><a href="https://toonformat.dev/guide/benchmarks">Official benchmarks</a></p></li></ul><div><hr></div><h2>&#128142; The Hidden Gem: zot</h2><h3>What it is</h3><p>A vendor-neutral, OCI-native container registry. CNCF Sandbox since December 2022, still shipping - v2.1.19 landed in early August. About 2,700 stars, small next to Harbor's, but the commit graph is alive.</p><h3>Where it differs from Harbor</h3><p>I went looking for "zot has full OCI Distribution Spec support, Harbor doesn't" as the pitch, and that angle is stale - Harbor implements spec 1.1 too now, so that's not a real differentiator anymore.</p><p>The real one: zot runs with no database at all. Harbor needs Postgres behind it. For an edge deployment or anything resource-constrained, that one line is the whole pitch: one static binary, no stateful dependency to babysit. zot also has native support for the OCI Referrers API, which is the mechanism Cosign actually uses to store and look up signatures, so signature and SBOM attachment works as a first-class citizen rather than a bolted-on scan step.</p><h3>The honest trade</h3><p>Harbor still has a web UI. RBAC, project-based multi-tenancy, built-in vulnerability scanning, replication between registries, audit logging - and it's CNCF Graduated, zot is Sandbox. None of that is small. zot isn't trying to be a Harbor replacement; it's what you reach for when you want a registry to just serve OCI artifacts and nothing else runs on the same box.</p><p>CoreWeave uses zot as the registry behind their CKS Kubernetes service - it's in their own changelog, not a case study somebody paid for.</p><h3>Links</h3><ul><li><p><a href="https://github.com/project-zot/zot">GitHub: project-zot/zot</a></p></li><li><p><a href="https://www.cncf.io/projects/zot/">zot on CNCF</a></p></li></ul><div><hr></div><h2>&#9876;&#65039; The Showdown: the Probe CRD gap nobody's closed</h2><h3>The comparison I went looking for, and didn't find</h3><p>I went in expecting a head-to-head: some operator wrapping Prometheus Blackbox Exporter versus using Blackbox Exporter directly. What I found instead is that the project going by that description - endpoint-monitoring-operator, 23 stars - isn't a Blackbox Exporter wrapper at all. It's its own thing: a separate <code>EndpointMonitor</code> CRD, its own probe logic for HTTP, HTTP-with-JSON-body-validation, TCP, DNS, ICMP, even Trino and OpenSearch health checks, and it alerts straight to Slack or email. It doesn't talk to Prometheus. So the comparison as pitched doesn't exist - which turned out to be more interesting than the comparison itself.</p><h3>Where the real gap is</h3><p>Prometheus Operator ships a <code>Probe</code> CRD for exactly this job - declaratively wiring blackbox-style checks to Blackbox Exporter, either a static target list or auto-discovered from your Ingress objects. It's mature, it's part of kube-prometheus-stack, most clusters running the standard observability stack already have it.</p><p>But it was built for simple, ping-style checks. Go looking at the project's own GitHub discussions and you'll find someone trying to describe hundreds of HTTP targets, each with its own headers and body, and hitting a wall - the Probe CRD has no way to express per-target Blackbox module configuration at that level. A maintainer's own answer in that thread: what's actually missing is "a CRD to deploy and manage Blackbox Exporter" itself, not just targets pointed at one. That issue has been open for years.</p><h3>My read</h3><p>That's why people write bespoke operators like endpoint-monitoring-operator instead of extending the Prometheus ecosystem: not out of preference, but because managing Blackbox Exporter's own module config was never made declarative. Anyone whose checks need real assertions - not "did we get a 200," but "does this JSON field say what it should" - ends up walking away from Blackbox Exporter entirely and building something narrower and custom. Nobody built a rival tool here. They just never wrote the CRD that would have made Blackbox Exporter's own config declarative.</p><h3>Links</h3><ul><li><p><a href="https://kubespec.dev/prometheus-operator/monitoring.coreos.com/v1/Probe">Probe CRD reference</a></p></li><li><p><a href="https://github.com/prometheus-operator/prometheus-operator/issues/2680">prometheus-operator issue #2680</a></p></li><li><p><a href="https://github.com/LiciousTech/endpoint-monitoring-operator">GitHub: LiciousTech/endpoint-monitoring-operator</a></p></li></ul><div><hr></div><h2>&#128110; The Policy: the RCE Kubernetes calls working as intended</h2><h3>What it takes</h3><p>This attack starts from one RBAC permission most clusters hand out without a second thought: <code>get</code> on <code>nodes/proxy</code>. No prior foothold on the node, no shell on the cluster required - the permission alone gets you in.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!StuQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!StuQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 424w, https://substackcdn.com/image/fetch/$s_!StuQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 848w, https://substackcdn.com/image/fetch/$s_!StuQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!StuQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!StuQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png" width="1752" height="1008" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1008,&quot;width&quot;:1752,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!StuQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 424w, https://substackcdn.com/image/fetch/$s_!StuQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 848w, https://substackcdn.com/image/fetch/$s_!StuQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!StuQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25af6921-f9f5-491d-bd49-88680c31931f_1752x1008.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Security researcher Graham Helton published the mechanism in late January. The apiserver's <code>nodes/proxy</code> endpoint is meant to let you read things like node metrics through the apiserver instead of reaching the kubelet directly. The kubelet, on the other end, decides whether to allow a WebSocket upgrade based on the HTTP method of the handshake - and per RFC 6455, that method is always <code>GET</code>. Which means the secondary check that's supposed to gate an actual <code>/exec</code> - the <code>create</code> verb - never fires on the WebSocket path. <code>get</code> on <code>nodes/proxy</code> is enough, by itself, to open a connection to kubelet:10250 and run a command inside any pod on that node.</p><h3>The 69 charts</h3><p>Helton didn't stop at the theory - he audited what ships with this permission. Sixty-nine Helm charts grant <code>nodes/proxy</code> <code>get</code> as a matter of course, the full list with links is in his write-up. The names on it are the ones nobody questions: Prometheus, Grafana's Promtail, Datadog, Elastic Agent, Cilium, the OpenTelemetry Collector, Trivy Operator when its cluster role is enabled. Exactly the observability stack most platform teams install without reading the RBAC it asks for.</p><h3>Working as intended</h3><p>There's no CVE here, and there isn't going to be one. Kubernetes' Security Response Committee closed it on January 23rd, classified as Won't Fix, working as intended. In their own words, checking a verb at one layer and an HTTP method at another is "brittle, architecturally incorrect" - so instead of patching the specific hole, they're pointing at KEP-2862, Fine-Grained Kubelet API Authorization. It splits the kubelet API into separate, narrower permissions - <code>nodes/metrics</code>, <code>nodes/stats</code>, <code>nodes/log</code>, <code>nodes/healthz</code>, <code>nodes/pods</code> - instead of one <code>nodes/proxy</code> that covers all of it. It's Beta now, behind a feature gate, GA is targeted for April.</p><p>Worth being precise about how this differs from the kubelet-API piece I ran back in issue-015: that one assumed an attacker already sitting on the node network, reaching port 10250 directly. This one needs nothing but a ServiceAccount token carrying a permission your monitoring stack probably already has. No node access required at all - a different privilege boundary, not a rerun of the same finding.</p><h3>What to do about it</h3><p>Audit which service accounts in your cluster hold <code>nodes/proxy</code>. If you're running any of the standard observability charts, the answer is probably "more than you'd guess." Then treat KEP-2862's rollout as your real timeline, not a patch that's coming - because it isn't.</p><h3>Links</h3><ul><li><p><a href="https://grahamhelton.com/blog/nodes-proxy-rce">Original research: nodes/proxy RCE</a></p></li><li><p><a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-auth/2862-fine-grained-kubelet-authz">KEP-2862: Fine-Grained Kubelet API Authorization</a></p></li></ul><div><hr></div><p>Four boundaries, four different people drawing them. What TOON decided an LLM doesn't need. What zot decided a registry doesn't need. The CRD nobody at Prometheus Operator got around to writing. And the line Kubernetes drew around what even counts as a bug.</p><p>The last one's the one I keep coming back to - not because it got patched, but because it didn't, and it's probably sitting in your cluster right now if you're running anything that watches your nodes.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><p>- Ilia</p>]]></content:encoded></item><item><title><![CDATA[LowCardinality: the dictionary stops at 8192]]></title><description><![CDATA[The rule of thumb everyone repeats is downstream of one default setting, and crossing it produces no error - just a column that quietly goes back to storing strings]]></description><link>https://podostack.com/p/clickhouse-lowcardinality-inversion-point</link><guid isPermaLink="false">https://podostack.com/p/clickhouse-lowcardinality-inversion-point</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Fri, 28 Aug 2026 14:04:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ZHVS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZHVS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZHVS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!ZHVS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!ZHVS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!ZHVS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZHVS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZHVS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!ZHVS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!ZHVS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!ZHVS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a135eb6-49d1-4b40-8cc2-da25e56a2733_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Wrap a string column in <code>LowCardinality</code> and ClickHouse stops storing the string. It builds a dictionary of distinct values and stores small integer positions into it, so a <code>GROUP BY</code> compares integers instead of strings and the column compresses like integers too. On a country code or an HTTP method the win is large and free.</p><p>The advice attached to it is always some version of "use it under ten thousand distinct values". That number is real, and it's a rounded-off consequence of a setting most people have never looked at.</p><pre><code>low_cardinality_max_dictionary_size = 8192</code></pre><h2>What the setting does</h2><p>8192 is the maximum size, in rows, of the dictionary ClickHouse will write for a column. The documentation's blunt about what happens when you exceed it:</p><blockquote><p>All the data that can't be encoded due to maximum dictionary size limitation ClickHouse writes in an ordinary method.</p></blockquote><p>No error. No warning. No log line at the default level. Values that fit go in the dictionary as integer keys; everything past the limit is written as plain strings, in the same column, and the column keeps working exactly as before.</p><p>There's a companion setting that shapes what "past the limit" means:</p><pre><code>low_cardinality_use_single_dictionary_for_part = false</code></pre><p>With the default, when a dictionary fills up the server starts a new one rather than giving up. So a part with many distinct values ends up carrying several dictionaries plus whatever spilled over. Set it to <code>1</code> and you get exactly one dictionary per part and everything beyond 8192 stored the ordinary way.</p><p>Either way the outcome is the same in the direction that matters: the type stops being an optimisation and starts being an overhead, and nothing tells you.</p><h2>Why the failure is quiet in both directions</h2><p>The reason this catches people is that the type's behaviour degrades along two axes that don't move together.</p><p>Reads degrade gently. A partly-encoded column still answers queries; you just get less of the compression and less of the integer-comparison speedup than you expected, proportional to how much spilled.</p><p>Writes degrade immediately. Every insert has to hash the incoming value and look it up in the current dictionary, and that cost is paid whether or not the value ends up encoded. On a column with millions of distinct values you're paying full price for hashing on every row and getting nothing back.</p><p>Memory sits in the middle. Dictionaries live in RAM while parts are being read and merged, so a high-cardinality <code>LowCardinality</code> column adds resident memory in exchange for the storage saving it isn't delivering.</p><p>The documentation puts the far edge at a hundred thousand distinct values, past which the type "can perform worse in comparison with using ordinary data types". Between 8192 and 100k you're in a zone where it's neither clearly good nor clearly bad, and that zone is where nobody investigates.</p><h2>Finding out which columns are lying to you</h2><p>Cardinality per column is one query, and it's worth running against a real table rather than a sample:</p><pre><code>SELECT
    uniqExact(status)      AS status_uniq,
    uniqExact(user_agent)  AS ua_uniq,
    uniqExact(request_id)  AS rid_uniq
FROM logs
WHERE event_date &gt;= today() - 7;</code></pre><p>Anything above 8192 that's currently declared <code>LowCardinality</code> is spilling. Anything comfortably under it that's a plain <code>String</code> is leaving a win on the table.</p><p>The subtlety is that cardinality is measured per part, not per table, and parts are built from inserts and merges. A column with 50,000 distinct values spread evenly across time might sit under 8192 within any single part and encode perfectly well, while the same 50,000 values arriving in one bulk load will not. Sorting order matters here too, since data ordered by the column groups its values into fewer parts.</p><p>Which means the honest test is measurement rather than arithmetic:</p><pre><code>SELECT
    name,
    formatReadableSize(sum(data_compressed_bytes))   AS compressed,
    formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed,
    round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 1) AS ratio
FROM system.columns
WHERE table = 'logs' AND database = currentDatabase()
GROUP BY name
ORDER BY sum(data_compressed_bytes) DESC;</code></pre><p>A <code>LowCardinality</code> column whose compression ratio looks like its neighbouring plain strings isn't being encoded.</p><h2>Enum is not the alternative people think it is</h2><p><code>Enum8</code> and <code>Enum16</code> do the same job with the value list fixed in the DDL, which makes them faster on insert - the mapping is static, so there's no hashing and no dictionary to grow - and completely rigid. A value that isn't in the enum is an error, and adding one means an <code>ALTER TABLE</code>.</p><p>That rigidity is a feature for a closed set: days of the week, a payment state machine you control, HTTP methods. It's a production incident for anything a third party can extend, because the day a new value appears your inserts start failing.</p><p><code>Enum16</code> also tops out at 65,535 values, which is comfortably above the point where <code>LowCardinality</code> has stopped helping. If you're choosing between them on capacity you've already picked the wrong tool for that column.</p><h2>The limits worth knowing</h2><p><code>LowCardinality</code> isn't free at query time in every direction. Most string functions work through it transparently, but anything that has to materialise the actual strings - tokenisation, some regex paths, certain joins - decodes the whole column first, and for those the type costs you rather than saving you.</p><p>The setting is per-insert rather than per-column, which surprises people who go looking for a way to raise the limit on one wide column. Raising <code>low_cardinality_max_dictionary_size</code> globally trades RAM across every <code>LowCardinality</code> column in the server for the benefit of one, so the usual answer is to change the column type rather than the setting.</p><p>And the guidance shifts with version: this is ClickHouse 26.7 behaviour, and defaults in this area have moved before. Read the value out of your own server rather than trusting a number in a post:</p><pre><code>SELECT name, value FROM system.settings WHERE name LIKE 'low_cardinality%';</code></pre><h3>Links</h3><ul><li><p><a href="https://clickhouse.com/docs/en/sql-reference/data-types/lowcardinality">LowCardinality data type (ClickHouse docs)</a></p></li><li><p><a href="https://github.com/ClickHouse/ClickHouse/blob/master/src/Core/Settings.cpp">Settings.cpp - low_cardinality_max_dictionary_size</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Knative and PDB: protects nothing, blocks drains forever]]></title><description><![CDATA[A PodDisruptionBudget on a Knative revision cannot stop the autoscaler removing your pod, and it can absolutely stop you draining the node it runs on]]></description><link>https://podostack.com/p/knative-pdb-protects-nothing-blocks-drain</link><guid isPermaLink="false">https://podostack.com/p/knative-pdb-protects-nothing-blocks-drain</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Wed, 26 Aug 2026 14:01:31 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!0fCV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0fCV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0fCV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!0fCV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!0fCV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!0fCV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0fCV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0fCV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!0fCV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!0fCV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!0fCV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b411a40-a565-4d33-b484-e352474f6ef6_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The advice sounds reasonable and it's everywhere: don't put a PodDisruptionBudget on a Knative service, because <code>minAvailable: 1</code> will block scale-to-zero and you'll lose the thing you adopted Knative for.</p><p>It will not block scale-to-zero. It cannot. And that is worse, because people set it expecting protection and get a pod that disappears anyway plus a node they can no longer drain.</p><h2>Why the autoscaler doesn't care</h2><p>A PodDisruptionBudget is enforced in exactly one place: the Eviction API. When something calls <code>POST /pods/{name}/eviction</code>, the API server consults every PDB whose selector matches that pod and refuses if the budget would be violated. That is the entire mechanism.</p><p>Scaling down is not an eviction. When the Knative Pod Autoscaler decides a revision has no traffic, it writes to the Deployment's scale subresource - <code>replicas: 0</code> - and the ReplicaSet controller deletes the pods directly. No eviction call, no PDB lookup, no budget check.</p><p>So the sequence people expect - PDB says one pod must stay, therefore Knative can't go to zero - never happens. The pod goes away on schedule and the PDB is not consulted at any point.</p><p>You can watch this from two terminals if you want to be sure. Send traffic, watch a pod appear, stop traffic, and watch <code>kubectl get pdb</code> report <code>ALLOWED DISRUPTIONS 0</code> the whole time the pod is being removed underneath it.</p><h2>What it does block</h2><p>Node drains, which is precisely what a PDB is for, and precisely the interaction nobody plans for.</p><p>Take a service pinned with <code>autoscaling.knative.dev/minScale: "1"</code> because it can't tolerate cold starts, and a PDB with <code>minAvailable: 1</code> because that's what you always write. There is one pod. The budget allows zero disruptions. <code>kubectl drain</code> on that node calls the Eviction API, gets a 429, retries, and keeps retrying.</p><p>Forever. The autoscaler has no reason to add a second pod, because there's no traffic pressure to justify one, and the drain has no way to ask for one. A cluster upgrade rolling through the fleet stops on that node and waits for a human.</p><p>That failure has a shape worth recognising: a PDB that permits zero disruptions on a workload that will never scale up on its own is a permanent drain block, and it is silent until the day somebody drains that specific node.</p><h2>Then there's the revision problem</h2><p>Even where a PDB is the right tool, Knative's object model fights the way you'd normally write one.</p><p>Revisions are immutable. Every configuration change makes a new Revision, each with its own Deployment and its own pod labels:</p><pre><code>serving.knative.dev/revision: my-app-00042
serving.knative.dev/service:  my-app</code></pre><p>Write a PDB against the revision label and it's dead weight the moment you deploy: it now selects a Revision that's scaling to zero while the new one runs unprotected. Keeping up means creating a PDB per deploy and garbage-collecting the old ones, which is a controller nobody wants to own.</p><p>Selecting on <code>serving.knative.dev/service</code> covers every revision of a service at once, including both sides of a traffic split, which is usually what you meant. It also means your budget arithmetic has to account for pods across two revisions during a rollout.</p><h2>Where Knative's availability comes from</h2><p>For a service that scales to zero, asking "how do I keep a pod available" is the wrong question, because the answer is that there isn't one and that's the design.</p><p>Requests to a zero-scaled revision don't fail. They go to the Activator, which buffers the request, tells the autoscaler to scale up, and forwards it once a pod is ready. Your availability during that window is the Activator's availability, not your pod's.</p><p>Which is why the PDBs that matter for a Knative install are the ones on Knative's own components, and they already exist. A default install runs the activator, autoscaler, controller and webhook with two replicas, and ships an <code>activator-pdb</code> with <code>minAvailable: 70%</code>. If you're going to spend attention on disruption budgets in a Knative cluster, spend it verifying those survived your install tooling rather than adding new ones to application revisions.</p><h2>What to do instead</h2><p>For most services: nothing. Scale-to-zero and the Activator are the availability story, and a PDB adds a drain hazard in exchange for no protection.</p><p>For a service that must always be warm, set <code>minScale: 2</code> rather than 1. Two pods across two nodes give a drain somewhere to move work to, and only then does a budget make sense - written as <code>maxUnavailable: 1</code> rather than <code>minAvailable: 1</code>, because that shape always permits one disruption regardless of how the replica count moves under you.</p><p>If you inherited a cluster and want to know whether this is already waiting for you:</p><pre><code>kubectl get pdb -A -o custom-columns=\
NS:.metadata.namespace,NAME:.metadata.name,ALLOWED:.status.disruptionsAllowed \
  | awk '$3=="0"'</code></pre><p>Anything listing zero allowed disruptions is a node drain that will hang. Cross-check those against Knative services with <code>minScale: 1</code>.</p><h2>The limits worth knowing</h2><p>PDBs only ever governed voluntary disruptions. A node that dies takes your pod with it whatever the budget says, and no arrangement of PDBs changes that - the guarantee was always about planned maintenance.</p><p>Knative also isn't unique here. Any autoscaler that writes the replica count directly, HPA included, moves pods without consulting a PDB. The Knative case stands out because scale-to-zero makes the gap between "budget says one" and "reality says none" a routine daily event rather than an edge case.</p><p>And this is Knative Serving 1.23 behaviour. Component defaults, including the activator PDB, are the kind of thing that shifts between releases, so check yours rather than trusting a number in a post.</p><h3>Links</h3><ul><li><p><a href="https://knative.dev/docs/serving/autoscaling/scale-to-zero/">Configuring scale to zero (Knative docs)</a></p></li><li><p><a href="https://knative.dev/blog/articles/demystifying-activator-on-path/">Demystifying Activator on the data path (Knative blog)</a></p></li><li><p><a href="https://kubernetes.io/docs/concepts/workloads/pods/disruptions/">Disruptions (Kubernetes docs)</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Issue #032 - Keyless signing: nobody manages signing keys anymore]]></title><description><![CDATA[The key-custody problem is solved and what replaced it is a verification problem, which is harder to notice when you have got it wrong]]></description><link>https://podostack.com/p/issue-032-keyless-signing-identity-is-the-key</link><guid isPermaLink="false">https://podostack.com/p/issue-032-keyless-signing-identity-is-the-key</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Tue, 25 Aug 2026 14:02:01 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ipd_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ipd_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ipd_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!ipd_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!ipd_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!ipd_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ipd_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ipd_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!ipd_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!ipd_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!ipd_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7a5edf5b-772b-4f59-ae4c-83c32a00a8c6_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For about twenty years, code signing meant key custody. Generate a key, protect it for its lifetime, and accept that losing it costs you your identity while leaking it costs everyone else theirs. Every practice around it - HSMs, air-gapped signing machines, elaborate ceremonies - existed because the key was a long-lived secret that had to stay secret.</p><p>That problem is gone. Not mitigated, gone: in a Sigstore signature there's no key to keep, because the key is generated, used once, and thrown away inside the same command.</p><p>Which is a genuine win, and it moved the failure somewhere less visible. I covered Sigstore as one block in <a href="https://podostack.com/p/supply-chain-security">issue #005</a> back in February, at the level of "cosign signs your images". This is the other half: what you're actually trusting once the key is gone, and why the interesting bugs now live in the verify path rather than the sign path.</p><h2>&#127959;&#65039; Architectural Pattern: a certificate that expires before you finish reading this</h2><h3>The exchange</h3><p>The flow is short enough to hold in your head.</p><p>You authenticate to an OIDC provider - Google, GitHub, your own issuer - and get an ID token. Cosign generates a throwaway keypair locally. It sends the public key and the token to Fulcio, the CA. Fulcio validates the token against the provider, then issues an X.509 certificate binding that public key to the identity in the token: the email or workflow reference goes in the Subject Alternative Name, and the OIDC issuer goes in a dedicated extension.</p><p>The certificate's valid for ten minutes.</p><p>You sign, the private key is discarded, and the signature plus the certificate go into a Rekor entry, which timestamps the fact that this certificate signed this digest while the certificate was still valid.</p><h3>Why ten minutes is the point</h3><p>A ten-minute certificate can't be revoked in any meaningful sense, and it doesn't need to be. Revocation infrastructure - CRLs, OCSP, the whole apparatus - exists because certificates outlive the circumstances that justified them. Shrink the window below the time it takes to notice and respond to a compromise and the problem stops being worth solving.</p><p>The cost is that a ten-minute certificate is useless as a verification anchor on its own. By the time anyone checks your signature, the certificate expired months ago. So verification can't ask "is this certificate valid now". It asks "was this certificate valid when the signature was made", and the only reason that question has an answer is the transparency log entry that timestamped it.</p><p>That inverts a dependency people don't expect. In classic signing, the log is an audit nicety. Here it's load-bearing: without a log entry, an expired certificate proves nothing.</p><h3>What you're trusting instead</h3><p>The key is gone and the trust didn't evaporate, it moved. A verifier is now trusting, in order: that the OIDC provider correctly authenticated whoever asked; that Fulcio correctly validated the token; that the log entry is genuine and inclusion-proven; and that the identity in the SAN is one you meant to accept.</p><p>The last one is yours. The first three belong to somebody else, which is fine - that's what a public good looks like - but it's worth saying plainly, because "keyless" gets read as "trustless" often enough to matter.</p><h3>Links</h3><ul><li><p><a href="https://github.com/sigstore/fulcio">Fulcio (sigstore/fulcio)</a></p></li><li><p><a href="https://github.com/sigstore/fulcio/blob/main/docs/certificate-specification.md">Sigstore certificate profile</a></p></li></ul><div><hr></div><h2>&#127386; The Showdown: Rekor v2 against the log it replaced</h2><p>Rekor v2 reached GA this year, and it's an unusual upgrade in that most of the release notes are things being taken away.</p><p>Gone: signed timestamps returned by the log. Gone: the search index. Gone: entry types <code>intoto</code>, <code>rekord</code>, <code>helm</code>, <code>tuf</code>, <code>rfc3161</code>, <code>jar</code>, <code>rpm</code>, <code>cose</code> and <code>alpine</code>, leaving <code>hashedrekord</code> for artifacts and <code>dsse</code> for attestations. Gone: attestation storage, so you keep your attestations next to your artifacts like any other file.</p><p>Underneath, the Trillian backend was replaced with a tile-based log built on Tessera, and each shard now has its own URL along the lines of <code>logYEAR-rev.rekor.sigstore.dev</code> rather than hiding behind one endpoint.</p><p><strong>The case for deleting things.</strong> A transparency log's only job is to make it impossible to sign something without leaving a permanent, publicly auditable trace. Every additional feature - a search index, ten entry formats, blob storage - is surface that has to stay correct forever, because a transparency log you have to migrate off is not much of a transparency log. Cutting to two entry types and a tile-based backend makes it cheap enough to run that other people can run their own, which is the actual goal.</p><p><strong>The case against, and it's real.</strong> The search index going away means you can no longer ask the log "show me everything signed by this identity" without building that index yourself. For anyone doing detection - watching for signatures made by an identity that shouldn't be signing - that was the useful interface, and its replacement is "coming later as a separate verifiable service".</p><p>The client-side consequence is the one to plan for: shard URLs must come from the TUF root, not from your config. Hardcode a log URL today and you've built a thing that breaks on the next rotation. Rekor v1 keeps running in parallel and will eventually freeze for new uploads, with a year's notice.</p><h3>Links</h3><ul><li><p><a href="https://blog.sigstore.dev/rekor-v2-ga/">Rekor v2 GA (Sigstore blog)</a></p></li><li><p><a href="https://github.com/sigstore/rekor-tiles">sigstore/rekor-tiles</a></p></li></ul><div><hr></div><h2>&#128110; The Policy: the verify command is the whole security boundary</h2><p>Signing is now easy enough that people do it. Verification is where the work moved, and it's the half that gets copy-pasted.</p><p>Cosign v2 made two flags mandatory for keyless verification, and the reason is worth internalising:</p><pre><code>cosign verify \
  --certificate-identity 'https://github.com/acme/api/.github/workflows/release.yml@refs/heads/main' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/acme/api:1.4.2</code></pre><p>Without those, "verify" means "some identity that Fulcio was willing to certify signed this", which is every GitHub account in existence. The command would succeed on an image signed by an attacker with a free account, and it'd look exactly like success.</p><p><strong>The regex habit is the same bug wearing a hat.</strong> <code>--certificate-identity-regexp='.*'</code> shows up constantly, usually added to make a pipeline go green. It restores precisely the property the mandatory flags were introduced to remove. If you need a pattern, anchor it: pin the issuer exactly, and let the regex cover only what varies, like a tag or a branch under a repository you control.</p><p><strong>Pin the workflow, not the repository.</strong> For CI identities the SAN contains the workflow file and the ref. Accepting any workflow in a repository accepts the one an attacker adds in a pull request, so the identity you pin should name the release workflow on the branch you protect.</p><h3>The verification path is where the bugs are</h3><p>If that sounds theoretical, January settled it. <a href="https://github.com/sigstore/cosign/security/advisories/GHSA-whqx-f9j3-ch6m">CVE-2026-22703</a> let a crafted bundle verify successfully even though its embedded Rekor entry didn't reference the artifact's digest, signature or public key. Cosign checked the log entry's signature and skipped the comparison that ties the entry to the thing in front of it, so any valid entry would do.</p><p>The practical effect is that an attacker who already had your identity could sign something and attach a Rekor entry pointing at an unrelated event, which removes the audit trail - the one property the log exists to provide.</p><p>Two details make it more instructive than an ordinary CVE. It affected cosign v2 users on default flags, while v3 users on default flags were unaffected. And it was a regression: the same class of bug had been fixed before and came back through a refactor.</p><p>Fixed in v2.6.2 and v3.0.4. Current releases are v3.1.3 and v2.6.5, and if your CI's pinned to a cosign version from last year, that's this week's fifteen-minute job.</p><h3>The honest part</h3><p>None of this makes an artifact good. A verified signature says a particular identity signed a particular digest at a particular time, and that's all it says. A compromised CI account produces perfectly valid signatures over malicious images.</p><p>What it buys you is attribution and an audit trail nobody can edit without being seen, which is a large improvement over an unsigned artifact and a smaller one than the word "verified" suggests.</p><p>And the monitoring gap is real. Sigstore's value proposition assumes somebody notices when an identity signs something it shouldn't. With the search index gone from Rekor v2, the tooling for that is thinner than it was, so if your threat model depends on detection rather than on after-the-fact forensics, that's a project rather than a flag.</p><h3>Links</h3><ul><li><p><a href="https://docs.sigstore.dev/cosign/verifying/verify/">cosign verify (docs)</a></p></li><li><p><a href="https://github.com/sigstore/cosign/security/advisories/GHSA-whqx-f9j3-ch6m">GHSA-whqx-f9j3-ch6m</a></p></li></ul><div><hr></div><h2>Where the difficulty went</h2><p>The thing I keep noticing about this transition is that it didn't remove difficulty, it relocated it into a place with worse feedback.</p><p>Key custody was hard, and it was hard in a way that announced itself. Everyone knew the HSM was a chore. Nobody accidentally believed they had good key management when they didn't.</p><p>Identity-based verification is easy to get wrong and comfortable to leave wrong, because a misconfigured verify command returns zero and prints a checkmark. The pipeline is green, the dashboard says signatures are enforced, and the policy admits anyone with a GitHub account. There's no chore to remind you.</p><p>We checked ours after reading the January advisory. Two of nine pipelines pinned issuer and identity properly. Five used a regexp with <code>.*</code> somewhere in it. Two verified with <code>--insecure-ignore-tlog</code>, which somebody added during an incident in March and nobody removed.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><ul><li><p>Ilia</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Topology-aware routing: 7 replicas work, 8 don't]]></title><description><![CDATA[The annotation refuses to act when the arithmetic looks risky, the newer trafficDistribution field has no such guard, and migrating between them quietly drops a safety net]]></description><link>https://podostack.com/p/topology-aware-routing-activation-thresholds</link><guid isPermaLink="false">https://podostack.com/p/topology-aware-routing-activation-thresholds</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Fri, 21 Aug 2026 14:01:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!FNUV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FNUV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FNUV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!FNUV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!FNUV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!FNUV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FNUV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FNUV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!FNUV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!FNUV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!FNUV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6162c12e-8244-4ced-813f-867f6eca327f_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Cross-zone traffic is billed per gigabyte, so the annotation that keeps requests inside one availability zone looks like free money:</p><pre><code>metadata:
  annotations:
    service.kubernetes.io/topology-mode: Auto</code></pre><p>You apply it, the bill doesn't move, and there's nothing in <code>kubectl describe service</code> that looks like an error. The usual conclusion is that the feature is broken or that the cluster is missing zone labels.</p><p>Neither. The controller looked at your service, decided that routing by zone would overload something, and declined. It even said so, in an event nobody reads.</p><h2>What the controller is actually deciding</h2><p>The EndpointSlice controller doesn't route anything. It writes hints into EndpointSlices - a note on each endpoint saying "this one is for zone X" - and kube-proxy on each node honours the hints by restricting its endpoint set to the local zone.</p><p>It decides who gets a hint from allocatable CPU per zone, and pod counts never enter into it:</p><blockquote><p>The controller allocates a proportional amount of endpoints to each zone. This proportion is based on the allocatable CPU cores for nodes running in that zone.</p></blockquote><p>A zone with twice the CPU is expected to receive twice the traffic, so it should hold twice the endpoints. From that expectation the controller computes, per zone, the smallest endpoint count it would tolerate:</p><pre><code>const overloadThreshold float64 = 0.2

desired := ratio * float64(numEndpoints)
minimum := int(math.Ceil(desired * (1 / (1 + overloadThreshold))))</code></pre><p>Read that as: each endpoint may end up carrying at most 20% more than its fair share. Anything worse and the controller refuses. It sums <code>minimum</code> across zones, and if the total exceeds the number of endpoints you actually have, it gives up and leaves the service on cluster-wide routing.</p><p><code>overloadThreshold</code> is a package-level constant and it isn't exported. There's no flag, no annotation value, no per-service override.</p><h2>Adding a replica can turn it off</h2><p>Because <code>minimum</code> is rounded up per zone, the outcome isn't monotonic in replica count. Adding a replica can turn the feature off.</p><p>Three zones with equal allocatable CPU, varying the number of ready endpoints:</p><pre><code> 3  hints
 4  refused
 5  refused
 6  hints
 7  hints
 8  refused
 9  hints
10  hints
11  refused
12  hints</code></pre><p>Eight is the one that catches teams, because eight is a normal number of replicas and seven works. Walk through it: each zone's fair share is 8/3 = 2.67 endpoints, the minimum is <code>ceil(2.67 / 1.2)</code> = 3, three zones need 9, and you have 8. Refused.</p><p>Nine replicas works, which is where the "at least 3 per zone" rule of thumb comes from. That rule is a decent summary of the safe region and a bad description of the boundary, and the boundary is where the surprise lives - an HPA that scales 7 to 8 silently disables zone routing, then re-enables it at 9.</p><p>Unequal CPU across zones shifts the whole pattern. On a cluster where one zone runs bigger nodes, the working counts differ from the table above, so treat it as a shape rather than a lookup.</p><h2>Finding out which case you're in</h2><p>The controller emits a Warning event on the Service with reason <code>TopologyAwareHintsDisabled</code>, and the message names the specific reason:</p><ul><li><p><code>InsufficientNumberOfEndpoints</code> - fewer endpoints than zones</p></li><li><p><code>MinAllocationExceedsOverloadThreshold</code> - the arithmetic above didn't work out</p></li><li><p><code>NodesReadyInOneZoneOnly</code> - only one zone has ready nodes</p></li><li><p><code>NoZoneSpecified</code> - an endpoint's node has no zone label</p></li></ul><p>So the first diagnostic step is not tcpdump:</p><pre><code>kubectl get events --field-selector reason=TopologyAwareHintsDisabled -A</code></pre><p>If nothing comes back and the hints still aren't there, check whether the hints exist but kube-proxy is ignoring them:</p><pre><code>kubectl get endpointslice -l kubernetes.io/service-name=web \
  -o jsonpath='{range .items[*].endpoints[*]}{.zone}{" -&gt; "}{.hints}{"\n"}{end}'</code></pre><p>Empty <code>hints</code> with no event usually means the controller never considered the service at all - the annotation is misspelled, or it's on the Deployment instead of the Service.</p><h2>The newer field is a different mechanism</h2><p><code>spec.trafficDistribution</code> shows up in every discussion of this, and reading it as a tidier spelling of the annotation is a mistake worth avoiding.</p><p>They're separate code paths in the same controller. The annotation goes through the capacity heuristic described above. <code>trafficDistribution</code> goes through its own reconciler, whose entire policy is one comment in the source:</p><blockquote><p>update adds a same zone topology hint for all ready endpoints</p></blockquote><p>Every ready endpoint gets a hint for its own zone. No CPU ratios, no overload threshold, no minimum endpoint count, no refusal path. <code>PreferSameNode</code> does the same thing at node granularity.</p><p>Which means the migration people describe as cosmetic changes the behaviour in both directions. You gain a feature that actually turns on at 8 replicas. You lose the guard that was protecting a thin zone from taking a full share of traffic with one pod in it.</p><p>There's a precedence rule too, and it's silent: the reconciler computes <code>canUseTrafficDistribution</code> as valid field <strong>and</strong> annotation absent. Leave the old annotation on a Service while adding the new field, and the field is ignored entirely. No event, no warning, no status. The source carries a note that the whole branch goes away once the annotation is deprecated under <a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/4444-service-routing-preference">KEP-4444</a>, so the annotation is on its way out - but while both exist, the old one wins.</p><h2>The assumption underneath all of it</h2><p>Both mechanisms allocate endpoints by where capacity is. Neither knows where traffic comes from.</p><p>That's fine when request origin roughly tracks capacity, which is the common case for internal service-to-service calls in a balanced cluster. It falls apart when a single zone originates most of the traffic - a batch job, a cron-driven fan-out, an ingress tier that isn't spread the way your workloads are. Then zone-local routing concentrates that load onto one zone's endpoints while the others idle, and the cost saving comes out of your tail latency.</p><p>This is also the second half of a two-hop story. <code>externalTrafficPolicy</code> governs the first hop, from the external load balancer to a node, and <a href="https://podostack.com/p/external-traffic-policy-local-load-skew">it has its own skew problem</a>. Topology hints govern the second hop, from the node to a pod. They compose, and they fail independently.</p><h2>The limits worth knowing</h2><p>Hints are ignored entirely when <code>internalTrafficPolicy: Local</code> is set on the Service. The two features want the same decision made in different places, and the node-local policy wins.</p><p>The controller excludes control-plane nodes from its CPU ratios, ignores unready nodes, and pays no attention to taints or tolerations. A zone whose nodes are tainted so your workload can't schedule there still counts toward that zone's expected share, which is a good way to get refusals you can't explain from the pod distribution alone.</p><p>And if any single endpoint is missing a hint while others have them, kube-proxy falls back to all endpoints for that service. Partial hints aren't partially applied - it's all or nothing, per service, on every node.</p><h3>Links</h3><ul><li><p><a href="https://kubernetes.io/docs/concepts/services-networking/topology-aware-routing/">Topology Aware Routing (Kubernetes docs)</a></p></li><li><p><a href="https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/endpointslice/topologycache/topologycache.go">endpointslice/topologycache source</a></p></li><li><p><a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/4444-service-routing-preference">KEP-4444: service routing preference</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Informer resync: the API call that never happens]]></title><description><![CDATA[Lowering resyncPeriod to get fresher data is advice built on a misreading, and the client-go source settles it in one line]]></description><link>https://podostack.com/p/k8s-informers-local-cache-resync</link><guid isPermaLink="false">https://podostack.com/p/k8s-informers-local-cache-resync</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Wed, 19 Aug 2026 14:01:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cVHZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cVHZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cVHZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!cVHZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!cVHZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!cVHZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cVHZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cVHZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!cVHZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!cVHZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!cVHZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff5d8687d-aa4d-4d1a-90b3-28e419e106b6_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The controller was acting on stale data, so somebody lowered <code>resyncPeriod</code> from ten minutes to thirty seconds. It didn't help. Then to five seconds, which didn't help either, and made the process burn CPU on a schedule.</p><p>Resync doesn't fetch anything. From the client-go docs on <code>AddEventHandlerWithResyncPeriod</code>:</p><blockquote><p>The resync operation consists of delivering to the handler an update notification for every object in the informer's local cache; it does not add any interactions with the authoritative storage.</p></blockquote><p>Every word of that is load-bearing, and the last clause is the one that gets skipped.</p><h2>What an informer is made of</h2><p>Four pieces, and they're easier to reason about named:</p><p>A <strong>reflector</strong> does one LIST against the API server, then opens a WATCH from the resourceVersion that LIST returned. It's the only component that talks to the API server at all.</p><p>A <strong>DeltaFIFO</strong> queues what the reflector produces, as typed deltas: Added, Updated, Deleted, Sync.</p><p>An <strong>Indexer</strong> is the local cache - a thread-safe store with secondary indexes, so your controller can ask for "all pods on node X" without a query leaving the process.</p><p><strong>Event handlers</strong> are your code, called as deltas drain out of the queue.</p><p>Everything a controller reads comes from the Indexer. <code>client.Get()</code> through a cached client, a lister, the informer's <code>GetStore()</code> - all of it is memory, populated by that one watch stream. That's the whole point of the pattern: one watch per resource type per process, shared across every controller in the binary, instead of N controllers polling.</p><h2>What a resync actually does</h2><p>The current client-go handles a full resync in five lines, and they answer the question completely:</p><pre><code>case SyncAll:
    objs := clientState.List()
    for _, obj := range objs {
        handler.OnUpdate(obj, obj)
    }</code></pre><p><code>clientState</code> is the local store. <code>OnUpdate(obj, obj)</code> passes the same object as both the old and the new value.</p><p>So a resync walks your cache and calls your update handler once per object, with old and new identical. Nothing is fetched. Nothing is compared. If your handler starts with a check like "did the spec change since the last version" it will find that it didn't, every time, for every object.</p><p>Two consequences fall out of this immediately.</p><p>A handler that diffs old against new does nothing on resync. If you wrote your controller to skip work when nothing changed - which is a good habit - resync is a no-op for you, and turning it down will never make anything fresher.</p><p>A handler that enqueues unconditionally does full work on resync. Cache of fifty thousand pods, resync every thirty seconds, and you've signed up for fifty thousand reconcile enqueues every thirty seconds. The API server sees none of it. Your CPU graph sees all of it.</p><h2>Then what is it for</h2><p>Resync exists for drift between your cache and something the cache can't observe.</p><p>A controller that creates cloud load balancers can't see someone deleting one in the console. No Kubernetes object changed, so no watch event fires, and the controller has no reason to look. A periodic resync re-runs reconciliation for everything and gives it that reason.</p><p>That's the honest use case, and it's why the sensible values are minutes, not seconds. Ten to thirty minutes covers the drift case. Anything shorter is usually somebody trying to fix a freshness problem that resync has never addressed.</p><p>If you have no external state to drift against, zero is a legitimate setting. Plenty of controllers run with resync disabled and are correct, because the watch already delivers every change to every object they care about.</p><h2>When the data really does go stale</h2><p>Two failure modes, and neither is fixed by resync.</p><p><strong>The watch broke and the reflector is re-listing.</strong> Watches die - connection resets, apiserver rollouts, a resourceVersion too old for the watch cache to serve. The reflector handles this by re-listing and starting a new watch, and during that window your cache is whatever it was. Watch <code>HasSynced</code>, which the source is explicit about:</p><blockquote><p>HasSynced returns true if the shared informer's store has been informed by at least one full LIST of the authoritative state of the informer's object collection. This is unrelated to "resync".</p></blockquote><p><strong>You read your own write.</strong> You update an object through the API and immediately read it back from the cache. The write went to etcd; the cache updates when the watch event arrives, which is milliseconds later but not zero. Controllers that assume read-after-write against a cached client have a race, and it's the most common informer bug I've seen. If you need the object you just wrote, either read through an uncached client or structure the reconcile to be re-entrant and let the next event carry the new state.</p><h2>The expensive part is the LIST</h2><p>Resync is free for the API server. The initial LIST is not.</p><p>Historically the apiserver assembled the entire collection in memory before sending a byte of it, so a controller restarting against a hundred thousand pods produced a large allocation spike on the control plane, and a cluster full of controllers restarting together produced several at once.</p><p>That's what streaming lists fix. Under <a href="https://github.com/kubernetes/enhancements/issues/3157">KEP-3157</a> the initial state arrives as a watch that streams items individually, giving the apiserver constant memory overhead instead of one proportional to the collection. Server-side it's the <code>WatchList</code> gate; client-side it's <code>WatchListClient</code>, which reached beta and is on by default. Both are beta rather than GA, so it's worth knowing which of your controllers actually use it before you take credit for the memory graph.</p><p>The apiserver side of this story - how watch requests get served without touching etcd at all - is <a href="https://podostack.com/p/k8s-api-server-watch-cache">the watch cache</a>, and it's a separate mechanism from anything in this piece.</p><h2>The limits worth knowing</h2><p>Resync periods are per handler, but not freely. A shared informer has one resync check period, and a handler asking for a shorter interval than that gets the informer's, not its own. Requesting two seconds on an informer configured for ten minutes gets you ten minutes.</p><p>Shared informers are also shared in a way that bites: the informer factory dedupes by resource type, so two controllers asking for pod informers get the same one, along with each other's resync behaviour and each other's cache memory. That's usually what you want and it's occasionally why a memory number doesn't match anyone's mental model of who allocated it.</p><p>And the cache holds full objects. An informer on pods in a large cluster is holding every pod spec and status in your process. Transform functions on the informer let you strip fields you'll never read before they hit the store, which is the cheapest memory win available to most controllers and one almost nobody applies.</p><h3>Links</h3><ul><li><p><a href="https://github.com/kubernetes/client-go/blob/master/tools/cache/shared_informer.go">client-go tools/cache: shared_informer.go</a></p></li><li><p><a href="https://github.com/kubernetes/enhancements/issues/3157">KEP-3157: watch list</a></p></li><li><p><a href="https://kubernetes.io/blog/2024/12/17/kube-apiserver-api-streaming/">API streaming in kube-apiserver (Kubernetes blog)</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Issue #031 - Ambient mesh: the sidecar was a five-year detour]]></title><description><![CDATA[Istio's node proxy opens its listening sockets inside your pod's network namespace, and it holds a workload certificate for every service account scheduled there]]></description><link>https://podostack.com/p/issue-031-ambient-mesh-sidecar-detour</link><guid isPermaLink="false">https://podostack.com/p/issue-031-ambient-mesh-sidecar-detour</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Tue, 18 Aug 2026 14:02:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!0juF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0juF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0juF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!0juF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!0juF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!0juF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0juF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0juF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!0juF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!0juF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!0juF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1b30ad1-3119-4cb3-b448-56f8d3f2a01e_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Ambient mode went GA in Istio 1.24, in November 2024. That's twenty-one months ago. Istio is on 1.30.3 now, and somewhere in that stretch the question quietly flipped: it stopped being "should we try ambient" and became "why are we still injecting sidecars".</p><p>I covered ambient in <a href="https://podostack.com/p/sidecar-free-mesh-slo-from-yaml-and">issue #002</a> back in January, at the level of the sidecar tax - one Envoy per pod, fifty to a hundred megabytes each, startup ordering problems. That framing is fine and it's also the least interesting thing about the architecture. The resource number is a consequence. The mechanism underneath is stranger than "we moved the proxy to the node".</p><h2>&#127959;&#65039; Architectural Pattern: how a node proxy pretends to be a sidecar</h2><h3>The file descriptor trick</h3><p>The obvious way to build a node-level proxy is to run it on the node, in the host network namespace, and route pod traffic to it. That design has a well-known problem: once the packet leaves the pod, you've lost the pod. Source addresses get rewritten, the proxy has to reconstruct which workload sent what, and every policy decision downstream depends on that reconstruction being right.</p><p>Ambient does something else. When a pod joins an ambient-enabled namespace, the <code>istio-cni</code> node agent enters the pod's network namespace, installs redirection rules there, and then hands ztunnel a file descriptor referring to that namespace over a Unix socket.</p><p>Ztunnel uses the descriptor to open listening sockets <strong>inside the pod's network namespace</strong>. One logical proxy instance per pod, sockets that live in the pod's own netns, a process that lives on the node.</p><p>So from the pod's point of view the proxy is local. From the node's point of view there's one Rust process instead of a hundred Envoys. The pod identity never has to be reconstructed, because the traffic never left the pod's namespace unattributed.</p><p>Three ports do the work, and they're worth knowing when you read a <code>ss</code> output at 2am:</p><pre><code>15001   outbound, before HBONE encapsulation
15006   inbound plaintext
15008   inbound HBONE</code></pre><p>Inbound TCP is redirected to 15008 or 15006 depending on whether it arrived encrypted. Outbound goes to 15001 and leaves the node wrapped in HBONE, which is HTTP CONNECT over mTLS - a tunnel that carries the original destination as connection metadata rather than as a rewritten address.</p><h3>The identity problem, and the fix</h3><p>In the sidecar model, the Envoy holding a workload's certificate ran in that workload's pod. The blast radius of a compromised proxy was one pod, and the certificate it held was its own.</p><p>Ztunnel holds a certificate for every unique service account among the pods on its node. It has to - it terminates mTLS on their behalf. A node running forty pods across twelve service accounts has one process holding twelve workload identities.</p><p>The guardrail is on the CA side. Ztunnel authenticates to the control plane as itself and then requests certificates for other identities, and the CA enforces that it may only do that for identities actually scheduled on that node. Requests for identities not running there are rejected.</p><p>That's a sound design and it is a genuinely different threat model from a sidecar. It's node-scoped rather than pod-scoped, and the thing standing between a compromised ztunnel and a cluster-wide identity grab is a control-plane check rather than the absence of the key material. If your security review asked "what changed", that's the answer, and it's better stated plainly than discovered later.</p><h3>Where waypoints sit</h3><p>Ztunnel does L4 and only L4. mTLS, L4 authorization, TCP-level telemetry. It doesn't parse HTTP headers, by design - that's how it stays small and how it stays Rust.</p><p>Anything at L7 needs a waypoint, which is a real Envoy, deployed as a normal Deployment for a namespace or a service account. The path becomes:</p><pre><code>source pod -&gt; source ztunnel -&gt; waypoint -&gt; destination ztunnel -&gt; destination pod</code></pre><p>Four hops where a sidecar mesh had two. That's the trade you're making for the resource savings, and whether it matters depends entirely on how much of your traffic actually needs L7 treatment.</p><h3>Links</h3><ul><li><p><a href="https://istio.io/latest/docs/ambient/architecture/traffic-redirection/">Ambient traffic redirection (Istio docs)</a></p></li><li><p><a href="https://istio.io/latest/docs/ambient/architecture/data-plane/">Ambient data plane (Istio docs)</a></p></li></ul><div><hr></div><h2>&#127386; The Showdown: what the sidecar was still doing for you</h2><p>The migration guides are cheerful. The list below is the one I'd want before committing, because every item on it is a thing that worked yesterday and needs a decision today.</p><p><strong>Per-workload EnvoyFilter and WASM.</strong> In sidecar mode you could attach a filter to one workload, because that workload had its own Envoy. Ambient has no per-pod Envoy to attach to. Filters either become global or move into a waypoint, and a waypoint serves a namespace or a service account rather than a single pod. Teams with one bespoke filter for one service feel this immediately.</p><p><strong>L7 telemetry by default.</strong> A sidecar emitted HTTP metrics, logs and traces for everything passing through it, because everything passing through it was already parsed. Ztunnel gives you L4: bytes, connections, TCP-level authorization decisions. Your request-rate-by-response-code dashboard goes quiet for any workload without a waypoint. This is the single most common surprise, and it surprises people after the migration rather than during it.</p><p><strong>Locality-aware routing.</strong> A sidecar carried locality information and preferred endpoints in its own zone without being asked. In ambient, traffic that goes through a waypoint goes to wherever that waypoint is, so a waypoint in the wrong zone turns a same-zone call into a cross-zone one - with the latency and the inter-AZ transfer bill that implies. The fix is deploying a waypoint per zone, which is fine, and which nobody plans for on the first pass.</p><p><strong>VirtualService.</strong> Gateway API is the preferred routing surface in ambient. VirtualService support is alpha there. If your routing rules are years of accumulated VirtualService, that's a translation project, not a label change.</p><p><strong>Multi-cluster.</strong> Stable in sidecar mode. Available in ambient with limitations that move release to release, which is the polite way of saying check the notes for your exact version rather than trusting a blog post.</p><p>And the resource claim deserves an honest reading. "Up to ninety percent" is real arithmetic under the right conditions: it's a ratio between one node proxy and N sidecars, so it scales with pod density and it shrinks the moment you deploy waypoints, because a waypoint is an Envoy again. A cluster with thirty pods a node and L7 policy everywhere saves much less than a cluster with a hundred pods a node running L4 mTLS only. Both are ambient. The savings differ by an order of magnitude.</p><h3>The failure mode nobody mentions</h3><p>A sidecar failing takes down one pod. Ztunnel is a DaemonSet, and a ztunnel restart interrupts every meshed pod on that node.</p><p>In practice this is mostly fine, because ztunnel restarts are fast and connections re-establish. It's still a different operational shape, and it belongs in the same conversation as the identity scoping: you traded per-pod failure domains for per-node ones, in exchange for not having to restart every application pod to upgrade the mesh.</p><h3>Links</h3><ul><li><p><a href="https://istio.io/latest/docs/overview/dataplane-modes/">Sidecar or ambient? (Istio docs)</a></p></li><li><p><a href="https://istio.io/latest/docs/ambient/usage/waypoint/">Configure waypoint proxies</a></p></li></ul><div><hr></div><h2>&#128110; The Policy: migrating without a flag day</h2><p>The good news is that the migration is genuinely incremental, and not in the marketing sense. Sidecar pods and ambient pods coexist in one mesh. That means the unit of migration is a namespace, and the rollback is removing a label.</p><p>A sequence that works:</p><p>Start by turning on L4 for one namespace and changing nothing else. Label it, and you get mTLS and L4 authorization with no pod restarts and no waypoint. This step costs almost nothing in resources and it's where most of the security benefit lives. If you stop here permanently for most namespaces, that's a legitimate end state rather than a half-migration.</p><p>Then work out which services need L7. Not which ones have an Envoy today - which ones use a feature that requires parsing HTTP. Header-based routing, JWT claims in authorization policy, retries and timeouts on HTTP semantics, request-level telemetry that someone alerts on. Usually a much shorter list than the number of workloads currently running sidecars.</p><p>Deploy waypoints for those and size them like anything else you own. A waypoint is a Deployment: it needs requests, limits, an HPA if the traffic warrants it, and a copy per zone if you care about locality. Treating it as invisible infrastructure is how you get a mesh-wide latency regression out of one under-provisioned pod.</p><p>Sort out the dashboards before the cutover, not after. Know which panels are fed by L7 metrics from sidecars, and decide for each one whether it moves to a waypoint or gets rebuilt on L4 signals. Leaving it until afterwards buys you a window where nobody can see anything, and that window is usually when somebody asks whether the migration broke something.</p><h3>The honest part</h3><p>This isn't free and it isn't instant. The resource win is real, the operational win of not restarting pods to upgrade the mesh is real, and both are paid for with an extra hop for L7 traffic, a node-scoped identity surface, and a translation project for anyone deep in VirtualService or per-workload filters.</p><p>There's also a version of this decision that ends with "neither". If you're running a mesh for mTLS and nothing else, and you're on a cloud CNI that can do transparent encryption between nodes, the honest comparison isn't sidecar against ambient - it's mesh against no mesh. Ambient makes the mesh cheap enough that the question is worth asking again, which is not the same as answering it yes.</p><h3>Links</h3><ul><li><p><a href="https://istio.io/latest/docs/ambient/overview/">Ambient overview (Istio docs)</a></p></li><li><p><a href="https://github.com/istio/istio/releases">istio/istio releases</a></p></li></ul><div><hr></div><h2>What the detour was actually for</h2><p>Calling the sidecar a five-year detour is unfair in one direction and accurate in the other.</p><p>It was unfair because the sidecar proved the model. Transparent mTLS, policy that doesn't live in application code, telemetry nobody had to instrument - all of that had to be demonstrated before anyone would accept a new component in the datapath at all, and the sidecar demonstrated it in the only way available at the time. The kernel primitives ambient depends on weren't there in 2018.</p><p>It's accurate because the cost model was wrong from the start, and everyone knew it. One proxy per pod is O(pods) for a job that's O(nodes) plus a bit of L7. We spent five years paying a linear tax for a constant-ish problem, and building tooling to make the tax bearable: injection webhooks, startup ordering, the whole <code>holdApplicationUntilProxyStarts</code> genre of workaround.</p><p>The pattern generalises past meshes, and it's the thing I'd take away if I only took one. When a piece of infrastructure needs a lot of tooling to make its cost model tolerable, the tooling is a signal about the cost model, not a solution to it. Sidecar injection webhooks were a very good answer to a question that shouldn't have been asked.</p><p>I went and counted our own sidecars after Istio 1.30 shipped. Two hundred and eleven Envoys, of which eleven are attached to workloads that use a single L7 feature. The other two hundred are doing mTLS.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><ul><li><p>Ilia</p></li></ul>]]></content:encoded></item><item><title><![CDATA[externalTrafficPolicy Local: client IP for skewed load]]></title><description><![CDATA[The setting that preserves the source address also hands each node an equal share of traffic regardless of how many pods it is running]]></description><link>https://podostack.com/p/external-traffic-policy-local-load-skew</link><guid isPermaLink="false">https://podostack.com/p/external-traffic-policy-local-load-skew</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Fri, 14 Aug 2026 14:00:30 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!F66X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F66X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F66X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!F66X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!F66X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!F66X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F66X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!F66X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!F66X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!F66X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!F66X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F05169325-25a6-4ddb-afbc-26dbc8a265aa_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Every access log said <code>10.244.1.x</code>. Rate limiting was useless, the geo rules did nothing, and the abuse dashboard showed one very busy internal address.</p><p>The fix is one line, and everyone finds it within an afternoon:</p><pre><code>spec:
  externalTrafficPolicy: Local</code></pre><p>Real client addresses come back immediately. What arrives later is a load distribution nobody asked for.</p><h2>Why the addresses were wrong</h2><p>Default policy is <code>Cluster</code>. Traffic hits any node, and kube-proxy forwards it to a pod wherever that pod lives.</p><p>That forward is the problem. If the packet arrives at node A and the pod is on node B, node B's reply has to travel back through node A, because node A is the one holding the connection with the load balancer. To make the return path work, node A rewrites the source address to its own. Your pod sees the node, not the client.</p><p><code>Local</code> tells kube-proxy to stop forwarding. If there's a pod for this Service on the node the packet landed on, deliver it locally, unmodified. If there isn't, drop it. No cross-node hop means no return-path problem means no rewrite, and the client address survives.</p><p>The dropped-packet half sounds alarming and mostly isn't, because cloud load balancers health-check the NodePort. Nodes without a local pod fail the check and stop receiving traffic. The mechanism is doing exactly what it says.</p><h2>The skew</h2><p>Here's what the health check can't fix.</p><p>The load balancer distributes across the nodes that pass the check, and it distributes evenly, because it has no idea how many pods each one is running. It's balancing nodes. You care about pods.</p><p>Three nodes pass the health check. Node A runs one pod, node B runs one, node C runs four. The load balancer sends a third of the traffic to each node.</p><p>Node A's single pod gets 33% of all traffic. Each of node C's four pods gets about 8%.</p><p>A four-to-one spread across identical replicas, from a scheduler decision nobody made deliberately. It shows up as some pods pinned at high CPU while others idle, an HPA that scales on average utilisation and therefore scales at the wrong time, and p99 that tracks whichever pod drew the short straw.</p><p>Nothing is broken. Every component is doing its job. The mismatch is that one layer balances nodes and the layer you care about is pods.</p><h2>Making the distribution match</h2><p>Two shapes work, and both come down to making pods-per-node uniform.</p><p>A DaemonSet gives you exactly one pod per eligible node by construction, which makes node-balancing and pod-balancing the same thing. This is why ingress controllers are so often deployed as DaemonSets, and it's the cleanest answer if the workload is a proxy tier you want on every node anyway.</p><p>Otherwise, <code>topologySpreadConstraints</code> with <code>maxSkew: 1</code> over <code>kubernetes.io/hostname</code>:</p><pre><code>topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: web</code></pre><p><code>DoNotSchedule</code> rather than <code>ScheduleAnyway</code> matters here. The soft version lets the scheduler pile pods onto one node when things get tight, which is precisely the state you're trying to prevent.</p><p>Both of these have a cost the docs are quiet about: you've coupled your replica count to your node count. Scaling to 7 replicas across 3 nodes gives you 3/2/2 and a 50% spread that no constraint can remove.</p><h2>The feature that is not the answer</h2><p>Kubernetes has grown a newer field that comes up in every discussion of this, and it solves a different problem.</p><p><code>spec.trafficDistribution</code> takes <code>PreferSameZone</code> or, since 1.35, <code>PreferSameNode</code>. The older <code>PreferClose</code> is still accepted and is now explicitly deprecated in the source in favour of <code>PreferSameZone</code>, which means the same thing with a name that says what it does.</p><p>It's tempting to read <code>PreferSameNode</code> as a gentler <code>Local</code>. It isn't, for two reasons.</p><p>It doesn't preserve the client address. <code>trafficDistribution</code> influences which endpoint kube-proxy picks. It has nothing to say about SNAT, and for NodePort and LoadBalancer traffic the client address still gets rewritten. If source IP is what you came for, this field does not deliver it.</p><p>And it's a hint. The API comment is unambiguous: implementations "can use this field as a hint, but are not required to guarantee strict adherence." <code>Local</code> is a rule with a defined failure mode, which is what you want from something load-bearing.</p><p>What <code>trafficDistribution</code> does help with is cross-zone data transfer cost and the latency of an extra network hop. Worth having. Different problem.</p><h2>The limits worth knowing</h2><p>There's a cheaper answer that people skip past because it feels like a workaround. If your traffic is HTTP and it comes through a layer-7 load balancer or an ingress controller, the client address is already in <code>X-Forwarded-For</code>, and reading it there costs you nothing in distribution. <code>Local</code> earns its keep when you're terminating TCP or TLS yourself, or when you need the address for something below HTTP.</p><p>Some clouds also expose a middle path. AWS NLB and the ALB controller can target pod IPs directly rather than NodePorts, which sidesteps the node-level hop entirely and gets you both properties at once. If you're on EKS, check that before reaching for <code>Local</code>.</p><p>And a detail worth knowing before you debug this at 3am: pods on a node with <code>Local</code> set can reach the Service through the ClusterIP normally. The policy governs traffic entering from outside. Testing from inside the cluster will not reproduce any of the behaviour described here.</p><h3>Links</h3><ul><li><p><a href="https://kubernetes.io/docs/reference/networking/virtual-ips/">Virtual IPs and Service Proxies (Kubernetes docs)</a></p></li><li><p><a href="https://kubernetes.io/docs/tutorials/services/source-ip/">Using Source IP (Kubernetes tutorial)</a></p></li><li><p><a href="https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/">Pod Topology Spread Constraints</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Hyperthreading: the throughput win that costs you latency]]></title><description><![CDATA[Netflix turned SMT off on a 24xlarge and container launches got 20-30% faster, because two siblings fighting over one lock also fight over one core]]></description><link>https://podostack.com/p/hyperthreading-inverts-under-global-lock</link><guid isPermaLink="false">https://podostack.com/p/hyperthreading-inverts-under-global-lock</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Wed, 12 Aug 2026 14:02:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IpjB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IpjB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IpjB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!IpjB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!IpjB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!IpjB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IpjB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IpjB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!IpjB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!IpjB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!IpjB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14d4c200-ece2-4230-9c10-9d47a769ecaa_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Turning hyperthreading off halves your logical core count. Every capacity model you have says that's a bad trade.</p><p>Netflix did it on an <code>m7i.metal-24xl</code> and container launch latency improved by 20 to 30%.</p><p>The workload was the mount storm from <a href="https://podostack.com/p/issue-030-overlayfs-mount-storm-100-pods">this week's issue</a>: a hundred containers coming up at once, twenty thousand mount syscalls, all funnelling through one global kernel lock. Under that specific shape of load, more logical cores made things worse.</p><h2>What a hyperthread actually is</h2><p>An SMT sibling isn't a core. It's a second set of architectural registers bolted onto one physical core, sharing that core's execution units, its L1 and L2 caches, its TLB, and its share of memory bandwidth.</p><p>The bet SMT makes is that a single thread leaves a lot of that machinery idle. Waiting on memory, mispredicting a branch, stalling on a dependency. A second thread fills those gaps, and on a mixed workload it usually does, which is where the familiar 20-30% throughput number comes from.</p><p>That bet is about <strong>throughput</strong>. Nobody ever claimed a hyperthread makes an individual thread finish sooner. It can only make it finish later, because the core it was using alone is now shared.</p><p>Most of the time you don't care. Then you hit a lock.</p><h2>Why contention inverts the trade</h2><p>Take a spinlock, or any lock whose waiters burn CPU rather than sleeping. Two threads on the same physical core, both spinning on the same cache line.</p><p>They're now competing twice. Once for the lock, which is the competition you knew about. And once for the execution units and cache of the core they share, which is the one nobody models.</p><p>The waiter isn't idle while it spins. It's issuing loads, hammering the same cache line, consuming issue slots. Those are exactly the resources the lock <strong>holder</strong> needs to finish its critical section and release. So the waiter slows down the holder, which lengthens the critical section, which means more waiting, which means more interference.</p><p>Turn SMT off and every runnable thread gets a whole core. Fewer threads spinning at once, and the one holding the lock gets undivided execution resources to get out of the critical section. Total wait drops even though you halved the core count.</p><p>The number of logical cores went down. The rate at which the lock changes hands went up. Under contention that second number is the one that decides your latency.</p><h2>Where this shows up</h2><p>The pattern needs two things: a hot lock, and enough parallelism to keep both siblings of a core busy fighting over it. That combination is more common than it sounds.</p><p>Kubernetes nodes under container churn. CI runners, serverless backends, anything doing rapid pod turnover, where the kernel's mount and cgroup paths become the contended resource. This is the Netflix case.</p><p>JVM applications with heavily contended <code>synchronized</code> blocks, or a <code>ConcurrentHashMap</code> where the access pattern collapses onto one bucket. The JVM's biased-lock optimisations stopped helping years ago and were removed in JDK 15.</p><p>Databases under high concurrency on one hot table. Postgres buffer-mapping locks, MySQL latch contention. There's a reason database vendors have historically recommended physical-core sizing for these workloads rather than counting vCPUs.</p><p>In-memory stores with a global structure. Redis is single-threaded for command execution so it dodges this, but the surrounding I/O threads and anything doing global key-space operations do not.</p><h2>How to tell whether it's you</h2><p>The tell is that your tail latency degrades faster than your average as concurrency rises. If p50 is flat and p99 is climbing while CPU utilisation still looks reasonable, contention is a good hypothesis.</p><p>From there, <code>perf</code> will tell you where the time goes:</p><pre><code>perf lock record -a -- sleep 10
perf lock report --sort wait_total</code></pre><p>If a single lock dominates <code>wait_total</code>, you have the first of the two ingredients. Then check whether the busy threads are landing on sibling pairs:</p><pre><code>lscpu -e=CPU,CORE,SOCKET | head
# CPU 0 and CPU 24 sharing CORE 0 means those two are siblings</code></pre><p>Testing it is cheap and reversible, which is the best thing about this whole topic. You don't need a maintenance window or a new instance type:</p><pre><code>echo off | sudo tee /sys/devices/system/cpu/smt/control
# and back
echo on  | sudo tee /sys/devices/system/cpu/smt/control</code></pre><p>Run your benchmark both ways on one node. If the answer is no, you've spent ten minutes.</p><h2>The limits worth knowing</h2><p>This is not an argument that SMT is bad. For CPU-bound stateless work with little shared state, the throughput win is real and you should take it. Batch processing, video encoding, most stateless request handling that isn't fighting over anything.</p><p>The trade also moves with core count. On a machine with a small number of physical cores, halving them hurts more than the contention does, and the inversion may never show up. Netflix measured this on a 24xlarge, where there were plenty of physical cores to go around.</p><p>And there's a second reason this button exists. After Spectre, L1TF and the MDS family, several clouds already recommend SMT off for multi-tenant workloads, because sibling threads sharing a core also share the microarchitectural state those attacks read. If you're turning it off for isolation anyway, measure the performance side while you're there. It may not be the cost you budgeted for.</p><h3>Links</h3><ul><li><p><a href="https://netflixtechblog.com/mount-mayhem-at-netflix-scaling-containers-on-modern-cpus-f3b09b68beac">Mount Mayhem at Netflix (Netflix TechBlog)</a></p></li><li><p><a href="https://docs.kernel.org/admin-guide/hw-vuln/l1tf.html">Linux SMT control documentation</a></p></li><li><p><a href="https://man7.org/linux/man-pages/man1/perf-lock.1.html">perf lock (Linux manual)</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Issue #030 - Mount storm: 20,200 syscalls to start 100 pods]]></title><description><![CDATA[User namespaces made containers safer and rewrote the arithmetic of startup, and the global lock underneath is a pattern you have met before]]></description><link>https://podostack.com/p/issue-030-overlayfs-mount-storm-100-pods</link><guid isPermaLink="false">https://podostack.com/p/issue-030-overlayfs-mount-storm-100-pods</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Tue, 11 Aug 2026 14:00:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!61vC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!61vC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!61vC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!61vC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!61vC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!61vC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!61vC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!61vC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!61vC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!61vC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!61vC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21d5e8e9-1200-4ce7-805c-9597f32507d8_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The formula is the whole story, so here it is before anything else:</p><pre><code>100 containers x 2 x (1 + 50 + 50) = 20,200 mount operations</code></pre><p>That's what Netflix measured on the startup path of a hundred containers with fifty image layers each, after moving from Docker to containerd. Twenty thousand mount syscalls, every one of them taking a global kernel lock, to launch a hundred pods that between them don't mount a single volume.</p><p>Nobody wrote that. It emerged from three reasonable decisions - use user namespaces, use overlayfs, use containerd - each defensible, and the multiplication between them nowhere in any of the three designs. That combination is what makes it worth an issue: not a bug in any component, an interaction that only exists at density and only on a node packed the way real nodes get packed.</p><h2>&#127959;&#65039; Architectural Pattern: where 20,200 mounts come from</h2><h3>The security upgrade that changed the arithmetic</h3><p>Old model: a shared host UID range for every container. When a layer was unpacked, file ownership got shifted once, at untar time, on disk. Cheap, done, never thought about again. Also weak - a container escape landed you in a UID space shared with every other container on the box.</p><p>New model: each container gets its own host UID range. A break-out from one lands nowhere useful, because the identity space doesn't overlap with anything. This is a genuine improvement and it's the direction everything is going.</p><p>The kernel implements it with idmapped mounts. Rather than rewriting ownership on disk, you create a mount of the layer that presents a shifted view of ownership, per container. Per container is the load-bearing phrase. Two containers sharing an image layer can't share its idmapped view, because the whole point is that their UID ranges differ.</p><p>So for each layer, per container, the runtime performs the modern mount dance:</p><pre><code>open_tree()       # get a detached handle on the layer directory
mount_setattr()   # attach the UID/GID mapping to that handle
move_mount()      # put it where the overlay expects it</code></pre><p>Fifty layers, fifty of those. Then one overlayfs mount stacking them. Then, on teardown, fifty unmounts.</p><p>And containerd walks that path twice. Once to read user information out of the image so it knows what UID to run as, once to build the actual rootfs. Which gives <code>2 x (1 + 50 + 50)</code> = 202 mount operations per container, and a hundred containers coming up together makes 20,200.</p><h3>The lock underneath</h3><p>Mount operations aren't independent. Linux keeps the mount namespace in a global structure guarded by global locks, and every mount, unmount, and <code>move_mount</code> takes them. A hundred containers starting in parallel don't get a hundred parallel mount paths. They get one, with ninety-nine waiting.</p><p>The symptoms Netflix saw are the recognisable shape of this. Reading <code>/proc/mounts</code> taking thirty seconds and more, because generating that file walks the mount table while everyone else is mutating it. systemd falling behind processing mount events. kubelet timing out on containerd, because containerd was blocked in the kernel rather than doing anything wrong.</p><p>None of those look like "too many mounts" in a dashboard. They look like a control plane that has gone soft, and the natural response - restart kubelet, drain the node - makes it worse, because a drain is a large batch of unmounts followed by a large batch of mounts somewhere else.</p><h3>Links</h3><ul><li><p><a href="https://netflixtechblog.com/mount-mayhem-at-netflix-scaling-containers-on-modern-cpus-f3b09b68beac">Mount Mayhem at Netflix (Netflix TechBlog)</a></p></li><li><p><a href="https://man7.org/linux/man-pages/man2/mount_setattr.2.html">mount_setattr(2)</a></p></li><li><p><a href="https://news.ycombinator.com/item?id=47204203">Discussion on Hacker News</a></p></li></ul><div><hr></div><h2>&#127386; The Showdown: the hardware fix vs the arithmetic fix</h2><p>Two ways out, and the comparison is more interesting than either one alone, because they're the same two options you always get with lock contention.</p><p><strong>Make the lock cheaper.</strong> Global lock throughput depends on how fast cores can pass a cache line between them, and on a modern server that's a function of the interconnect. A monolithic mesh die and a chiplet design with cross-die hops behave very differently under heavy contention on one line, and the gap is large enough that Netflix routed demanding workloads toward the architectures that degrade more gracefully.</p><p>It works. It also has an unsatisfying property: nothing got faster, the same 20,200 operations still happen, you just bought hardware that suffers less. The bottleneck is unchanged and the next density increase finds it again.</p><p><strong>Do less work.</strong> The upstream direction is to stop mounting per layer. Map a common parent directory once, with the container's mapping, and let overlayfs address all the layers underneath it through that single idmapped view. One mount per container instead of one per layer, so the per-container cost drops from O(layers) to O(1), and the hundred-container number drops by roughly two orders of magnitude.</p><p>That's the real fix, and it's the one that keeps working when someone doubles the pod density or ships an image with ninety layers.</p><p>The comparison generalises past mounts, which is why I keep coming back to it. Kernel global lock plus linear work per unit of workload gives you a scaling limit that's completely invisible until you cross it, and then arrives all at once. Postgres had this with <code>ProcArrayLock</code> and connection counts, fixed in 14 by making snapshots scale rather than by asking people to buy better CPUs. The apiserver has a version of it in watch fan-out under write storms. The pattern is worth carrying around: when you profile a scale problem and find one lock, ask what's linear in front of it before you go shopping.</p><p>And a practical corollary. Microbenchmarks won't show you any of this. One container starting on a laptop performs the same 202 mounts and never contends, because there's nobody to contend with. Density limits have to be tested at density, on the CPU topology you actually run.</p><h3>Links</h3><ul><li><p><a href="https://docs.kernel.org/filesystems/vfs.html">Linux VFS documentation</a></p></li><li><p><a href="https://www.postgresql.org/docs/14/release-14.html">PostgreSQL 14 snapshot scalability</a></p></li></ul><div><hr></div><h2>&#128110; The Policy: layer count is a startup-latency metric</h2><p>The reader-actionable part is small and nobody does it: count the layers in your images.</p><pre><code>crane config &lt;image&gt; | jq '.rootfs.diff_ids | length'</code></pre><p>Or across everything running:</p><pre><code>kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' \
  | sort -u</code></pre><p>then run the first command over that list. Most people are surprised twice - once by the maximum, once by how many images sit above forty when the base image alone accounts for a dozen.</p><p>Layer count has been treated as an image-size concern for a decade, and on that metric it barely matters, because layers dedupe and registries cache. On mount cost it's linear and it doesn't dedupe, because idmapped views are per container by construction. That reframes the usual image-hygiene advice: multi-stage builds, collapsing <code>RUN</code> chains, and distroless bases aren't only about pull time and CVE surface. They are also about how many times the kernel takes a global lock when your node reboots.</p><p>Worth putting a check in CI. A build that adds five layers to a base image is fine; one that ships sixty because every <code>apt-get</code> got its own line is a node-density problem waiting for a busy Monday.</p><h3>The honest part</h3><p>This is a density problem and most clusters aren't dense. At thirty pods a node with twenty-layer images you're looking at roughly 1,200 mount operations spread over a rolling start, and you'll never see it. The teams that hit this are running a hundred-plus pods per node, or restarting a whole node's worth of workloads at once, or both.</p><p>The other honest bit: you probably can't apply the real fix yourself. Whether the per-layer dance collapses into one mount is decided by your container runtime and kernel version, not by anything in your manifests. What you control is the multiplier. Fewer layers, and not restarting every pod on a node simultaneously when a rolling drain would do.</p><p>If you want to know whether it's you, <code>mountsnoop</code> from bcc-tools on a node during a scale-up will tell you in a minute. Count the syscalls and compare with the formula. If the number matches, you now know exactly which term to attack.</p><h3>Links</h3><ul><li><p><a href="https://github.com/google/go-containerregistry">google/go-containerregistry (crane)</a></p></li><li><p><a href="https://github.com/iovisor/bcc">bcc-tools mountsnoop</a></p></li></ul><div><hr></div><h2>The class of bug this belongs to</h2><p>What I like about this one is that no individual decision was wrong. User namespaces per container is correct. Idmapped mounts are the right implementation. Overlayfs stacking layers is how images work. Containerd reading image config before building a rootfs is reasonable. Multiply them and you get twenty thousand syscalls through a single lock.</p><p>That's most interesting infrastructure failure, in my experience. Not a bad component, four good ones whose costs compose in a direction nobody was measuring, showing up only past a density threshold that the people who designed each piece never tested at.</p><p>I went and counted the layers in our own images after reading the Netflix writeup, expecting to feel smug. Our worst one is at forty-three. It's a Python service, and thirty of those layers are a base image that somebody assembled carefully, one dependency per layer, to get good cache behaviour in CI. Which was a completely sound decision, made for a completely different metric.</p><p>Questions? Feedback? Reply to this email. I actually read them.</p><ul><li><p>Ilia</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Instance-store NVMe: the fastest disk that erases itself]]></title><description><![CDATA[Reboot keeps your data, stop cryptographically shreds it, and the difference decides which workloads can use the cheapest fast storage in EC2]]></description><link>https://podostack.com/p/instance-store-nvme-ephemeral-on-stop</link><guid isPermaLink="false">https://podostack.com/p/instance-store-nvme-ephemeral-on-stop</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Fri, 07 Aug 2026 14:01:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Onrm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Onrm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Onrm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!Onrm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!Onrm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!Onrm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Onrm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Onrm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!Onrm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!Onrm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!Onrm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb19ff8-acdb-4a1b-93b2-404e35d5939c_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Somebody resized the node group. Bigger instance type, same everything else, a routine change done during business hours because it carries no risk.</p><p>Changing an EC2 instance type means stop, modify, start. And a stop wipes every instance-store volume attached to that host, permanently, before the instance comes back. The ClickHouse data directory was on one of them.</p><p>The cluster recovered from replicas and nobody lost a customer. What stayed with me is that nothing in the change ticket, the Terraform plan, or the console warned about it. The disk's called a volume, it mounts like a volume, <code>df</code> reports it like a volume. It just isn't one.</p><h2>Reboot keeps it, stop shreds it</h2><p>The rule has an edge that catches people, because two operations that feel equivalent are not:</p><ul><li><p>Reboot: data survives</p></li><li><p>Stop, then start: gone</p></li><li><p>Hibernate: gone</p></li><li><p>Terminate: gone</p></li><li><p>Underlying host failure: gone</p></li></ul><p>Reboot keeps the instance on the same physical host, so the disks are still right there. Stop releases the host. When you start again you get a different machine with different disks, and AWS will not carry the old ones across.</p><p>The wording in the AWS documentation is worth reading closely, because it describes a mechanism rather than a policy: when the instance is stopped, hibernated, or terminated, every block of the instance-store volume is <strong>cryptographically erased</strong>. There's no dangling copy that a support ticket can recover. The key is destroyed and the blocks become noise.</p><p>Hibernate is the one that catches people who read half the docs. Hibernation saves RAM to the root EBS volume and feels like a pause rather than a stop, so the instinct is that everything survives. The instance-store volumes stay attached and their contents are gone.</p><h2>Why it's fast, in one sentence</h2><p>The disk is on the PCIe bus of the host your instance runs on. EBS is storage over a network, however good that network is.</p><p>That's the whole difference, and everything else follows from it. No network hop means latency in microseconds rather than milliseconds, and IOPS in the hundreds of thousands to millions rather than the tens of thousands an EBS volume will give you without a provisioned-IOPS bill. You also stop paying for the IO, because there isn't anything to meter: the disk comes with the instance.</p><p>The trade is not performance versus cost. It's performance against whether the disk exists tomorrow.</p><h2>The taxonomy that actually decides it</h2><p>One question sorts almost every workload: <strong>if this disk vanished right now, who reconstructs the data, and how long does it take?</strong></p><p>If the answer is "nobody, it's gone" then instance store is wrong, whatever the benchmark says.</p><p>If the answer is "the system does it, automatically, and we've tested that" then instance store is often the best storage in EC2 for the job. Three shapes qualify.</p><p>Caches. Redis or Valkey or Memcached holding hot data that has a source of truth elsewhere. Losing it costs you a cold period, not data.</p><p>Scratch space. ETL intermediates, video encoding, model training checkpoints you can regenerate, build artifacts. Anything where the file exists to be consumed and then deleted.</p><p>Distributed stores that own their replication. ClickHouse, Cassandra, ScyllaDB, Elasticsearch data nodes. A node that loses its disk gets rebuilt from peers, which these systems are designed around rather than treating it as an incident.</p><p>The trap in that third category is the word "distributed". A three-node cluster with replication factor 1 is distributed and it'll still lose data permanently when one node stops. The property that matters is the replication factor, not the topology.</p><h2>What this means on Kubernetes</h2><p>Instance store shows up in a cluster as a node-local disk, which puts you in one of two patterns.</p><p>Either you use it as ephemeral storage for the kubelet itself, which is a straightforwardly good idea. Container images, <code>emptyDir</code>, and the writable layer all live on <code>/var/lib/kubelet</code> and <code>/var/lib/containerd</code>. Putting those on instance-store NVMe makes image pulls and container startup measurably faster, and everything there is already expected to disappear with the node.</p><p>Or you use it for a StatefulSet through a local-volume provisioner, and that's where care is needed. A <code>local</code> PersistentVolume binds a pod to a specific node. When that node goes away the PV becomes unusable and the pod stays <code>Pending</code> until somebody cleans it up. That's the correct behaviour for local storage, and it's nothing like the recovery story people expect from a PVC.</p><p>If you're on Karpenter or any consolidation-happy autoscaler, add this to the list: consolidation stops and replaces nodes. That's the same erase path as the manual resize. Workloads on instance store need to either tolerate node replacement by design, or carry a <code>karpenter.sh/do-not-disrupt</code> annotation and an explicit story for how you ever patch them.</p><h2>The limits worth knowing</h2><p>You can't attach instance store after the fact. It comes with the instance type or it doesn't, and the only way to add it is to launch a different type. That's the opposite of EBS, and it changes capacity planning: storage becomes a property of the instance family, not a dial.</p><p>Storage-optimized families are where it lives in quantity, and both the sizes and the naming move faster than any article can track. Check the current instance-type table rather than trusting a number you read somewhere, including here.</p><p>And one operational note that costs people an afternoon. On most storage-optimized types the NVMe devices arrive raw. No filesystem, no mount, no entry in <code>/etc/fstab</code>. A fresh instance has fast empty disks that nothing's using until userdata or your AMI formats and mounts them, which is exactly the kind of step that works on the golden image and gets forgotten in the Terraform module.</p><h3>Links</h3><ul><li><p><a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-store-lifetime.html">Data persistence for EC2 instance store volumes</a></p></li><li><p><a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-hibernate-overview.html">How EC2 instance hibernation works</a></p></li><li><p><a href="https://kubernetes.io/docs/concepts/storage/volumes/#local">Kubernetes local persistent volumes</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[NetworkPolicy runs after DNAT: the hairpin nobody tests]]></title><description><![CDATA[The spec declares the ordering undefined, which is why the same manifest can allow traffic in staging and drop it in production]]></description><link>https://podostack.com/p/networkpolicy-after-service-dnat-hairpin</link><guid isPermaLink="false">https://podostack.com/p/networkpolicy-after-service-dnat-hairpin</guid><dc:creator><![CDATA[Ilia Gusev]]></dc:creator><pubDate>Wed, 05 Aug 2026 14:02:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!s_k0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s_k0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s_k0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!s_k0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!s_k0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!s_k0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s_k0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png" width="1200" height="675" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:675,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!s_k0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 424w, https://substackcdn.com/image/fetch/$s_!s_k0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 848w, https://substackcdn.com/image/fetch/$s_!s_k0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 1272w, https://substackcdn.com/image/fetch/$s_!s_k0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98bcdb35-292f-4b1e-8dd1-d45061e3411b_1200x675.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The policy was the careful kind. Egress allowed to <code>0.0.0.0/0</code> on 443, with the RFC1918 ranges carved out, so pods could reach the internet but not wander around the private network. It passed review. It worked in staging for three weeks.</p><p>In production it dropped a call that had nothing to do with private networks: a pod talking to the company's own API over its public hostname.</p><p>The manifest was identical in both places. What differed was where the packet got rewritten.</p><h2>The bit the manifest doesn't tell you</h2><p>A NetworkPolicy is written against addresses. Packets get matched against addresses. In between sits the part nobody diagrams: kube-proxy, or your CNI's replacement for it, rewriting the destination.</p><p>Pod sends to <code>203.0.113.10:443</code>, the load balancer address for <code>api.example.com</code>. That address resolves back into the cluster, so the packet never leaves. A Service picks it up and DNATs the destination to a backend pod, <code>10.244.3.17:8443</code>.</p><p>Now the policy gets evaluated. So what address does it see?</p><p>If enforcement happens before DNAT, it sees <code>203.0.113.10</code> and the rule matches <code>0.0.0.0/0</code>. Allowed.</p><p>If enforcement happens after DNAT, it sees <code>10.244.3.17</code>, which lands squarely in the <code>10.0.0.0/8</code> block you carved out. Dropped.</p><p>Same manifest. Same intent. Opposite outcome, decided by a mechanism the manifest can't see.</p><h2>The spec says it's undefined</h2><p>This is where it stops being a CNI bug and starts being a design gap. From the Kubernetes NetworkPolicy documentation:</p><p>&gt; Cluster ingress and egress mechanisms often require rewriting the source or destination IP of packets. In cases where this happens, it is not defined whether this happens before or after NetworkPolicy processing, and the behavior may be different for different combinations of network plugin, cloud provider, Service implementation, etc.</p><p>And more directly:</p><p>&gt; Connections from pods to Service IPs that get rewritten to cluster-external IPs may or may not be subject to ipBlock-based policies.</p><p>"May or may not" is doing a lot of work in a security primitive. Nobody's going to fix this in a patch release, because there is nothing to fix. Two implementations can both be conformant and disagree.</p><p>GKE takes the honest route and refuses the question: in Dataplane V2 you can't put a Pod or Service IP in <code>ipBlock.cidr</code> at all. The API rejects it rather than accepting a rule whose meaning depends on packet-rewrite ordering.</p><h2>Why ipBlock is the fragile selector</h2><p>Every other NetworkPolicy selector matches on something Kubernetes owns. <code>podSelector</code> matches labels. <code>namespaceSelector</code> matches labels. Those are stable facts in etcd, and no datapath component rewrites them mid-flight.</p><p><code>ipBlock</code> matches an IP address, which is the one field in the packet that half your infrastructure exists to change. kube-proxy rewrites it. The CNI rewrites it. The cloud load balancer rewrites it. Your egress gateway rewrites it. The rule's written against the one value that's explicitly in motion.</p><p>Which gives a rule of thumb worth more than any specific workaround: <strong>use label selectors for anything inside the cluster, and treat `ipBlock` as a tool for addresses that live outside the cluster.</strong> If the target has a Pod or a Service in front of it, select the Pod.</p><p>The carve-out policy above should have been two rules. Allow egress to the API by <code>podSelector</code> plus <code>namespaceSelector</code>, since it is in the cluster. Allow egress to <code>0.0.0.0/0</code> on 443 with private ranges excluded for everything that really is outside. Once the in-cluster case is handled by labels, the DNAT ordering stops mattering, because no rewritten address is ever matched against a CIDR.</p><h2>Where the dev/prod split comes from</h2><p>The reason this passes staging is that hairpin routing is not universal.</p><p>In a small cluster, a pod reaching for the external hostname of a Service in the same cluster often gets hairpinned by the node: the packet turns around at the local datapath and gets DNATed there. In a production setup with a real cloud load balancer, the same request can leave the node, reach the LB, and come back with a rewrite performed somewhere else entirely.</p><p>Two paths, two rewrite points, one policy. The environment where you test the policy is the environment where its behaviour is defined, and only there.</p><p>Testing that catches it looks like this: from a pod in the restricted namespace, curl the target the way the application actually addresses it. Not the Service DNS name if the app uses the public hostname. Not the public hostname if the app uses the Service. The address the app uses is the only one whose rewrite path you're testing.</p><pre><code>kubectl -n restricted run probe --rm -it --image=curlimages/curl --restart=Never -- \
  curl -sS -o /dev/null -w '%{http_code} %{remote_ip}\n' --max-time 5 https://api.example.com</code></pre><p><code>%{remote_ip}</code> is the useful part. It tells you which address curl actually connected to, which tells you whether you're on the hairpin path or the external one before you start reasoning about policy.</p><h2>The limits worth knowing</h2><p>Knowing your CNI's enforcement point helps, but it isn't a general answer. Cilium enforces in eBPF at the socket and tc layers, Calico in iptables or eBPF depending on the mode, and both move that boundary between versions. Anything you learn today is a fact about the version you're on, not about the API.</p><p>There's one thing the spec does guarantee, and it surprises people the other way: traffic to and from the node a pod runs on is always allowed, whatever your rules say. A policy that looks like it blocks the node isn't blocking the node.</p><p>And the honest summary of the whole area: NetworkPolicy is a good tool for expressing which workloads may talk to which workloads. It's a poor tool for expressing which IP ranges a workload may reach, because IP ranges aren't what it's built on. When you find yourself reaching for <code>ipBlock</code> to describe something inside your own cluster, that's usually the signal to go back to labels.</p><h3>Links</h3><ul><li><p><a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">Network Policies (Kubernetes docs)</a></p></li><li><p><a href="https://cloud.google.com/kubernetes-engine/docs/how-to/network-policy">GKE Dataplane V2 network policy limitations</a></p></li><li><p><a href="https://github.com/kubernetes/kubernetes/issues/114369">NetworkPolicy tests for north/south traffic (kubernetes#114369)</a></p></li></ul>]]></content:encoded></item></channel></rss>