<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kalislav Smirnov</title>
    <description>The latest articles on DEV Community by Kalislav Smirnov (@kalikys).</description>
    <link>https://dev.to/kalikys</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149547%2F2abd2af8-3d43-4eb8-b303-e4f353e27c11.png</url>
      <title>DEV Community: Kalislav Smirnov</title>
      <link>https://dev.to/kalikys</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kalikys"/>
    <language>en</language>
    <item>
      <title>A /24 Subnet Fits 80 Pods on EKS. Your Nodes Will Be Bored.</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:01:49 +0000</pubDate>
      <link>https://dev.to/kalikys/a-24-subnet-fits-80-pods-on-eks-your-nodes-will-be-bored-138g</link>
      <guid>https://dev.to/kalikys/a-24-subnet-fits-80-pods-on-eks-your-nodes-will-be-bored-138g</guid>
      <description>&lt;p&gt;&lt;strong&gt;An m5.xlarge running 10 pods holds 30 IP addresses from your subnet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's with the defaults: the Amazon VPC CNI in secondary-IP mode and &lt;code&gt;WARM_ENI_TARGET=1&lt;/code&gt;. &lt;strong&gt;A /24 has 251 usable addresses&lt;/strong&gt;, so it's full at eight such nodes, 80 pods. If those are small 250m / 512Mi pods, the eight nodes have 62% of their CPU and 31% of their memory requested. They're bored, and you still can't schedule pod number 81. Most teams find out from a pod stuck in &lt;code&gt;ContainerCreating&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning  FailedCreatePodSandBox  kubelet
  failed to assign an IP address to container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd4ed1u7qkr6mbbthdxk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd4ed1u7qkr6mbbthdxk.jpg" alt="Is this a pigeon: me planning a 10-node EKS cluster, pointing at a /24 subnet: is this enough IPs for years?" width="557" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I went through the AWS docs, the VPC CNI source and every public IP-exhaustion story I could find, and ran the numbers for all 1,328 instance types with published ENI limits. Here's what eats your subnet, and the three ways out.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  The warm ENI nobody asked for
&lt;/h2&gt;

&lt;p&gt;With the default settings every pod gets a real VPC address from one of the node's ENIs (elastic network interfaces). &lt;strong&gt;The CNI keeps one spare ENI fully loaded&lt;/strong&gt; (&lt;code&gt;WARM_ENI_TARGET=1&lt;/code&gt;) so new pods start fast. An ENI on an m5.xlarge carries 15 addresses, so the node holds:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pods on node   ENIs in use + warm   IPs held   IPs per pod
10             1 + 1                30         3.0
20             2 + 1                45         2.3
58 (max)       4                    60         1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why would anyone want that? Because &lt;strong&gt;attaching a new ENI can take up to 10 seconds&lt;/strong&gt;, and the CNI docs say plainly that this is "the main reason why we have WARM_ENI_TARGET=1 as the default setting." Fast pod starts, paid for in addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small, half-empty nodes are the worst case.&lt;/strong&gt; At a quarter of max pods, a t3.small holds 4 IPs per running pod, a t3.medium or t3.large 3, an m5.large 2.86, and the whole xlarge class (m5, m6i, c5, r5) 2.14. Across the fleet it gets silly: &lt;strong&gt;for 926 of 1,328 instance types, a node running a single pod holds 60 or more addresses&lt;/strong&gt;, and for 604 types one full node plus its warm ENI is already bigger than a /24.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtbuuytpeumr73qh0809.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtbuuytpeumr73qh0809.jpg" alt="Panik: pods stuck in ContainerCreating. Kalm: nodes at 12% CPU, just add a node. Panik: the new node grabs 30 more IPs" width="500" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  What it looks like when it happens to someone big
&lt;/h2&gt;

&lt;p&gt;The best public write-up I found is Neon's: &lt;a href="https://neon.com/blog/aws-cni-lessons-from-a-production-outage" rel="noopener noreferrer"&gt;AWS CNI lessons from a Production Outage&lt;/a&gt; (June 2025, with a &lt;a href="https://neon.com/blog/postmortem-delayed-start-compute-operations" rel="noopener noreferrer"&gt;postmortem&lt;/a&gt;). They're not a small shop with a /24. They had &lt;strong&gt;three /20 subnets, 12,273 usable addresses&lt;/strong&gt;, in us-east-1, starting around 500 pods a minute.&lt;/p&gt;

&lt;p&gt;When the pod count grew from roughly 5,000 to 8,000, &lt;strong&gt;only 71% of the addresses were assigned to pods, but 99% were allocated to nodes.&lt;/strong&gt; One node held about 450 IPs for 200 pods. In their words, "we never free IP addresses back to the subnet." Pod starts stalled for a total of 5.5 hours over two days.&lt;/p&gt;

&lt;p&gt;The fix that sounds obvious made it worse first. With &lt;code&gt;WARM_IP_TARGET=1&lt;/code&gt;, every pod deletion put an IP into a 30-second cooldown that still counted as "warm," so new pods waited. They ended up setting all three warm targets to 0.&lt;/p&gt;

&lt;p&gt;It isn't a one-off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wealthsimple&lt;/strong&gt; watched one EKS subnet go &lt;a href="https://engineering.wealthsimple.com/adventures-in-migrating-microservices" rel="noopener noreferrer"&gt;from 80% to 100% in two weeks&lt;/a&gt; during a migration (November 2024). Short term, more subnets. Long term, custom networking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adevinta&lt;/strong&gt; got paged because &lt;a href="https://medium.com/learnings-from-the-paas/how-we-avoided-an-outage-caused-by-running-out-of-ips-in-eks-c831ab97d0e4" rel="noopener noreferrer"&gt;DaemonSets on a small, mostly unused cluster&lt;/a&gt; failed with "failed to assign an IP address to container" (2024), and later paused their own migration because addresses ran low.&lt;/li&gt;
&lt;li&gt;A VPC CNI issue from 2020 shows that event &lt;a href="https://github.com/aws/amazon-vpc-cni-k8s/issues/1077" rel="noopener noreferrer"&gt;repeated 23,123 times over 21 hours&lt;/a&gt; on m5 nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The common thread: plenty of CPU, no addresses.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  Max pods is an ENI limit, not a CPU limit
&lt;/h2&gt;

&lt;p&gt;With secondary IPs:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max pods = ENIs × (IPs per ENI − 1) + 2

m5.large    3 × (10 − 1) + 2 = 29
t3.medium   3 × (6 − 1)  + 2 = 17
m5.xlarge   4 × (15 − 1) + 2 = 58
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The "+ 2" is aws-node and kube-proxy, which run on the host network and need no pod IP. These numbers match AWS's own &lt;a href="https://github.com/aws/amazon-vpc-cni-k8s/blob/master/misc/eni-max-pods.txt" rel="noopener noreferrer"&gt;eni-max-pods.txt&lt;/a&gt; for every one of the 1,302 types that appear in both lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Past that number pods stay Pending, however much CPU is free.&lt;/strong&gt; And since EKS reserves 11 MiB of memory per possible pod, the same number also decides how much memory the node gives you. More on that here:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca" class="crayons-story__hidden-navigation-link"&gt;Your Kubernetes node is smaller than you think: 3 reservation traps in EKS, GKE and AKS&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kalikys" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149547%2F2abd2af8-3d43-4eb8-b303-e4f353e27c11.png" alt="kalikys profile" class="crayons-avatar__image" width="498" height="357"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kalikys" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Kalislav Smirnov
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Kalislav Smirnov
                
                
              
              &lt;div id="story-author-preview-content-4832394" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kalikys" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149547%2F2abd2af8-3d43-4eb8-b303-e4f353e27c11.png" class="crayons-avatar__image" alt="" width="498" height="357"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Kalislav Smirnov&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca" id="article-link-4832394"&gt;
          Your Kubernetes node is smaller than you think: 3 reservation traps in EKS, GKE and AKS
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aws"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aws&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloud&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;Here's a twist I didn't expect: &lt;strong&gt;for most clusters, max pods is not the limit that hits first.&lt;/strong&gt; With 250m / 512Mi pods, only 132 of 1,264 non-GPU instance types run out of ENI slots before they run out of CPU or memory. An m5.large fits 6 such pods by resources and 26 by ENIs. Max pods only bites with tiny pods: at 100m / 128Mi it's the limit on 620 types. &lt;strong&gt;The thing that runs out first is usually the subnet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And a confession. While checking the article, I found that my own EKS calculator got max pods wrong for 62 big instance types, the ones with several network cards, like c6in.32xlarge. &lt;strong&gt;AWS only counts the ENIs of network card 0&lt;/strong&gt;, so the right answer is 394 pods, not the 786 my code showed. It's fixed now. The AWS script got the same fix in 2024, so at least I was wrong in good company.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwji5o7sjx0ojq3nkhg6e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwji5o7sjx0ojq3nkhg6e.gif" alt="Facepalm" width="380" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Way out 1: fewer warm IPs
&lt;/h2&gt;

&lt;p&gt;The cheapest fix is a config change on the &lt;code&gt;aws-node&lt;/code&gt; DaemonSet: replace the warm ENI with &lt;code&gt;WARM_IP_TARGET&lt;/code&gt; and &lt;code&gt;MINIMUM_IP_TARGET&lt;/code&gt;. The node keeps a few spare addresses instead of a whole ENI's worth.&lt;/p&gt;

&lt;p&gt;How many pods a subnet fits, for m5.xlarge nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                      default (warm ENI)      WARM_IP_TARGET=2 +       prefix delegation
                                              MINIMUM_IP_TARGET=pods
/24, 10 pods/node      8 nodes    80 pods      22 nodes   220 pods      7 nodes    70 pods
/24, 20 pods/node      5 nodes   100 pods      11 nodes   220 pods      5 nodes   100 pods
/22, 20 pods/node     22 nodes   440 pods      46 nodes   920 pods     20 nodes   400 pods
/20, 10 pods/node    136 nodes 1,360 pods     371 nodes 3,710 pods    123 nodes 1,230 pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Same subnet, 2.7× the pods, one environment variable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The price is more EC2 API calls (the CNI docs warn about throttling with low targets) and slower starts during bursts, because addresses are attached on demand. Neon's cooldown story above is what that looks like at 500 pods a minute. There's also a long-running issue where &lt;a href="https://github.com/aws/amazon-vpc-cni-k8s/issues/1724" rel="noopener noreferrer"&gt;nodes report Ready before their IPs exist&lt;/a&gt; when these targets are set. &lt;strong&gt;Tune it, then test a burst before you trust it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Way out 2: prefix delegation (on dense nodes)
&lt;/h2&gt;

&lt;p&gt;Prefix delegation puts a /28 (16 addresses) into each ENI slot, so an m5.large can run 110 pods instead of 29. The cap is 110 for nodes with up to 30 vCPUs and 250 above that, and managed node groups enforce it. Attaching a prefix takes under a second instead of up to 10 for an ENI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But the node now takes addresses in blocks of 16, plus one warm block.&lt;/strong&gt; At 20 pods an m5.large holds 49 IPs with prefixes and 30 without. On sparse nodes, prefixes cost more addresses, not fewer, which is why the prefix column in the table above never wins on a /24. AWS's own guidance is honest about it: "If you can afford (in the worst case) up to 15 unused IPs per node, we strongly recommend using the newer prefix assignment."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It also needs contiguous free /28 blocks.&lt;/strong&gt; A fragmented subnet fails with &lt;code&gt;InsufficientCidrBlocks&lt;/code&gt; even when plenty of single addresses are free. CloudKeeper &lt;a href="https://www.cloudkeeper.com/insights/blog/why-your-amazon-eks-pods-are-stuck-pending-despite-available-ips" rel="noopener noreferrer"&gt;wrote up exactly that&lt;/a&gt;: a /25 with prefix delegation, the console showing free IPs, and zero intact /28 blocks left.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dvo9rl746idg9iuv02i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dvo9rl746idg9iuv02i.jpg" alt="Is this a pigeon: the VPC console pointing at " width="557" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A subnet CIDR reservation for prefixes avoids that. Set it up before the subnet fragments, not after.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Way out 3: custom networking
&lt;/h2&gt;

&lt;p&gt;The usual way out at scale: add a secondary VPC CIDR (AWS recommends 100.64.0.0/10) and put pods there with custom networking. &lt;strong&gt;The node's primary ENI no longer hosts pods, so max pods drops&lt;/strong&gt;: an m5.large goes from 29 to 20. Wealthsimple hit exactly that number in their migration: "large instance types are limited to 20 pods, whereas without custom networking we could run 29."&lt;/p&gt;

&lt;p&gt;You trade a few pods per node for a pod address space that's no longer shared with everything else in the VPC. &lt;strong&gt;If your subnets are already tight, it's the only fix that doesn't depend on how bursty you are.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I'd run today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Count allocated, not assigned.&lt;/strong&gt; The VPC console's "available IPs" for your node subnets is the real number. Pods-per-node math hides the warm pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at your smallest, emptiest nodes.&lt;/strong&gt; They hold the most addresses per pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change &lt;code&gt;WARM_ENI_TARGET&lt;/code&gt; deliberately.&lt;/strong&gt; Either keep it and size subnets for it, or switch to &lt;code&gt;WARM_IP_TARGET&lt;/code&gt; / &lt;code&gt;MINIMUM_IP_TARGET&lt;/code&gt; and test a burst.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix delegation for dense nodes only&lt;/strong&gt;, with a CIDR reservation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan custom networking before the migration&lt;/strong&gt;, not during it. eksctl already creates /19 subnets by default for a reason.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Do the math before the subnet does
&lt;/h2&gt;

&lt;p&gt;I built a calculator for this in NodeSize. Pick an instance type and CNI mode, enter nodes and pods per node, and it shows max pods, IPs per node including the warm pool, and whether your subnet fits: &lt;strong&gt;&lt;a href="https://nodesize.kalik8s.com/eks-max-pods/" rel="noopener noreferrer"&gt;EKS max pods and subnet IP calculator&lt;/a&gt;&lt;/strong&gt;. It covers the ENI limits of 1,328 instance types, with the formulas and AWS sources on the page (and now the right answer for multi-card instances).&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When your subnet ran dry, what did you do: bigger CIDR, prefixes, warm-IP tuning, or custom networking?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I Built a Kubernetes CVE Bot. Its First Alert Was About Eclipse Che.</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Wed, 14 Oct 2026 07:01:07 +0000</pubDate>
      <link>https://dev.to/kalikys/i-built-a-kubernetes-cve-bot-its-first-alert-was-about-eclipse-che-34h2</link>
      <guid>https://dev.to/kalikys/i-built-a-kubernetes-cve-bot-its-first-alert-was-about-eclipse-che-34h2</guid>
      <description>&lt;p&gt;&lt;strong&gt;I wanted exactly one thing: a message on my phone when a new CVE hits a version I actually run.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built a Telegram bot. It pulls the official Kubernetes CVE feed, GitHub advisories, NVD and CISA's "known exploited" list, merges them and posts to a channel. Simple, clean, exactly one job.&lt;/p&gt;

&lt;p&gt;Then I opened the channel to admire my work. The very first post was a CVE in Eclipse Che. Eclipse Che is a browser IDE. A browser IDE is not what I asked for.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Eclipse Che runs on Kubernetes. It is not Kubernetes.
&lt;/h2&gt;

&lt;p&gt;Eclipse Che is a perfectly nice browser IDE. It runs on Kubernetes. So do a lot of things. My bot did not appreciate the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NVD's keyword search for "kubernetes" happily returns anything that mentions the word somewhere&lt;/strong&gt;, in a reference link, a product list, a footnote. My bot, raised on exact string matching, took every one of those as a yes. Out of its first six posts, three went to Eclipse Che, the HyperShift operator and Apache Camel Karavan.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpi6nv6st088ist1sgxk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpi6nv6st088ist1sgxk.jpg" alt="My Kubernetes CVE bot, an Eclipse Che CVE: is this Kubernetes?" width="557" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You know the two lumberjacks. One yells "we're cutting down the wrong forest!" and the other yells back "who cares, look at the progress!". My channel was making excellent progress.&lt;/p&gt;

&lt;p&gt;The fix was boring. Components I watch by name get through from medium severity up; everything else from the wider ecosystem needs CVSS 7+ and has to talk about Kubernetes in its own first sentence. Posts that stop qualifying get deleted. (The channel shrank by half. Very professional.)&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  A CVE feed tells you everything. That's the problem.
&lt;/h2&gt;

&lt;p&gt;Even with clean data, a feed has a structural problem. &lt;strong&gt;It tells you what was published. You want to know what hits you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remember IngressNightmare? Four CVEs in ingress-nginx (CVE-2025-1097, CVE-2025-1098, CVE-2025-24514 and CVE-2025-1974), unauthenticated remote code execution. &lt;a href="https://www.wiz.io/blog/ingress-nginx-kubernetes-vulnerabilities" rel="noopener noreferrer"&gt;Wiz found&lt;/a&gt; over 6,500 clusters with the vulnerable admission controller exposed and estimated that about 43% of cloud environments were vulnerable.&lt;/p&gt;

&lt;p&gt;And what did most of us do that week? Not read the advisory. We opened the Hacker News comments to find out whether it was bad, then went to check which ingress-nginx version we had, then went looking for the chart value to bump. The advisory was open in another tab the whole time, unread, like a gym membership.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cy4jalaxlx5qynudfzk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cy4jalaxlx5qynudfzk.jpg" alt="Drake: no to reading the CVE feed every morning, yes to waiting for Hacker News to panic first" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three questions, every time: am I on an affected version, what do I upgrade to, and what's the exact command.&lt;/strong&gt; A feed answers none of them. It wasn't built to.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine. The bot asks kubectl instead.
&lt;/h2&gt;

&lt;p&gt;The new feature is a cluster snapshot. You send the bot &lt;code&gt;/cluster&lt;/code&gt;, it gives you one command to copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl version &lt;span class="nt"&gt;-o&lt;/span&gt; json
kubectl get nodes &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .items[*]}{.status.nodeInfo.kubeletVersion} {.status.nodeInfo.containerRuntimeVersion}{"\n"}{end}'&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That prints versions and image names. No secrets, no env vars, no manifests. You paste the output into the chat (or send it as a &lt;code&gt;.txt&lt;/code&gt;, Telegram cuts long messages), and the bot works out what you run from the images: the control plane, kubelet, containerd, ingress-nginx, CoreDNS, Cilium, Istio, Argo CD, cert-manager and friends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It keeps only component and version pairs.&lt;/strong&gt; Your private registry and your &lt;code&gt;payments-api&lt;/code&gt; image are read once and forgotten. &lt;code&gt;/forget&lt;/code&gt; deletes the rest. I don't want your cluster. I have enough trouble with mine.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  A boring EKS cluster: 9 components, 0 of them fine
&lt;/h2&gt;

&lt;p&gt;To test it I wrote a snapshot shaped like a very ordinary EKS cluster: Kubernetes 1.29, containerd 1.7, Argo CD 2.10, cert-manager 1.13, the community ingress-nginx, Flux, and Traefik on &lt;code&gt;latest&lt;/code&gt; because someone was in a hurry. A made-up cluster, not anyone's real one. Just the kind we all have somewhere.&lt;/p&gt;

&lt;p&gt;Here's a trimmed version of what the bot replied, run against its live data:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🛰 Cluster snapshot · Amazon EKS · 9 components
🔴 affected: 7 · ⏳ support: 5 · ✅ fine: 0 · ❔ unknown: 2

Argo CD v2.10.4: 14 CVE
  🔴 CVE-2025-55190: Argo CD's Project API Token Exposes Repository Credentials
→ upgrade to v3.2.12
helm upgrade  argo/argo-cd -n  --reuse-values --version 

ingress-nginx v1.10.1: 12 CVE
  🔴 CVE-2025-1974: ingress-nginx admission controller RCE escalation
→ upgrade to v1.13.8

⏳ Support
• Kubernetes 1.29 (EKS): support ended 2026-03-23, no more security fixes.
• ingress-nginx: retired in March 2026, no more releases or security fixes.

❔ Version unknown
• Traefik: the image tag has no version (latest or digest), check it by hand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Seven of nine components affected, five out of support, and zero "you're fine".&lt;/strong&gt; The two it couldn't read, it says so instead of guessing. I'd rather get "I don't know" than a green tick that lies.&lt;/p&gt;

&lt;p&gt;That ingress-nginx line is the one I'd frame. &lt;a href="https://www.kubernetes.io/blog/2026/01/29/ingress-nginx-statement/" rel="noopener noreferrer"&gt;Kubernetes retired Ingress NGINX in March 2026&lt;/a&gt;. One of the most widely used ingress controllers now has open CVEs and no future patches. If your cluster still runs it, the report says so in plain words, which is more than the cluster will ever do.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  A CVE list doesn't get an upgrade scheduled. $365 a month does.
&lt;/h2&gt;

&lt;p&gt;Then I tried snapshots on EKS 1.31, 1.32 and 1.33, and every one came back with this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• Kubernetes 1.32 (EKS): standard support ended 2026-03-23, now in extended support
  until 2027-03-23. That's +$365/month per cluster (+$0.50/h at AWS prices).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's straight from &lt;a href="https://aws.amazon.com/eks/pricing/" rel="noopener noreferrer"&gt;AWS pricing&lt;/a&gt;: $0.10 per cluster-hour in standard support, $0.60 in extended. &lt;strong&gt;Fifty cents an hour doesn't sound like anything until you multiply it by 730 hours and by the number of clusters nobody upgraded.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfpakq1jzd28zeh9ikg3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfpakq1jzd28zeh9ikg3.jpg" alt="This is fine: EKS 1.32, standard support ended in March. It's only fifty cents an hour." width="580" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Security people get a CVE list. Whoever pays the AWS bill gets a number. Turns out the number is what gets an upgrade on the calendar. (I'd love to say the CVEs did it. Nobody has ever rescheduled a sprint over a CVSS score.)&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  It's not Trivy (and it once flagged a 2023 bug on Kubernetes 1.34)
&lt;/h2&gt;

&lt;p&gt;It's not Trivy. Trivy and trivy-operator scan every package inside your images; this bot looks at the versions of the platform components you run. &lt;strong&gt;Different question: "which of my cluster's building blocks are affected" rather than "which library inside my app image".&lt;/strong&gt; If you run Trivy, keep running it.&lt;/p&gt;

&lt;p&gt;It also can't see what kubectl can't: runc, for example, so it tells you to run &lt;code&gt;runc --version&lt;/code&gt; on a node. Honest beats clever.&lt;/p&gt;

&lt;p&gt;And version ranges are harder than they look. During development my checker announced that Kubernetes v1.34 was vulnerable to CVE-2023-5528, a 2023 Windows bug, because one source described the affected range as "&amp;gt;= v1.8.0" with no upper end. A 2023 bug flagged on a 2025 release. (It was me. It's always the regex.) Now the fixed versions override open-ended ranges, and the bot has stopped time-travelling.&lt;/p&gt;

&lt;p&gt;For context, the biggest free option in Telegram is @cveNotify with about 19.5k subscribers. It posts every CVE in the world, unfiltered. That's a different tool for a different person, probably one with more patience than me.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it on your cluster
&lt;/h2&gt;

&lt;p&gt;The bot is &lt;a href="https://t.me/cveAlertive_bot?start=devto" rel="noopener noreferrer"&gt;@cveAlertive_bot&lt;/a&gt;. Snapshot reports are free and unlimited, you can save one cluster for free, and you get a weekly digest that says either what hit you or "nothing touched your versions this week" (the second message is the one you actually want).&lt;/p&gt;

&lt;p&gt;The paid part is the channel, 199 Telegram Stars a month: personal alerts within about half an hour when a new CVE hits a saved cluster's versions, a 🔥 follow-up when it lands in CISA's exploited list, and up to five clusters.&lt;/p&gt;

&lt;p&gt;If you liked counting money you didn't know you were spending, the previous post was about the same thing for node sizes:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca" class="crayons-story__hidden-navigation-link"&gt;Your Kubernetes node is smaller than you think: 3 reservation traps in EKS, GKE and AKS&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kalikys" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149547%2F2abd2af8-3d43-4eb8-b303-e4f353e27c11.png" alt="kalikys profile" class="crayons-avatar__image" width="498" height="357"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kalikys" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Kalislav Smirnov
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Kalislav Smirnov
                
                
              
              &lt;div id="story-author-preview-content-4832394" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kalikys" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149547%2F2abd2af8-3d43-4eb8-b303-e4f353e27c11.png" class="crayons-avatar__image" alt="" width="498" height="357"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Kalislav Smirnov&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca" id="article-link-4832394"&gt;
          Your Kubernetes node is smaller than you think: 3 reservation traps in EKS, GKE and AKS
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aws"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aws&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloud&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do you find out today that a new CVE hits a version you actually run, and how long does it take you to be sure?&lt;/strong&gt; A scanner in CI, a Slack channel nobody reads, the cloud provider's bulletin, or the Hacker News thread? If your honest answer is "Trivy already does this", say so, I want to hear that case too. And tell me what a report like this should say that mine doesn't.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Your Kubernetes node is smaller than you think: 3 reservation traps in EKS, GKE and AKS</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:02:33 +0000</pubDate>
      <link>https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca</link>
      <guid>https://dev.to/kalikys/your-kubernetes-node-is-smaller-than-you-think-3-reservation-traps-in-eks-gke-and-aks-3mca</guid>
      <description>&lt;p&gt;You buy a 16 GiB node. Kubernetes lets your pods use 11.9 GiB of it. The other 4 GiB go to kubelet reservations you never configured — and the managed services changed the rules recently, so most blog posts and calculators still show the old numbers.&lt;br&gt;
I went through the EKS nodeadm source and the current GKE and AKS docs and computed allocatable CPU and memory for every instance type. Three things surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. On EKS, prefix delegation quietly eats a gigabyte
&lt;/h2&gt;

&lt;p&gt;EKS reserves memory per possible pod: &lt;strong&gt;11 MiB × maxPods + 255 MiB&lt;/strong&gt;. With the default VPC CNI, maxPods is the ENI limit — 17 on a t3.medium. Turn on prefix delegation (to fit more pods) and maxPods jumps to 110, so the reservation jumps too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;t3.medium, default CNI: 442 MiB reserved → &lt;strong&gt;87%&lt;/strong&gt; of RAM usable- t3.medium, prefix delegation: 1465 MiB reserved → &lt;strong&gt;62%&lt;/strong&gt; of RAM usable- m5.large: 92% → 81%On small nodes you pay a gigabyte for pod slots you will never fill, because the 4 GiB box runs out of memory long before pod 110. Prefix delegation pays off on big nodes (m6i.4xlarge goes from 95.5% to 97.6%), not small ones.
## 2. GKE 1.37 gave you up to 7 GiB per node back
Until 1.36, GKE reserved a regressive share of memory: 25% of the first 4 GiB, 20% of the next 4, 10% of the next 8, 6% up to 128 GiB. From 1.37 on Container-Optimized OS it is &lt;strong&gt;min(old value, 15 MiB × maxPods + 500 MiB)&lt;/strong&gt; — about 2.1 GiB with 110 pods, whatever the node size:&lt;/li&gt;
&lt;li&gt;n2-standard-16 (64 GiB): 5.5 GiB reserved → 2.1 GiB- n2-standard-32 (128 GiB): 9.3 GiB reserved → 2.1 GiB, &lt;strong&gt;+7.2 GiB&lt;/strong&gt; for podsIf you sized node pools on the old numbers, a node pool upgrade alone may let you drop nodes. The CPU reservation is also capped at 1 vCPU now. (Small print: shared-core e2-medium still reserves 1060m of its 2 vCPUs — 940m left.)
## 3. AKS with Azure CNI Overlay reserves for 250 pods by default
Since 1.29 AKS reserves &lt;strong&gt;min(20 MB × maxPods + 50 MB, 25% of RAM)&lt;/strong&gt;. Azure CNI Overlay defaults to 250 max pods, so on a 16 GiB D4s v5 you hit the 25% cap:&lt;/li&gt;
&lt;li&gt;D4s v5, overlay default (250 pods): 4 GiB reserved → &lt;strong&gt;74%&lt;/strong&gt; usable- D4s v5, maxPods 110: 2.2 GiB reserved → &lt;strong&gt;86%&lt;/strong&gt; usableThat is 1.8 GiB per node for one flag at node pool creation. You can’t change maxPods on an existing pool — you add a new pool and migrate.
## What I do now&lt;/li&gt;
&lt;li&gt;Set maxPods to what the node can realistically run, not the maximum the CNI allows.- Size requests against kubectl describe node → Allocatable, minus DaemonSets — never against the instance spec.- Re-check after control-plane upgrades: the formula can change under you (GKE 1.37 did).I put the math for all ~2,900 AWS, GCP and Azure instance types into a free calculator: enter your pod requests and replica count, and it ranks node types by monthly cost after these reservations — &lt;a href="https://nodesize.kalik8s.com/" rel="noopener noreferrer"&gt;nodesize.kalik8s.com&lt;/a&gt;. Each instance type has its own page, e.g. &lt;a href="https://nodesize.kalik8s.com/aws/t3.medium/" rel="noopener noreferrer"&gt;t3.medium on EKS&lt;/a&gt;. Sources for every formula are on the &lt;a href="https://nodesize.kalik8s.com/formulas/" rel="noopener noreferrer"&gt;formulas page&lt;/a&gt;; if a number disagrees with your cluster, tell me and I’ll fix it.
Which reservation bit you? I’m curious whether anyone measured the GKE 1.37 change on a real cluster yet.
AI-assisted: numbers computed by a script I wrote with an AI coding assistant, checked against the sources above.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>What My AI Broke This Week #1: Nothing. It Was Me, Three Times</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Thu, 08 Oct 2026 14:31:03 +0000</pubDate>
      <link>https://dev.to/kalikys/what-my-ai-broke-this-week-1-nothing-it-was-me-three-times-4n0n</link>
      <guid>https://dev.to/kalikys/what-my-ai-broke-this-week-1-nothing-it-was-me-three-times-4n0n</guid>
      <description>&lt;p&gt;&lt;strong&gt;This week the agent broke nothing. I broke three things, and it spent the week politely proving it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup:&lt;/strong&gt; one mini-PC with Proxmox in the corner, a pile of side projects, Claude Code as the pair.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS is down (DNS was fine)
&lt;/h2&gt;

&lt;p&gt;Saturday night, half past midnight. Nothing at home resolves, so I type the obvious: "something's wrong with DNS on the server, can you look?"&lt;/p&gt;

&lt;p&gt;Three minutes later, the verdict. &lt;strong&gt;DNS was fine. One name didn't open, and it was the new monitoring dashboard.&lt;/strong&gt; The reverse proxy had 11 hosts and that one wasn't among them, so TLS died with &lt;code&gt;unrecognized name&lt;/code&gt;. The setup plan had a step 5, "add proxy host in the UI". Someone skipped it. (Me.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6ii0zcgycj6v01b0i48.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6ii0zcgycj6v01b0i48.gif" alt="It was me all along"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent didn't add it either. &lt;strong&gt;It had no login to the proxy and refused to hand-edit the config&lt;/strong&gt;, because the proxy rewrites that file from its own database. Correct, and slightly annoying, which is the right amount.&lt;/p&gt;

&lt;p&gt;Then the real outage, the five minutes when nothing resolved. The logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;04:31     Tailscale on the server changed its external addresses
04:35:09  51 DNS connections from the Mac dropped in the same second
          AdGuard: 4 days up, 0 restarts, 1 upstream timeout in 24 h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;My Mac had "Use Tailscale DNS" switched on.&lt;/strong&gt; So every lookup left the laptop, went through the tunnel to a relay in another country, came back through the subnet router, and landed on an AdGuard box three metres from my chair. When the relay hiccuped, DNS went with it. &lt;strong&gt;Every other device at home talked to AdGuard directly and never noticed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3w1sf2qc69h2fwgwv3v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3w1sf2qc69h2fwgwv3v.jpg" alt="My Mac's DNS queries, a Tailscale relay in another country, and the AdGuard box in the same room"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I'd tell another engineer: &lt;strong&gt;before you blame the DNS server, draw the path your query actually takes.&lt;/strong&gt; Mine had a layover.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  Out of memory (we had 16 GB)
&lt;/h2&gt;

&lt;p&gt;Monday evening. "The server has almost no RAM left. Find what's eating it and what we can stop."&lt;/p&gt;

&lt;p&gt;The agent stopped nothing. &lt;strong&gt;The host showed 25 of 31 GB used, with memory pressure at zero and 5.5 GB free.&lt;/strong&gt; The number was inflated by config, and the config was mine.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VM            allocated   held on host   guest actually uses
side-project  8 GB        8.3 GB         1.7 GB
render box    6 GB        6.2 GB         0.5 GB   (idle since 2 Oct)
ZFS ARC       up to 3.1   3.0 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Every VM had &lt;code&gt;balloon: 0&lt;/code&gt;.&lt;/strong&gt; A guest fills its page cache once, after a render or a build, and never hands the memory back, so the host charges the full allocation forever. That 6 GB box renders video for a side project and had last done anything three days earlier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hst4n6iyuckt1olhoyz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hst4n6iyuckt1olhoyz.jpg" alt="Is this out of memory?"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix took five minutes: smaller allocations with ballooning on, ARC capped at 2 GB, a 7.8 GB zram swap. &lt;strong&gt;The host went from 25 GB used to 14, with 16 available.&lt;/strong&gt; Then I said the words: "better commit and apply."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plan wanted to create a VM that was already running.&lt;/strong&gt; The side-project VM wasn't in Terraform state at all, so a full apply would have built a second copy on top of the live one, imported five LXC containers, rewritten the tunnel config and added four DNS records on the way. &lt;strong&gt;The agent added an import block, applied with &lt;code&gt;-target&lt;/code&gt; on the two VMs it could account for, and got "No changes" on the next plan.&lt;/strong&gt; The rest of the drift is still there, waiting for a calmer evening.&lt;/p&gt;

&lt;p&gt;What I'd tell another engineer: &lt;strong&gt;state is a story the repo tells itself. Read the plan like it's lying to you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;



&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;
  
  
  It's the network (it was the uptime)
&lt;/h2&gt;

&lt;p&gt;Some Claude Code sessions worked. Others died with &lt;code&gt;ECONNRESET&lt;/code&gt; on every retry. &lt;strong&gt;I asked the working one to debug the broken one&lt;/strong&gt;, which is a sentence I didn't expect to write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dead sessions had screenshots in them. Lots.&lt;/strong&gt; One had 28 images, about 17 MB of base64, and Claude Code resends all of it on every turn. The healthy chats carried 2 to 9 images, around 3 MB. A curl test: 1 MB fine, 20 MB fine, 8 MB died mid-stream with an HTTP/2 error, 35 MB rejected with a 413. &lt;strong&gt;The network was fine. Multi-megabyte uploads sometimes weren't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I asked whether we could report this to Anthropic and maybe get paid. &lt;strong&gt;The agent explained, kindly, that the bounty covers security bugs and this was a networking inconvenience.&lt;/strong&gt; Then it found an open issue with the same picture (46 images), drafted a comment with our numbers, and posted it from my account after I said "go".&lt;/p&gt;

&lt;p&gt;Last week I told everyone to write up their bugs instead of their feelings. &lt;strong&gt;Here I am doing it in a GitHub thread at nine on a Sunday morning&lt;/strong&gt;, so at least I'm consistent.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kalikys/the-job-market-is-brutal-your-complaint-thread-wont-fix-it-2af0" class="crayons-story__hidden-navigation-link"&gt;The job market is brutal. Your complaint thread won't fix it.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kalikys" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149547%2F2abd2af8-3d43-4eb8-b303-e4f353e27c11.png" alt="kalikys profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kalikys" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Kalislav Smirnov
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Kalislav Smirnov
                
                
              
              &lt;div id="story-author-preview-content-4818234" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kalikys" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149547%2F2abd2af8-3d43-4eb8-b303-e4f353e27c11.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Kalislav Smirnov&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kalikys/the-job-market-is-brutal-your-complaint-thread-wont-fix-it-2af0" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 8&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kalikys/the-job-market-is-brutal-your-complaint-thread-wont-fix-it-2af0" id="article-link-4818234"&gt;
          The job market is brutal. Your complaint thread won't fix it.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/career"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;career&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/writing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;writing&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/kalikys/the-job-market-is-brutal-your-complaint-thread-wont-fix-it-2af0" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/kalikys/the-job-market-is-brutal-your-complaint-thread-wont-fix-it-2af0#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;By lunchtime a bigger session died, half a million tokens of context, and &lt;code&gt;/compact&lt;/code&gt; died with it, because compaction sends the same giant request. &lt;strong&gt;Another issue, another thread of Mac users, and what fixed it for them was a reboot.&lt;/strong&gt; One had 52 days of uptime; a restart brought back all eight of his dead sessions, one of them at 826k tokens.&lt;/p&gt;

&lt;p&gt;The agent then checked mine. &lt;strong&gt;102 days.&lt;/strong&gt; I had been quietly proud of that number.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqcmwjbffyp831xn1s7k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqcmwjbffyp831xn1s7k.jpg" alt="Reboot the Mac, or spend a morning proving to GitHub that the API drops my 17 MB of screenshots"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I'd tell another engineer: &lt;strong&gt;an agent can prove the network innocent ten different ways. It still won't tell you to turn it off and on again&lt;/strong&gt;, because you taught it not to be rude.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsvmyva1qpt1nfm2ipjoi.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsvmyva1qpt1nfm2ipjoi.gif" alt="Hello, IT. Have you tried turning it off and on again?"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What did your AI pair spend this week proving wasn't its fault, and was it right?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>claude</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The job market is brutal. Your complaint thread won't fix it.</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:55:08 +0000</pubDate>
      <link>https://dev.to/kalikys/the-job-market-is-brutal-your-complaint-thread-wont-fix-it-2af0</link>
      <guid>https://dev.to/kalikys/the-job-market-is-brutal-your-complaint-thread-wont-fix-it-2af0</guid>
      <description>&lt;p&gt;Four of this week's front-page posts are about the job search. One calls the market disgusting. I'm in the same market, applying for DevOps and platform roles, so this isn't shouted from the shore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The market really is bad
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22% fewer&lt;/strong&gt; software development postings on Indeed in the US than in February 2020 (index at 78 on 2 October, &lt;a href="https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE" rel="noopener noreferrer"&gt;FRED&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;61%&lt;/strong&gt; of job seekers ghosted after an interview; &lt;strong&gt;18 to 22%&lt;/strong&gt; of jobs on Greenhouse's own platform classified as ghost jobs in any quarter (&lt;a href="https://www.greenhouse.com/blog/greenhouse-2024-state-of-job-hunting-report" rel="noopener noreferrer"&gt;Greenhouse, 2024&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;37%&lt;/strong&gt; of job seekers say they apply to more jobs than ever and hear back less; &lt;strong&gt;73%&lt;/strong&gt; of HR professionals say under half of applications meet the listed criteria (&lt;a href="https://news.linkedin.com/2025/talent-campaign-b2b-press-release" rel="noopener noreferrer"&gt;LinkedIn, January 2025&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22%&lt;/strong&gt; of candidates use bots to apply automatically (&lt;a href="https://www.greenhouse.com/blog/greenhouse-2025-workforce-hiring-report" rel="noopener noreferrer"&gt;Greenhouse, 2025&lt;/a&gt;); Greenhouse's own explanation for the ghosting is employers drowning in AI applications.&lt;/li&gt;
&lt;li&gt;One dev.to author sent &lt;a href="https://dev.to/mikachu/i-applied-to-59-tech-jobs-in-14-days-heres-what-actually-happened-46lp"&gt;59 applications in 14 days&lt;/a&gt;. &lt;strong&gt;51&lt;/strong&gt; got no answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody who has seen these numbers gets to say "try harder". The market is unfair, and good engineers got laid off for reasons unrelated to their work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The post doesn't help you either
&lt;/h2&gt;

&lt;p&gt;I pulled this week's dev.to posts under &lt;code&gt;career&lt;/code&gt; and &lt;code&gt;jobsearch&lt;/code&gt; and kept the 43 with "job", "interview", "market", "rejected" or "ghost" in the title.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;This week on dev.to&lt;/th&gt;
&lt;th&gt;Posts&lt;/th&gt;
&lt;th&gt;Zero reactions&lt;/th&gt;
&lt;th&gt;10+ reactions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Job-search posts&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;devops&lt;/code&gt;, &lt;code&gt;kubernetes&lt;/code&gt;, &lt;code&gt;sre&lt;/code&gt;, &lt;code&gt;terraform&lt;/code&gt; posts&lt;/td&gt;
&lt;td&gt;292&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So yes, most of everything gets ignored, mine included. The difference is what happens after. An ignored Terraform post still answers someone's search in six months and still sits under your name when a recruiter looks. An ignored rant does neither. And the two job-search posts that did break ten reactions weren't rants. (The "disgusting" post is tagged &lt;code&gt;jobmarket&lt;/code&gt;, so it isn't in the table; it had 23 reactions the morning after. Fair enough. That's still sympathy from people who can't hire you.)&lt;/p&gt;

&lt;p&gt;And look who's in the thread. Other job seekers. The hiring manager isn't, and if she wanders in she learns that you're frustrated, which she assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a stranger sees
&lt;/h2&gt;

&lt;p&gt;Picture a recruiter with your name in a search box and four minutes before the next call. She finds three posts about ghosting and nothing you built. Next tab.&lt;/p&gt;

&lt;p&gt;Same recruiter, same four minutes, and the first result is a README that explains one decision. She reads it. Maybe she writes. That's the gap between "unread" and "read", the only gap a post of yours can close.&lt;/p&gt;

&lt;h2&gt;
  
  
  It doesn't have to be impressive
&lt;/h2&gt;

&lt;p&gt;It has to be specific. My home lab is one mini-PC running Proxmox, which is nothing. The README line I'd point a reader at says that changing a container's network interface through the Terraform provider restarts the container even though the plan shows an in-place update. One sentence, and it says more about how I work than a cover letter.&lt;/p&gt;

&lt;p&gt;An incident write-up with a timeline. A comment that politely corrects someone's YAML. A write-up from March still gets found in October; a rant from March is gone by Tuesday.&lt;/p&gt;

&lt;p&gt;One more Greenhouse number: &lt;strong&gt;28%&lt;/strong&gt; of candidates admit to using AI to generate fake work samples. A repo with a commit history and a README that explains why beats a polished sample because it's hard to fake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops working
&lt;/h2&gt;

&lt;p&gt;No time: skip the homelab. One write-up of a problem from your last job, names filed off, thirty minutes. Less than the forms nobody reads.&lt;/p&gt;

&lt;p&gt;Burned out: rest, and ignore everyone telling you to build in public, me included. This is for people who still have the energy for a thousand-word complaint.&lt;/p&gt;

&lt;p&gt;Visa, location, a role that doesn't exist in your city: proof of work gets you read. It doesn't get you past a wall the hiring side can't move either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ask
&lt;/h2&gt;

&lt;p&gt;Keep writing. Change the ratio. For every post about the search, two about the work: the bug, the migration, the thing that broke on Friday. If the market is still like this in a year, you'll have a year of visible work instead of a year of visible frustration. Only one of those is something a hiring manager can act on.&lt;/p&gt;

&lt;p&gt;Tell me I'm wrong in the comments. Or better: drop the one write-up you'd want a recruiter to find first. I'll read every one.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;FRED, "Software Development Job Postings on Indeed in the United States" (IHLIDXUSTPSOFTDEVE), index Feb 1 2020 = 100, observation 2 October 2026. &lt;a href="https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE" rel="noopener noreferrer"&gt;https://fred.stlouisfed.org/series/IHLIDXUSTPSOFTDEVE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Greenhouse, "Ghosting, ghost jobs and bots: Candidates reveal their top challenges in the Greenhouse 2024 State of Job Hunting report", 10 December 2024. &lt;a href="https://www.greenhouse.com/blog/greenhouse-2024-state-of-job-hunting-report" rel="noopener noreferrer"&gt;https://www.greenhouse.com/blog/greenhouse-2024-state-of-job-hunting-report&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Greenhouse, "Fighting bias, bots and burnout: Global job market trends from the Greenhouse 2025 Workforce &amp;amp; Hiring Report". &lt;a href="https://www.greenhouse.com/blog/greenhouse-2025-workforce-hiring-report" rel="noopener noreferrer"&gt;https://www.greenhouse.com/blog/greenhouse-2025-workforce-hiring-report&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LinkedIn, "More Applicants But Harder To Hire: Majority Of Job Applications Do Not Meet Listed Criteria, Finds LinkedIn", 15 January 2025. &lt;a href="https://news.linkedin.com/2025/talent-campaign-b2b-press-release" rel="noopener noreferrer"&gt;https://news.linkedin.com/2025/talent-campaign-b2b-press-release&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"I Applied to 59 Tech Jobs in 14 Days. Here's What Actually Happened", dev.to, 1 October 2026. &lt;a href="https://dev.to/mikachu/i-applied-to-59-tech-jobs-in-14-days-heres-what-actually-happened-46lp"&gt;https://dev.to/mikachu/i-applied-to-59-tech-jobs-in-14-days-heres-what-actually-happened-46lp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;dev.to API, &lt;code&gt;/api/articles?top=7&amp;amp;tag=...&lt;/code&gt;, pulled 8 October 2026; counts in the table are from that pull.&lt;/li&gt;
&lt;li&gt;My home lab repository, README note on Terraform and container network interfaces. &lt;a href="https://github.com/kalikys/homelab" rel="noopener noreferrer"&gt;https://github.com/kalikys/homelab&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>discuss</category>
      <category>writing</category>
      <category>devops</category>
    </item>
    <item>
      <title>A denylist let 46 of 75 prompt injections through. Capabilities let 3.</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Sat, 03 Oct 2026 16:40:44 +0000</pubDate>
      <link>https://dev.to/kalikys/a-denylist-let-46-of-75-prompt-injections-through-capabilities-let-3-6d1</link>
      <guid>https://dev.to/kalikys/a-denylist-let-46-of-75-prompt-injections-through-capabilities-let-3-6d1</guid>
      <description>&lt;p&gt;Two documents appeared on 8 September. An arXiv paper measures what happens when a coding agent reads a poisoned repository under four permission models. A Google threat intelligence report describes malware that, inside GitHub Actions, reads the runner's OIDC token out of memory and publishes packages with valid SLSA Build Level 3 attestations. Together they say something uncomfortable about how most teams give agents permissions today, which is by writing sentences.&lt;/p&gt;

&lt;p&gt;My position: in a pipeline, an agent's permissions are whatever the runner job holds. A rule in &lt;code&gt;CLAUDE.md&lt;/code&gt; that says "do not push to main" is a request to a reader, and the attacker can write to that file too. If the agent job holds &lt;code&gt;id-token: write&lt;/code&gt;, the agent can publish. If it holds &lt;code&gt;actions: write&lt;/code&gt;, it can delete the logs that would show it did. Permission has to be a property of the job, fixed before the agent reads a single untrusted byte. This is commentary on the two documents linked below; I have not reproduced either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Google report shows
&lt;/h2&gt;

&lt;p&gt;Most of &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/from-prompting-to-autonomy-the-evolution-of-adversarial-ai" rel="noopener noreferrer"&gt;GTIG's "From Prompting to Autonomy"&lt;/a&gt; is about attackers using agents. The part that belongs on an operations desk is the section on UNC6780, which has been compromising PyPI, npm and Docker Hub packages since March 2026, and its credential stealer DUSTMAKER.&lt;/p&gt;

&lt;p&gt;The malware checks where it is: "DUSTMAKER samples contain functionality to detect when it is running in a continuous integration and continuous delivery (CI/CD) environment. If confirmed, it extracts OIDC tokens from the process memory of GitHub Actions runners." With those tokens it "authorizes itself as a trusted publisher and publishes compromised versions of packages with valid, cryptographically signed SLSA Build 3 attestations", and such packages "will pass AI coding agent automated trust checks."&lt;/p&gt;

&lt;p&gt;It hides where agents look: it "drops or modifies malicious files into hidden project workspace directories for AI coding assistants and integrated development environments (IDEs) (.claude/, .vscode/, .cursor/, etc.)" and uses them "to instruct the AI assistant to run arbitrary commands or scripts". In Actions it "creates malicious pipeline tasks disguised under AI-themed names such as 'Copilot Setup'" and "issues automated API calls to delete the workflow execution logs."&lt;/p&gt;

&lt;p&gt;Last week I wrote that signature verification has to live in the harness. This report is the other half: a signature is evidence about who held the token, and when the token came out of your own runner the attestation is valid and tells you nothing.&lt;/p&gt;

&lt;p&gt;The same report describes an actor who ran a credential-harvesting campaign in under six hours from "an AI coding chatbot, a prompt, and a set of agent instructions", with "preconfigured markdown instruction sets as operational playbooks." Their agents are configured the way ours are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not show
&lt;/h2&gt;

&lt;p&gt;I could not find a count of repositories hit by DUSTMAKER or what the stolen tokens were scoped to when used. "Thousands of third-party credentials" is a magnitude, and the victim is unnamed. So I cannot tell you how likely this is to hit a given pipeline, only that it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the paper shows
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2609.08371" rel="noopener noreferrer"&gt;Bouras, Dai and Mechtaev, "Authority Is Not a String"&lt;/a&gt; (8 September 2026, accepted at LMPL '26) built a harness they call CapScope for the Pi coding agent. Before the agent reads any repository content, a preflight step looks only at the trusted user request and the file tree and sets a ceiling of typed capabilities, read, write and exec, each scoped to a path or argument prefix. The capabilities live in a host-side store the model cannot see or edit. Every tool call is checked against the store of the agent that proposed it, compound commands segment by segment. Sub-agents get a subset: the test runner gets exec, the patcher gets write, and a write proposed by the runner is refused even when it would be useful.&lt;/p&gt;

&lt;p&gt;They ran five Python repair tasks with injections in README, AGENTS.md, skill files, source comments and tool output. 300 runs. Under ambient authority the injected effect executed in 47 of 75 runs. Under a static denylist of the familiar hazards (&lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.ssh&lt;/code&gt;, recursive delete, &lt;code&gt;sudo&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;) it executed in 46 of 75. A policy written for the specific task, shared by all agents: 33 of 75. CapScope: 3 of 75. Repairs still completed in 68 of 75 runs, against 72 with no protection at all.&lt;/p&gt;

&lt;p&gt;The denylist number is the one I keep coming back to. That is the model most teams actually run, a list of things the agent must not do, written once and applied everywhere. It stopped one more attack than nothing, because the injected target was an ordinary source path no deny pattern matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the paper does not show
&lt;/h2&gt;

&lt;p&gt;It is small: five tasks, one language, one agent, no CI/CD setting. Mean wall-clock time was 316 seconds per run against 145 without the harness, five runs hitting a 900-second timeout because the model kept proposing the refused call. The authors draw the boundary themselves: "A permitted read followed by a permitted write can still move data, and an injection can misuse authority deliberately granted to its reader. Those cases require complementary information-flow controls or sandboxing." Running &lt;code&gt;pytest&lt;/code&gt; still imports untrusted project code. The 3 of 75 is a result about authority, and says nothing about confidentiality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whether this matters in two years
&lt;/h2&gt;

&lt;p&gt;I think the capability model wins, as a product default rather than something teams build. GitHub's own &lt;a href="https://github.github.com/gh-aw/introduction/architecture/" rel="noopener noreferrer"&gt;agentic workflows architecture&lt;/a&gt; already describes it: "the agent job runs with minimal read-only permissions, while write operations are deferred to separate jobs", with outputs buffered as artifacts and applied by a job holding &lt;code&gt;issues: write&lt;/code&gt; or &lt;code&gt;contents: write&lt;/code&gt;, and egress through a proxy with a domain allowlist. That is CapScope's split with CI job boundaries doing the enforcement. In two years I expect every hosted agent runner to look like this, and instruction files to remain as a convenience for the agent and nothing more.&lt;/p&gt;

&lt;p&gt;The DUSTMAKER case will not go away, because it does not need the agent to misbehave. It needs a runner that holds a publishing token while executing repository code, which was a pipeline design problem before agents arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday
&lt;/h2&gt;

&lt;p&gt;On a secure-development platform I ran Gitleaks, Trivy and SCA in every pipeline through shared GitLab CI templates. The template is where an agent job's permissions belong too, because the platform team owns it and the repository the agent is about to read does not.&lt;/p&gt;

&lt;p&gt;Give the token to the job that needs it. GitHub's documentation says to set &lt;code&gt;id-token: write&lt;/code&gt; inside the single job that fetches the token rather than at workflow level; GitLab's &lt;code&gt;id_tokens&lt;/code&gt; is a job keyword. The agent job should not have it. Publishing happens after review, in a different job, on a different runner.&lt;/p&gt;

&lt;p&gt;Separate reading from writing. If the agent runs with &lt;code&gt;contents: read&lt;/code&gt; and emits a patch as an artifact, a later job with write permission applies it. Whatever the agent is told, it cannot push, publish or approve.&lt;/p&gt;

&lt;p&gt;Put &lt;code&gt;.claude/&lt;/code&gt;, &lt;code&gt;.cursor/&lt;/code&gt;, &lt;code&gt;.vscode/&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt; under CODEOWNERS and review them like pipeline YAML, because that is what they are now. In CI, load them from the protected branch or not at all.&lt;/p&gt;

&lt;p&gt;Alert on log deletion. It needs write access to Actions; the agent's token should not have it, and a deletion in the audit log should page someone.&lt;/p&gt;

&lt;p&gt;Use ephemeral runners. &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/mitigation-guidance-for-supply-chain-compromise" rel="noopener noreferrer"&gt;Google's July mitigation guidance&lt;/a&gt; asks for "ephemeral runners for build pipelines that are purged immediately after completing a single task" and for federated credentials that "expire in a matter of minutes". A token read from memory is worth less when the process that held it is already gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would change my mind
&lt;/h2&gt;

&lt;p&gt;A replication of CapScope on a real CI workflow with a publishing step, showing the preflight ceiling is either too wide to matter or too narrow to let work through. Or incident data showing job-scoped, short-lived OIDC tokens abused at a rate close to long-lived PATs, which would mean the job boundary is not the control I think it is.&lt;/p&gt;

&lt;p&gt;What does the agent job in your pipeline hold today, and if the answer is "the same as the rest of the workflow", what is stopping it from publishing?&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Google Threat Intelligence Group. "From Prompting to Autonomy: The Evolution of Adversarial AI." 8 September 2026. &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/from-prompting-to-autonomy-the-evolution-of-adversarial-ai" rel="noopener noreferrer"&gt;https://cloud.google.com/blog/topics/threat-intelligence/from-prompting-to-autonomy-the-evolution-of-adversarial-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Dimitrios Stamatios Bouras, Yihan Dai, Sergey Mechtaev. "Authority Is Not a String: A Capability-Scoped Harness for Prompt-Injection-Resistant Coding Agents." arXiv:2609.08371, 8 September 2026. &lt;a href="https://arxiv.org/abs/2609.08371" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.08371&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Agentic Workflows, architecture page. &lt;a href="https://github.github.com/gh-aw/introduction/architecture/" rel="noopener noreferrer"&gt;https://github.github.com/gh-aw/introduction/architecture/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Threat Intelligence Group. "Mitigation guidance for supply chain compromise." 31 July 2026. &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/mitigation-guidance-for-supply-chain-compromise" rel="noopener noreferrer"&gt;https://cloud.google.com/blog/topics/threat-intelligence/mitigation-guidance-for-supply-chain-compromise&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Docs, OpenID Connect reference (&lt;code&gt;id-token: write&lt;/code&gt; per job). &lt;a href="https://docs.github.com/en/actions/reference/security/oidc" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/reference/security/oidc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Docs, permissions required for fine-grained tokens (delete workflow run logs: Actions, write). &lt;a href="https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens" rel="noopener noreferrer"&gt;https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitLab Docs, CI/CD YAML &lt;code&gt;id_tokens&lt;/code&gt;. &lt;a href="https://docs.gitlab.com/ci/yaml/#id_tokens" rel="noopener noreferrer"&gt;https://docs.gitlab.com/ci/yaml/#id_tokens&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>discuss</category>
      <category>ai</category>
      <category>cicd</category>
    </item>
    <item>
      <title>An SBOM is a tool output, not a fact about your software</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Sat, 03 Oct 2026 16:40:20 +0000</pubDate>
      <link>https://dev.to/kalikys/an-sbom-is-a-tool-output-not-a-fact-about-your-software-3j2g</link>
      <guid>https://dev.to/kalikys/an-sbom-is-a-tool-output-not-a-fact-about-your-software-3j2g</guid>
      <description>&lt;p&gt;Two papers appeared on arXiv in September that describe, from opposite ends, a pipeline control I have shipped many times. The first, from Inria and ANSSI, ran three SBOM generators over the same 3,326 repositories and found that two of them miss more than half of the dependencies in JavaScript projects. The second ran 1,920 trials of AI coding assistants installing software and found that they opened an SBOM, signature or attestation before installing in 0.5% of trials and verified one in none.&lt;/p&gt;

&lt;p&gt;My position: an SBOM generated in CI is evidence about the tool you ran, at the version you ran it, under the flags you set. It is weak evidence about the software. That still leaves it useful, but the thing to put under version control and defend in an audit is the generation recipe. The component count on its own means little.&lt;/p&gt;

&lt;p&gt;This is commentary on those two papers, linked where used; I have not reproduced them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Inria paper shows
&lt;/h2&gt;

&lt;p&gt;[Prado, Zendra, Boinot and Barais](&lt;a href="https://dub.sh/2DBS8sB" rel="noopener noreferrer"&gt;https://dub.sh/2DBS8sB&lt;/a&gt; (17 September 2026, accepted at SCORED 2026) took 2,050 JavaScript and 1,276 Rust repositories with over 1,000 GitHub stars, regenerated a fresh lockfile for each, and used that lockfile as ground truth. They ran Syft 1.38.2, Trivy 0.68.2 and cdxgen 12.0.0, all producing CycloneDX 1.6, and compared.&lt;/p&gt;

&lt;p&gt;With all lockfiles exposed, Syft reported 43.78% of the lockfile packages on JavaScript and Trivy 32.71%. On Rust, Syft reached 100% and Trivy 83.85%. cdxgen was at 98.72% and 99.34%. With no lockfile at all, Syft and Trivy produced an empty SBOM, and the paper is specific that this happens "with no error or warning shown to the operator."&lt;/p&gt;

&lt;p&gt;The useful part is why. Most of the JavaScript gap is devDependencies. Syft excludes them by design, but only for some package managers: in one npm project it reported 2249 of 2249 production packages and 0 of 4930 dev packages, while in a Yarn and a pnpm project it reported both at 100%. Trivy excludes them by default too, but the exclusion stops at the first level, so a transitive dependency of a dev package can show up attached to the root as if it were a top-level production dependency. The authors call Syft's behaviour a design choice applied inconsistently and Trivy's an implementation error. cdxgen's small residual gap was a hard-coded list that skips any directory named &lt;code&gt;examples&lt;/code&gt;, along with &lt;code&gt;docs&lt;/code&gt;, &lt;code&gt;tests&lt;/code&gt; and &lt;code&gt;.github&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same package also comes out with a different identifier depending on the tool. An npm alias is reported under the alias by cdxgen and Trivy and under the real scoped name by Syft. pnpm peer-dependency suffixes end up in the version string for cdxgen and Syft. One git-pinned dependency came back as a commit hash from one tool, a tarball URL from the second and a semantic version from the third. Vulnerability matching works on these identifiers, so the same code scanned with two tools can produce two different vulnerability lists.&lt;/p&gt;

&lt;p&gt;The Supplier element, one of NTIA's minimum elements since 2021, is populated by none of the three tools on either ecosystem. Licenses: 75.8% for cdxgen, 60.2% for Syft, 0% for Trivy on JavaScript, and 0% for all three on Rust. Hashes come almost only from cdxgen, CPEs almost only from Syft.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not show
&lt;/h2&gt;

&lt;p&gt;The sample is popular GitHub repositories, and the authors say they did not measure how the 1,000-star threshold affects coverage. The results are tied to three specific versions; the authors note that a 2026 study using Trivy 0.66.0 and Syft 1.33.0 got empty SBOMs for &lt;code&gt;package-lock.json&lt;/code&gt;, and a few releases later the same tools produce partial ones. Trivy 0.75.0 and Syft 1.54.0 both shipped on 1 October, so some of these numbers may already be stale. The field-completeness measurement checks whether a field is present, not whether its content is right. And the paper covers JavaScript and Rust only; I would not assume Java or Python look better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the second paper adds
&lt;/h2&gt;

&lt;p&gt;[Pengyin Shan's pre-registered audit](&lt;a href="https://dub.sh/vp0XYGh" rel="noopener noreferrer"&gt;https://dub.sh/vp0XYGh&lt;/a&gt; (7 September 2026) asks the other question: when the thing installing software is an AI assistant, does it read any of this? Six research software projects, nine variants each (no signal, a valid SBOM, a valid signature, a valid attestation, two with wrong-issuer material, one with every signal, one with contradictory metadata), three models, two harnesses, every trial in a network-less container with file access and commands logged, protocol deposited with a DOI before the first trial.&lt;/p&gt;

&lt;p&gt;In 9 of 1,920 trials the assistant opened a provenance file before installing. In 0 of 2,114 trials, including a frontier-model supplement, did any assistant run a verification command. Signal presence had no measurable effect (p = 0.50). The model that cost $1.00 per trial verified nothing in 50 trials; the only one that opened signals more than once cost about $0.10 per trial and did it in 9 of 360. The author's conclusion is that verification "has to be built into the program that runs the assistant."&lt;/p&gt;

&lt;p&gt;The limits are real: six projects, research software, a container with no package index so most installs failed at dependency resolution, and material signed by the author's own key rather than the upstream's. It measures the decision to install, not a successful install. Still, zero verification commands across six models and two harnesses is hard to explain away with sample size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;The CRA's main obligations apply from 11 December 2027, with reporting obligations from 11 September 2026, [per the Commission's own page](&lt;a href="https://dub.sh/iIYkCB9" rel="noopener noreferrer"&gt;https://dub.sh/iIYkCB9&lt;/a&gt; Most manufacturers will discharge that with one of these three tools, and the Inria authors put it bluntly: tool choice "is no longer just an engineering decision, it is a compliance-exposure question."&lt;/p&gt;

&lt;p&gt;I think SBOMs matter in two years as an inventory you can defend, and I doubt they will matter as a trust signal anyone checks at install time. The second paper shows the install-time consumer is already often an agent, and the agent does not look. The first shows that a human who looks cannot tell from the document whether it is complete. The fix for the first is standards work: the authors ask CycloneDX and SPDX to define which dependency scopes are in, how provenance is recorded, and what the root of the graph is, which will take years. The fix for the second is a harness that runs &lt;code&gt;cosign verify&lt;/code&gt; or &lt;code&gt;slsa-verifier&lt;/code&gt; before &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday
&lt;/h2&gt;

&lt;p&gt;I have run security checks (Gitleaks, Trivy, SCA) in every pipeline of a secure development platform through shared GitLab CI templates, and I have delivered signed packages into an air-gapped bank, where the receiving side cannot regenerate anything and works with what is in the bundle. This is what I would change this week.&lt;/p&gt;

&lt;p&gt;Pin the generator and write its version into the SBOM metadata. The paper's results changed between minor releases of the same tool. If an auditor asks why a component is missing, "Trivy 0.68.2 with default flags on a Yarn project" is an answer. "We run Trivy" is not.&lt;/p&gt;

&lt;p&gt;Fail the job on an empty SBOM. Syft and Trivy return zero components and exit 0 when there is no lockfile. A &lt;code&gt;jq '(.components // []) | length'&lt;/code&gt; with a threshold is five lines in a shared template and catches the case the paper calls more severe than under-reporting.&lt;/p&gt;

&lt;p&gt;Decide the dev-dependency question on purpose. Trivy's &lt;code&gt;--include-dev-deps&lt;/code&gt; exists; the default excludes them, and the exclusion leaks. For a deployment SBOM you may want them out. For a view of your build's supply chain you want them in, since the paper notes development dependencies have themselves been targets of supply-chain attacks. Write the choice down.&lt;/p&gt;

&lt;p&gt;Commit the lockfile, or generate the SBOM from the built artifact instead of the source tree. Without a resolved lockfile the tools are either guessing or silent.&lt;/p&gt;

&lt;p&gt;Run two generators once, on your five most important repositories, and diff the component sets. An afternoon of that tells you which scope and identifier rules bite your stack, before you promise anyone a complete inventory.&lt;/p&gt;

&lt;p&gt;For air-gapped delivery, put the SBOM inside the signed bundle together with the generator name, version and command line, and have the receiving side verify the bundle signature. The SBOM records what you believe is in there; the signature records who is accountable if that belief is wrong.&lt;/p&gt;

&lt;p&gt;If an AI agent sits anywhere in your dependency flow, put the verification step in the pipeline that runs it, because the model will not do it on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would change my mind
&lt;/h2&gt;

&lt;p&gt;A benchmark that pins these edge cases, which the Inria authors say they are building, with Syft and Trivy above 95% on JavaScript. A CycloneDX or SPDX release that defines dependency scope as a standard attribute, so "complete" means one thing across tools. Or a re-run of the agent study in which a production harness verifies attestations by default and leaves a log.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Alan Prado, Olivier Zendra, Philippe Boinot, Olivier Barais. "Mind the Gap: How SBOM Specification Ambiguities Lead to Divergent Software Bills of Materials. An Empirical Tool Study." arXiv:2609.19920, 17 September 2026. &lt;a href="https://dub.sh/ca1sWHN" rel="noopener noreferrer"&gt;https://dub.sh/ca1sWHN&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pengyin Shan. "Do AI Coding Assistants Check Before They Install? A Pre-Registered Demand-Side Audit of Trust Signals in the Research Software Supply Chain." arXiv:2609.07754, 7 September 2026. &lt;a href="https://dub.sh/QX6af62" rel="noopener noreferrer"&gt;https://dub.sh/QX6af62&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;European Commission, "Cyber Resilience Act" policy page (entry into force and application dates). &lt;a href="https://dub.sh/mkcsQtY" rel="noopener noreferrer"&gt;https://dub.sh/mkcsQtY&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Trivy documentation, Node.js coverage (&lt;code&gt;--include-dev-deps&lt;/code&gt;). &lt;a href="https://dub.sh/qeU8iuX" rel="noopener noreferrer"&gt;https://dub.sh/qeU8iuX&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release pages checked 2 October 2026: &lt;a href="https://dub.sh/274rekh" rel="noopener noreferrer"&gt;https://dub.sh/274rekh&lt;/a&gt; and &lt;a href="https://dub.sh/bSvz3pX" rel="noopener noreferrer"&gt;https://dub.sh/bSvz3pX&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>cicd</category>
      <category>sre</category>
    </item>
    <item>
      <title>An SBOM is a tool output, not a fact about your software</title>
      <dc:creator>Kalislav Smirnov</dc:creator>
      <pubDate>Fri, 02 Oct 2026 15:06:25 +0000</pubDate>
      <link>https://dev.to/kalikys/an-sbom-is-a-tool-output-not-a-fact-about-your-software-i6o</link>
      <guid>https://dev.to/kalikys/an-sbom-is-a-tool-output-not-a-fact-about-your-software-i6o</guid>
      <description>&lt;p&gt;Two papers appeared on arXiv in September that describe, from opposite ends, a pipeline control I have shipped many times. The first, from Inria and ANSSI, ran three SBOM generators over the same 3,326 repositories and found that two of them miss more than half of the dependencies in JavaScript projects. The second ran 1,920 trials of AI coding assistants installing software and found that they opened an SBOM, signature or attestation before installing in 0.5% of trials and verified one in none.&lt;/p&gt;

&lt;p&gt;My position: an SBOM generated in CI is evidence about the tool you ran, at the version you ran it, under the flags you set. It is weak evidence about the software. That still leaves it useful, but the thing to put under version control and defend in an audit is the generation recipe. The component count on its own means little.&lt;/p&gt;

&lt;p&gt;This is commentary on those two papers, linked where used; I have not reproduced them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Inria paper shows
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2609.19920" rel="noopener noreferrer"&gt;Prado, Zendra, Boinot and Barais&lt;/a&gt; (17 September 2026, accepted at SCORED 2026) took 2,050 JavaScript and 1,276 Rust repositories with over 1,000 GitHub stars, regenerated a fresh lockfile for each, and used that lockfile as ground truth. They ran Syft 1.38.2, Trivy 0.68.2 and cdxgen 12.0.0, all producing CycloneDX 1.6, and compared.&lt;/p&gt;

&lt;p&gt;With all lockfiles exposed, Syft reported 43.78% of the lockfile packages on JavaScript and Trivy 32.71%. On Rust, Syft reached 100% and Trivy 83.85%. cdxgen was at 98.72% and 99.34%. With no lockfile at all, Syft and Trivy produced an empty SBOM, and the paper is specific that this happens "with no error or warning shown to the operator."&lt;/p&gt;

&lt;p&gt;The useful part is why. Most of the JavaScript gap is devDependencies. Syft excludes them by design, but only for some package managers: in one npm project it reported 2249 of 2249 production packages and 0 of 4930 dev packages, while in a Yarn and a pnpm project it reported both at 100%. Trivy excludes them by default too, but the exclusion stops at the first level, so a transitive dependency of a dev package can show up attached to the root as if it were a top-level production dependency. The authors call Syft's behaviour a design choice applied inconsistently and Trivy's an implementation error. cdxgen's small residual gap was a hard-coded list that skips any directory named &lt;code&gt;examples&lt;/code&gt;, along with &lt;code&gt;docs&lt;/code&gt;, &lt;code&gt;tests&lt;/code&gt; and &lt;code&gt;.github&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same package also comes out with a different identifier depending on the tool. An npm alias is reported under the alias by cdxgen and Trivy and under the real scoped name by Syft. pnpm peer-dependency suffixes end up in the version string for cdxgen and Syft. One git-pinned dependency came back as a commit hash from one tool, a tarball URL from the second and a semantic version from the third. Vulnerability matching works on these identifiers, so the same code scanned with two tools can produce two different vulnerability lists.&lt;/p&gt;

&lt;p&gt;The Supplier element, one of NTIA's minimum elements since 2021, is populated by none of the three tools on either ecosystem. Licenses: 75.8% for cdxgen, 60.2% for Syft, 0% for Trivy on JavaScript, and 0% for all three on Rust. Hashes come almost only from cdxgen, CPEs almost only from Syft.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not show
&lt;/h2&gt;

&lt;p&gt;The sample is popular GitHub repositories, and the authors say they did not measure how the 1,000-star threshold affects coverage. The results are tied to three specific versions; the authors note that a 2026 study using Trivy 0.66.0 and Syft 1.33.0 got empty SBOMs for &lt;code&gt;package-lock.json&lt;/code&gt;, and a few releases later the same tools produce partial ones. Trivy 0.75.0 and Syft 1.54.0 both shipped on 1 October, so some of these numbers may already be stale. The field-completeness measurement checks whether a field is present, not whether its content is right. And the paper covers JavaScript and Rust only; I would not assume Java or Python look better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the second paper adds
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2609.07754" rel="noopener noreferrer"&gt;Pengyin Shan's pre-registered audit&lt;/a&gt; (7 September 2026) asks the other question: when the thing installing software is an AI assistant, does it read any of this? Six research software projects, nine variants each (no signal, a valid SBOM, a valid signature, a valid attestation, two with wrong-issuer material, one with every signal, one with contradictory metadata), three models, two harnesses, every trial in a network-less container with file access and commands logged, protocol deposited with a DOI before the first trial.&lt;/p&gt;

&lt;p&gt;In 9 of 1,920 trials the assistant opened a provenance file before installing. In 0 of 2,114 trials, including a frontier-model supplement, did any assistant run a verification command. Signal presence had no measurable effect (p = 0.50). The model that cost $1.00 per trial verified nothing in 50 trials; the only one that opened signals more than once cost about $0.10 per trial and did it in 9 of 360. The author's conclusion is that verification "has to be built into the program that runs the assistant."&lt;/p&gt;

&lt;p&gt;The limits are real: six projects, research software, a container with no package index so most installs failed at dependency resolution, and material signed by the author's own key rather than the upstream's. It measures the decision to install, not a successful install. Still, zero verification commands across six models and two harnesses is hard to explain away with sample size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;The CRA's main obligations apply from 11 December 2027, with reporting obligations from 11 September 2026, &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act" rel="noopener noreferrer"&gt;per the Commission's own page&lt;/a&gt;. Most manufacturers will discharge that with one of these three tools, and the Inria authors put it bluntly: tool choice "is no longer just an engineering decision, it is a compliance-exposure question."&lt;/p&gt;

&lt;p&gt;I think SBOMs matter in two years as an inventory you can defend, and I doubt they will matter as a trust signal anyone checks at install time. The second paper shows the install-time consumer is already often an agent, and the agent does not look. The first shows that a human who looks cannot tell from the document whether it is complete. The fix for the first is standards work: the authors ask CycloneDX and SPDX to define which dependency scopes are in, how provenance is recorded, and what the root of the graph is, which will take years. The fix for the second is a harness that runs &lt;code&gt;cosign verify&lt;/code&gt; or &lt;code&gt;slsa-verifier&lt;/code&gt; before &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday
&lt;/h2&gt;

&lt;p&gt;I have run security checks (Gitleaks, Trivy, SCA) in every pipeline of a secure development platform through shared GitLab CI templates, and I have delivered signed packages into an air-gapped bank, where the receiving side cannot regenerate anything and works with what is in the bundle. This is what I would change this week.&lt;/p&gt;

&lt;p&gt;Pin the generator and write its version into the SBOM metadata. The paper's results changed between minor releases of the same tool. If an auditor asks why a component is missing, "Trivy 0.68.2 with default flags on a Yarn project" is an answer. "We run Trivy" is not.&lt;/p&gt;

&lt;p&gt;Fail the job on an empty SBOM. Syft and Trivy return zero components and exit 0 when there is no lockfile. A &lt;code&gt;jq '(.components // []) | length'&lt;/code&gt; with a threshold is five lines in a shared template and catches the case the paper calls more severe than under-reporting.&lt;/p&gt;

&lt;p&gt;Decide the dev-dependency question on purpose. Trivy's &lt;code&gt;--include-dev-deps&lt;/code&gt; exists; the default excludes them, and the exclusion leaks. For a deployment SBOM you may want them out. For a view of your build's supply chain you want them in, since the paper notes development dependencies have themselves been targets of supply-chain attacks. Write the choice down.&lt;/p&gt;

&lt;p&gt;Commit the lockfile, or generate the SBOM from the built artifact instead of the source tree. Without a resolved lockfile the tools are either guessing or silent.&lt;/p&gt;

&lt;p&gt;Run two generators once, on your five most important repositories, and diff the component sets. An afternoon of that tells you which scope and identifier rules bite your stack, before you promise anyone a complete inventory.&lt;/p&gt;

&lt;p&gt;For air-gapped delivery, put the SBOM inside the signed bundle together with the generator name, version and command line, and have the receiving side verify the bundle signature. The SBOM records what you believe is in there; the signature records who is accountable if that belief is wrong.&lt;/p&gt;

&lt;p&gt;If an AI agent sits anywhere in your dependency flow, put the verification step in the pipeline that runs it, because the model will not do it on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would change my mind
&lt;/h2&gt;

&lt;p&gt;A benchmark that pins these edge cases, which the Inria authors say they are building, with Syft and Trivy above 95% on JavaScript. A CycloneDX or SPDX release that defines dependency scope as a standard attribute, so "complete" means one thing across tools. Or a re-run of the agent study in which a production harness verifies attestations by default and leaves a log.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Alan Prado, Olivier Zendra, Philippe Boinot, Olivier Barais. "Mind the Gap: How SBOM Specification Ambiguities Lead to Divergent Software Bills of Materials. An Empirical Tool Study." arXiv:2609.19920, 17 September 2026. &lt;a href="https://arxiv.org/abs/2609.19920" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.19920&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pengyin Shan. "Do AI Coding Assistants Check Before They Install? A Pre-Registered Demand-Side Audit of Trust Signals in the Research Software Supply Chain." arXiv:2609.07754, 7 September 2026. &lt;a href="https://arxiv.org/abs/2609.07754" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2609.07754&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;European Commission, "Cyber Resilience Act" policy page (entry into force and application dates). &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act" rel="noopener noreferrer"&gt;https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Trivy documentation, Node.js coverage (&lt;code&gt;--include-dev-deps&lt;/code&gt;). &lt;a href="https://trivy.dev/latest/docs/coverage/language/nodejs/" rel="noopener noreferrer"&gt;https://trivy.dev/latest/docs/coverage/language/nodejs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release pages checked 2 October 2026: &lt;a href="https://github.com/aquasecurity/trivy/releases" rel="noopener noreferrer"&gt;https://github.com/aquasecurity/trivy/releases&lt;/a&gt; and &lt;a href="https://github.com/anchore/syft/releases" rel="noopener noreferrer"&gt;https://github.com/anchore/syft/releases&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>cicd</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
