<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[TOPOLOGY]]></title><description><![CDATA[Think deeper. Build better. Lead clearly.]]></description><link>https://sidh4u.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!jPAX!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e2c2450-3770-4bd7-839b-93d57409b94f_512x512.png</url><title>TOPOLOGY</title><link>https://sidh4u.substack.com</link></image><generator>Substack</generator><lastBuildDate>Sat, 09 May 2026 21:24:32 GMT</lastBuildDate><atom:link href="https://sidh4u.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Sidhartha Mandal]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[sidh4u@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[sidh4u@substack.com]]></itunes:email><itunes:name><![CDATA[Sidhartha Mandal (sidh4u)]]></itunes:name></itunes:owner><itunes:author><![CDATA[Sidhartha Mandal (sidh4u)]]></itunes:author><googleplay:owner><![CDATA[sidh4u@substack.com]]></googleplay:owner><googleplay:email><![CDATA[sidh4u@substack.com]]></googleplay:email><googleplay:author><![CDATA[Sidhartha Mandal (sidh4u)]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[The Four People Who Built What We Call AI]]></title><description><![CDATA[It started with a Name. It ended in 1948.]]></description><link>https://sidh4u.substack.com/p/the-four-people-who-built-ai</link><guid isPermaLink="false">https://sidh4u.substack.com/p/the-four-people-who-built-ai</guid><dc:creator><![CDATA[Sidhartha Mandal (sidh4u)]]></dc:creator><pubDate>Thu, 07 May 2026 08:04:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IG0k!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IG0k!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IG0k!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 424w, https://substackcdn.com/image/fetch/$s_!IG0k!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 848w, https://substackcdn.com/image/fetch/$s_!IG0k!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 1272w, https://substackcdn.com/image/fetch/$s_!IG0k!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IG0k!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png" width="1200" height="679" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:679,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126337,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://sidh4u.substack.com/i/196748467?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IG0k!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 424w, https://substackcdn.com/image/fetch/$s_!IG0k!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 848w, https://substackcdn.com/image/fetch/$s_!IG0k!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 1272w, https://substackcdn.com/image/fetch/$s_!IG0k!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd90bb8e-0b59-4b90-b3b3-3baaa7f41c7c_1200x679.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I use Claude every day. It has quietly become part of how I think - a place to stress-test ideas, draft things, ask questions I&#8217;d be embarrassed to Google. After a while you stop noticing the tool and just notice the thinking.</p><p>Then one afternoon I asked it something I hadn&#8217;t thought to ask before: Why are you called Claude?</p><p>The answer - and the rabbit hole that followed - turned into this article. Because the name points to a person. And that person points to a moment in the 1940s when three or four human beings, working within a few years of each other, laid down the mathematics and the questions and the vocabulary that everything we call AI is built on.</p><p>One of those people is still alive. He just won a Nobel Prize. And he&#8217;s worried.</p><p>Let&#8217;s start with the name.</p><h3>Why Claude? And Why Anthropic?</h3><p>Anthropic - the company that built Claude - takes its name from the Greek word anthropos, meaning human. It&#8217;s a values statement embedded in the company name: we are building AI aligned with human interests. That&#8217;s the mission, right there in the etymology.</p><p>The product name is a different kind of tribute. <strong>Claude</strong> is almost certainly named after <strong>Claude Shannon</strong> - not officially confirmed, but widely acknowledged inside Anthropic and obvious once you understand what Shannon actually did. The AI built on the mathematics of information is named after the man who invented <a href="https://en.wikipedia.org/wiki/Information_theory">information theory</a> itself.</p><p>Which raises the question: What did Claude Shannon actually do?</p><h3>Claude Shannon - The Man Who Invented Information</h3><p><strong>Bell Labs, 1948</strong></p><p>In 1948, a mathematician at Bell Labs named Claude Shannon published a 77-page paper called <a href="https://people.math.harvard.edu/~ctm/home/text/others/shannon/entropy/entropy.pdf">A Mathematical Theory of Communication</a>. It contains, tucked inside the mathematics, one of the most consequential inventions of the twentieth century.</p><p style="text-align: center;"><strong><a href="https://en.wikipedia.org/wiki/Bit">The bit</a>.</strong></p><p>Not the bit as a vague concept. Shannon defined it formally: <em>the fundamental unit of information</em>, the answer to a single yes/no question, true/false question, represented as 0 or 1. Before Shannon, information was fuzzy - you could say a message was long or complex but you couldn&#8217;t measure it. Shannon gave information a unit, the way temperature has degrees and distance has metres.</p><p>His deeper insight was that any information - text, sound, images, anything - could be broken down into a sequence of these binary choices. It didn&#8217;t matter what the content meant. What mattered was how many yes/no decisions were needed to encode it.</p><p>Every hard drive measured in terabytes, every internet connection in megabits per second, every compressed image, every encrypted message - all of it is Shannon&#8217;s mathematics, running silently underneath.</p><p>But there&#8217;s something else in that 1948 paper that most people miss. Shannon described how systems that generate sequences probabilistically - choosing the next symbol based on the current state - could produce outputs that resemble language. He was describing, in 1948, the conceptual skeleton of what we now call a large language model (LLM). He didn&#8217;t have computers powerful enough to build one. He had the mathematics.</p><p>Shannon was also, by all accounts, a wonderfully strange person - and the strangeness is worth mentioning because it shaped how he thought. He rode a unicycle through the Bell Labs corridors. He juggled. He built a machine called <a href="https://mitmuseum.mit.edu/collections/object/2007.030.001">Theseus</a> - a mechanical mouse that could navigate a maze, remember the solution, and find its way back on subsequent attempts. In 1951, he calculated the information content of English and concluded that roughly half of every sentence is redundant - which is why you can <em>rd ths sntnc wth hlf th ltrs mssng</em> and still understand it perfectly. That wasn&#8217;t a party trick. It was the information theory, applied.</p><p>That <em>Theseus machine</em> is worth pausing on. A system that attempts a problem, remembers what worked, and performs better on the next attempt - that is not a historical curiosity. That is the core loop of modern machine learning. The maze is different. The scale is unimaginable by 1950 standards. But the logic - <em>try</em>, <em>remember</em>, <em>improve</em> - is the same logic that runs inside every AI model being trained today. Shannon built it out of copper and relays in a Bell Labs workshop. We now run it on hundreds of thousands of GPUs simultaneously. The idea didn&#8217;t change. Only the hardware did.</p><p style="text-align: center;"><em>He published the rulebook for the digital age in 1948 - before most computers existed. We are still playing by his rules.</em></p><h3>Alan Turing - The Man Who Asked the Question</h3><p><strong>Manchester, 1950</strong></p><p>Two years after Shannon&#8217;s paper, a British mathematician named Alan Turing published one of his own. It opens with a sentence that changed the direction of science:</p><p style="text-align: center;"><em>&#8220;<a href="https://courses.cs.umbc.edu/471/papers/turing.pdf">I propose to consider the question: Can machines think?</a>&#8221;</em></p><p>Turing had already spent the previous decade doing work that most people still don&#8217;t know about. In 1936, at 24 years old, he had published his theory of the <a href="https://en.wikipedia.org/wiki/Turing_machine">Turing Machine</a> - an abstract model of computation that described, for the first time, exactly what any computer could and could not do. Not a specific machine. Any machine. The theoretical limits of the entire category, defined fourteen years before most computers existed.</p><p>Then came the war. Turing worked at Bletchley Park and was instrumental in breaking the Nazi Enigma cipher. Historians estimate this shortened the war by two to four years - a number that translates into millions of lives.</p><p>Back to 1950. Turing knew the question &#8220;Can machines think?&#8221; was unanswerable as stated, so he replaced it with something testable: the Imitation Game, which we now call the <a href="https://en.wikipedia.org/wiki/Turing_test">Turing Test</a>. Put a human judge in text conversation with both a human and a machine. If the judge cannot reliably tell which is which, the machine has passed. It is still debated endlessly. But Turing&#8217;s real contribution wasn&#8217;t the test - it was the insistence that the question deserved to be asked seriously. That machine intelligence was a scientific problem, not a philosophical fantasy.</p><p>In 1952, the British government prosecuted him for homosexuality. He was subjected to chemical castration. He died in 1954, aged 41. <em>He received a royal pardon in 2013.</em></p><p>The <a href="https://en.wikipedia.org/wiki/Turing_Award">Turing Award</a> - computing&#8217;s Nobel Prize - has been given in his name every year since 1966. One of its recipients, decades later, would finally build what Turing had only imagined.</p><h3>John McCarthy - The Man Who Named It</h3><p><strong>Dartmouth College, 1956</strong></p><p>If Shannon built the mathematics and Turing asked the question, John McCarthy gave the whole enterprise its name - and with it, an identity.</p><p>In the summer of 1956, McCarthy organised a two-month workshop at Dartmouth College in New Hampshire. In his proposal for that workshop, he used a phrase that had never appeared before in print: <em><strong>Artificial Intelligence (AI)</strong></em>.</p><p>Before Dartmouth 1956, there was no field called AI. There were researchers thinking about thinking machines, about logic and computation and automata. McCarthy put them under one roof and gave them a common name. Naming a thing is more powerful than it sounds - by calling it artificial intelligence, he was claiming that the goal was not just automation or calculation, but something that deserved the word intelligence. That framing shaped research agendas, funding decisions, and public perception for the next seventy years.</p><p>McCarthy was famously optimistic about the timeline. He reportedly believed the core problems of machine intelligence could be solved in that two-month summer. It took seventy years. But the name he chose in that proposal is the one we still use - in every headline, every job title, every government policy document, every conversation about what is happening to the world right now.</p><p style="text-align: center;"><em>He thought it would take a summer. It took seventy years. The name, at least, was right.</em></p><h3>Geoffrey Hinton - The Man Who Kept the Faith</h3><p><strong>Toronto &#8594; Google &#8594; everywhere, 1986&#8211;2024</strong></p><p>Before we get into what Hinton did - a small note on what he is called. The press, the research community, and now the Nobel Committee all use the same phrase when they talk about him: <em><strong>The Godfather of AI</strong></em>. It is one of those titles that sounds like hyperbole until you understand the actual history. By the time you finish this section, you will understand why it fits.</p><p><em>The first three people built the theory. Geoffrey Hinton built the thing.</em></p><p>By the 1970s and 80s, AI research had hit a wall. The ideas were there - <em><strong>neural networks</strong></em>, <em><strong>machine learning</strong></em>, <em><strong>pattern recognition</strong></em> - but the computers weren&#8217;t powerful enough to make them work at scale. Funding dried up. Researchers moved on. The field went through what historians call the AI winters - long periods of scepticism and silence when almost everyone concluded that machine intelligence was further away than anyone had thought.</p><p>Almost everyone. Geoffrey Hinton kept going.</p><p>Hinton, a British-Canadian cognitive psychologist and computer scientist, spent decades working on <em><strong>neural networks</strong></em> - systems modelled loosely on the structure of the brain, where layers of simple units learn to recognise patterns through repeated exposure to data. The idea was not new. Frank Rosenblatt had proposed the Perceptron in 1958. But a famous 1969 book by Marvin Minsky had identified what seemed like fundamental limitations of neural nets, and most of the field abandoned them.</p><p>Hinton didn&#8217;t. Through the 80s and 90s, largely unfashionable and working with limited resources, he kept developing the algorithms - particularly <em><strong>backpropagation</strong></em>, the method by which neural networks learn from their mistakes - that would eventually make <em><strong>deep learning</strong></em> possible.</p><p>Then came the compute. GPUs, originally built for video games, turned out to be extraordinarily well-suited to the parallel calculations that neural networks require. When cheap, powerful GPU computing arrived, Hinton&#8217;s decades of theoretical work suddenly had an engine.</p><p>In 2012, his lab at the University of Toronto entered a neural network called AlexNet into the ImageNet competition - a benchmark for image recognition involving one million images across one thousand categories. AlexNet didn&#8217;t just win. It won by roughly ten percentage points. The margin was so large that it ended the debate. Deep learning was no longer a fringe idea kept alive by one stubborn researcher. It was the only idea that mattered.</p><p>Everything since - every large language model, every image generator, every voice assistant, every recommendation algorithm - descends directly from the architecture that Hinton spent thirty years keeping alive.</p><p>In 2018, Hinton shared the Turing Award with Yann LeCun and Yoshua Bengio - the three researchers who together built the deep learning foundations of modern AI. In 2024, he was awarded the Nobel Prize in Physics.</p><p>And then he left Google and started talking publicly about what he had helped create.</p><p style="text-align: center;">"I console myself with the normal excuse: if I hadn't done it, someone else would have. But I'm not sure that's true." - Geoffrey Hinton, 2023</p><p>The man who spent thirty years in the wilderness to prove that neural networks could work is now one of the most prominent voices warning about what happens when they work too well. That is not an irony. That is a conscience.</p><h3>The Thread</h3><p>What I find remarkable is how tight the timeline is. Shannon&#8217;s paper: 1948. Turing&#8217;s paper: 1950. McCarthy&#8217;s Dartmouth workshop: 1956. Hinton&#8217;s backpropagation work: 1986. AlexNet: 2012. ChatGPT: 2022.</p><p>The foundational ideas were all in place by 1956 - within eight years, by four people who knew each other, cited each other, built on each other. Shannon was at Dartmouth. Turing and Shannon had met. McCarthy built on both.</p><p>What took the next sixty-six years was not new ideas. It was compute catching up to the ambition of the mathematics. The ideas were always there. The machines were not yet powerful enough to run them.</p><p>Hinton provided the bridge - the decades of work that kept the neural network approach alive until the hardware finally arrived. Without him, the gap between McCarthy&#8217;s 1956 workshop and GPT-4 might have been even longer.</p><p style="text-align: center;"><em>The mathematics: Shannon. The question: Turing. The name: McCarthy. The proof: Hinton. Everything else is built on top of those four contributions.</em></p><p>I use Claude every day. I now know, in a way I didn&#8217;t before I went down this rabbit hole, exactly whose work I am sitting on top of. That feels like something worth knowing.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://sidh4u.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading TOPOLOGY! </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><br>Subscribe for free to receive new posts and support my work - or go paid for early access, deeper dives, and the occasional piece that never goes public.</p><p></p>]]></content:encoded></item><item><title><![CDATA[The Upgrade That Almost Wasn't]]></title><description><![CDATA[A production EKS upgrade gone wrong taught me everything this post covers. Control plane sequencing, pre-flight checks, environment progression, Terraform targeting, and why the rollback question is the wrong question. The playbook for zero-downtime Kubernetes upgrades on AWS.]]></description><link>https://sidh4u.substack.com/p/zero-downtime-eks-upgrades</link><guid isPermaLink="false">https://sidh4u.substack.com/p/zero-downtime-eks-upgrades</guid><dc:creator><![CDATA[Sidhartha Mandal (sidh4u)]]></dc:creator><pubDate>Sat, 02 May 2026 11:03:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-5rB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-5rB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-5rB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 424w, https://substackcdn.com/image/fetch/$s_!-5rB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 848w, https://substackcdn.com/image/fetch/$s_!-5rB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 1272w, https://substackcdn.com/image/fetch/$s_!-5rB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-5rB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png" width="1200" height="590" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:590,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:128063,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://sidh4u.substack.com/i/196202402?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-5rB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 424w, https://substackcdn.com/image/fetch/$s_!-5rB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 848w, https://substackcdn.com/image/fetch/$s_!-5rB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 1272w, https://substackcdn.com/image/fetch/$s_!-5rB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18e5d858-4bf5-4292-af6a-90c3aa0d92e7_1200x590.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>It&#8217;s 04:47pm. The control plane upgrade finished twenty minutes ago and everything looks fine. The API server is responding. System pods are healthy. So you move to the node groups - because the runbook says so, and the runbook has always been right.</p><p>Then a Slack message from on-call: &#8220;ALB stopped routing to the new nodes. Users are hitting 502s.&#8221;</p><p>You stare at the screen. The nodes are Ready. The pods are Running. The target group shows healthy. And yet.<br><br>Forty minutes later, you find it: a node affinity rule, written eight months ago by someone who has since left the team, that was silently excluding the new node group from a critical deployment. The pods were running - just not the right pods, on the right nodes, behind the right load balancer.<br><br>No one wrote it down. No runbook covered it. And now you&#8217;re explaining a 40-minute degradation to your VP of Engineering at midnight.</p><p style="text-align: center;"><em>I&#8217;ve been in that room. This article is everything I wish I&#8217;d known before I was.</em></p><h3>The Rule Nobody Actually Follows</h3><p style="text-align: center;"><em><strong>Control plane first. Node groups second. Never both simultaneously.</strong></em></p><p>Say it out loud. It sounds obvious. And yet I have watched teams - smart teams, teams with good intentions - violate this rule constantly. Not out of carelessness, but because their automation was doing something they didn&#8217;t fully understand, or because the pressure to &#8220;just get it done&#8221; overcame the discipline to do it right.</p><p>Here&#8217;s why the rule exists. AWS guarantees that your kubelets can run up to three minor versions behind the control plane - if your nodes are on 1.25 or newer. A control plane on 1.32 can manage nodes on 1.29. That gap is not a bug - it is a deliberately engineered operating window. It exists to give you time to validate that the control plane upgrade is stable before you touch a single node. One important caveat: if your nodes are still on kubelet 1.24 or older, the older two-version skew applies. If you're running anything current, you're in the three-version world.</p><p><strong>Use that window. </strong>Don&#8217;t race through it because you want to finish before midnight.</p><p>The teams I&#8217;ve seen upgrade with the least drama are the ones who treat the gap between control plane and node upgrade as intentional breathing room - time for their monitoring to settle, for their operators to reconnect, for them to look at the cluster and ask: does this feel right?</p><h3>Before You Touch Anything: The Pre-Flight</h3><p>The most expensive mistakes I&#8217;ve witnessed in EKS upgrades didn&#8217;t happen during the upgrade. They happened in the days before it, when teams skipped checks they assumed wouldn&#8217;t matter.</p><h4>Check your add-on compatibility matrix</h4><p>AWS managed add-ons - CoreDNS, kube-proxy, VPC CNI - have specific version compatibility matrices for each Kubernetes minor version. These are not suggestions. If your VPC CNI version is incompatible with the target Kubernetes version, new pods will fail to get network interfaces. New pods means no new capacity. No new capacity during a rolling node upgrade means workloads pile up on draining nodes and your cluster grinds to a halt.</p><p>Check this first. Every time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;98692ef5-7709-415d-9f59-9eaaf3015492&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">aws eks describe-addon --cluster-name &lt;cluster&gt; --addon-name coredns
aws eks describe-addon --cluster-name &lt;cluster&gt; --addon-name kube-proxy
aws eks describe-addon --cluster-name &lt;cluster&gt; --addon-name vpc-cni</code></pre></div><h4>Hunt your pod disruption budgets</h4><p>I once spent 90 minutes debugging a node drain that wasn&#8217;t moving. The node was cordoned. The drain command was running. Nothing was happening.</p><p>A PDB with maxUnavailable: 0 was doing exactly what it was designed to do: refusing to allow any disruption. The PDB was correct for its original purpose. But its original purpose was three months and two team members ago.</p><p>Find every PDB in your cluster before upgrade day. Review each one. Ask whether the constraint is still appropriate. Don&#8217;t find out mid-drain.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;25dfbc22-6206-4078-aa39-34a8f45adc50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">kubectl get pdb -A</code></pre></div><h4>Scan for deprecated API versions</h4><p>Kubernetes removes APIs that were deprecated two or three versions earlier. You will not get a warning at runtime. Your workloads will simply stop deploying after the upgrade - because the API version they reference no longer exists.</p><p>Tools like <strong>Pluto</strong> or <strong>kubent</strong> will scan your cluster and flag deprecated API usage before it becomes a 2am problem. Run them. Fix what they find. Then run them again after your fixes to confirm.</p><h4>Check your headroom</h4><p>During a rolling node replacement, pods from draining nodes need somewhere to land. If your cluster is running at 85% utilisation with no autoscaling headroom, they have nowhere to go. The upgrade stalls. Nodes queue behind each other waiting for capacity that isn&#8217;t coming.</p><p>Temporarily bump your node group min size before the upgrade. Or confirm your cluster autoscaler has room to expand. Either way - check before you start, not after you&#8217;re stuck.</p><h3>The Environment Progression: Why You Should Never Upgrade Production First</h3><p>This is the one practice that has saved me more times than any technical trick.</p><p>Never upgrade production first. Always follow the progression:</p><p style="text-align: center;"><em>POC &#8594; DEV &#8594; STAGE &#8594; PROD</em></p><p>Each environment in this chain serves a distinct purpose - and the discipline breaks down the moment you treat any of them as optional.</p><p><strong>POC </strong>is a temporary environment, spun up specifically to validate the upgrade path. It doesn&#8217;t need to mirror production perfectly. What it needs to produce is a <strong>runbook</strong>. Every decision, every issue, every resolution, written down as you go. The runbook is the primary output of your POC upgrade - not the working cluster.</p><p>But there&#8217;s a second thing POC must do that most teams skip: validate every unmanaged component against the target version. This is where the real surprises live.</p><p>AWS managed add-ons - CoreDNS, kube-proxy, VPC CNI - have compatibility matrices and AWS handles their upgrade path. Your external operators and controllers have no such safety net. The AWS Load Balancer Controller, Karpenter, Cluster Autoscaler, cert-manager, external-dns - each has its own Kubernetes version compatibility matrix, maintained separately, with its own release cadence. None of them will warn you at runtime if they fall out of compatibility. They&#8217;ll just start behaving incorrectly, or stop working entirely, in ways that may not be immediately obvious.</p><p>POC is where you find this out cheaply. Install every external operator and controller your production cluster runs. Upgrade the cluster. Watch what breaks. Specifically:</p><ul><li><p>Does the AWS Load Balancer Controller still reconcile ingress resources correctly after the upgrade?</p></li><li><p>Does Cluster Autoscaler still provision and register new nodes?</p></li><li><p>Does cert-manager still issue and renew certificates?</p></li><li><p>Does external-dns still sync records?</p></li><li><p>Do any of your custom operators - the ones your own team wrote - handle the new API versions correctly?</p></li></ul><p>If any of these fail in POC, you have found the issue at the cheapest possible moment - in a temporary cluster, with no users, and no pressure. Document the fix. Pin the compatible version. Add it to the runbook. By the time you reach PROD, you will have validated this component three times across three environments, and its behaviour will be a known quantity.</p><p><strong>DEV </strong>is your first real-world test. Real engineers use it. Real workload patterns emerge. I&#8217;ve seen compatibility issues surface in DEV that never appeared in POC - because real scheduling behaviour, real resource constraints, real network policy interactions only show up with real workloads. Fix the issues. Update the runbook.</p><p><strong>STAGE </strong>is where you should have no surprises. By this point you&#8217;ve done the upgrade twice. The runbook is battle-tested. If something new surfaces in STAGE, that&#8217;s a signal - your DEV environment doesn&#8217;t match STAGE closely enough. Fix the parity, not just the symptom.</p><p><strong>PROD </strong>is now a known quantity. The runbook is proven. The team has done this three times already. Muscle memory has replaced anxiety. The outcome is predictable - because you made it predictable.</p><p>This progression also solves a problem most teams don&#8217;t notice until it bites them: version drift between environments. When DEV is one minor version behind PROD, and STAGE is somewhere in between, your non-production upgrades tell you very little about your production upgrade. The progression - treated as a pipeline, not a one-off event - keeps everything current.</p><h3>IaC Makes Upgrades Boring (That&#8217;s the Point)</h3><p>The single biggest change in how I think about EKS upgrades came when I started managing clusters entirely through Terraform.</p><p>Not because Terraform is magic. Because IaC (Infrastructure as Code) forces you to be explicit about sequencing - and sequencing is where most upgrade failures live.</p><p>The terraform-aws-modules/eks module structures your cluster configuration cleanly, but it does not automatically sequence the control plane and node group upgrades for you. A plain terraform apply after bumping cluster_version will kick off both the control plane and node group upgrades in the same operation - which violates the fundamental rule. The sequencing discipline still sits with you.</p><p>The pattern that gives you back control is targeted applies. You upgrade the control plane first, validate it completely, then explicitly upgrade node groups as a separate step:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;3045062e-4137-4071-8844-c04a7da1e7c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash"># Step 1: Bump cluster_version in your config, then:
terraform apply -target=module.eks.aws_eks_cluster.this[0]

# Validate - kubectl get nodes, check system pods, confirm stable

# Step 2: Only then, upgrade node groups
-target='module.eks.module.eks_managed_node_group["your-node-group-name"]'</code></pre></div><p>This gives you the validation window the fundamental rule demands. The config change is a single version bump - clean, auditable, reviewable in a PR:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;73eb29a2-2c1b-4df4-b915-d025b7e2828e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">module &#8220;eks&#8221; {
  source = &#8220;terraform-aws-modules/eks/aws&#8221;
  version = &#8220;~&gt; 21.0&#8221;

  cluster_version = &#8220;1.32&#8221; # was 1.31

  cluster_addons = {
    coredns = { most_recent = true }
    kube-proxy = { most_recent = true }
    vpc-cni = { most_recent = true }
  }
}</code></pre></div><p>The apply is where you exercise the discipline - not the config. One change, two targeted applies, one validation gate in between.</p><p>One important nuance: most_recent = true is convenient for initial setup, but production clusters benefit from pinned add-on versions that you&#8217;ve explicitly validated. Use most_recent to discover the compatible version, then pin it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;45b872b7-9863-41fe-8f31-1741309e9c0b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">coredns = {
  addon_version = &#8220;v1.11.1-eksbuild.4&#8221;
}</code></pre></div><p>This prevents an add-on version from changing unexpectedly on a subsequent apply. It gives you explicit control over what changes during an upgrade - which means you can explain every change in your post-upgrade review, because you chose every change.</p><p>One thing Terraform does not handle for you: external operators. The AWS Load Balancer Controller, Cluster Autoscaler, cert-manager, external-dns, observability - these live outside the EKS managed add-on umbrella. They need to be upgraded in the same cycle, managed through their Helm chart versions in Terraform or your GitOps tooling. An EKS upgrade is not complete until every component in the cluster - managed and unmanaged - has been validated at the new version.</p><h3>In-Place vs A/B: Choose Deliberately</h3><p>I&#8217;ve done both. Neither is universally correct. What matters is choosing deliberately and understanding the trade-offs before you&#8217;re mid-upgrade.</p><h3>In-place rolling upgrades</h3><p>AWS handles the cordon, drain, replacement, and rejoin sequence within the existing node group. Simpler to orchestrate. Lower temporary cost. No DNS complexity. The trade-off: less control over the exact replacement sequence, and no parallel environment to fall back to if something goes wrong mid-upgrade.</p><p>In-place works well when your workloads are well-understood, your PDBs are accurate, and you&#8217;ve done the pre-flight thoroughly. It&#8217;s the approach most teams should start with.</p><h3>A/B switching</h3><p>You provision a fully operational target environment alongside the current one, validate completely, then cut over. The rollback is clean: if something is wrong, traffic stays on the original.</p><p>At the cluster level, the critical complexity is networking. Your ALB DNS names, ingress endpoints, and load balancer IPs are tied to the original cluster. A new cluster means new load balancers with new DNS names. The cutover sequence is non-negotiable:</p><ul><li><p>Create all resources in the new cluster</p></li><li><p>Validate end-to-end - every service, every endpoint, every health check</p></li><li><p>Update DNS records to point to the new endpoints</p></li><li><p>Only then decommission the old cluster</p></li></ul><p><strong>Cutting DNS before validation </strong>is the most common A/B failure mode. I&#8217;ve seen it happen. Don&#8217;t let it happen to you.</p><h4>A/B at the node group level</h4><p>This is often the best of both worlds. Provision a new node group at the target version alongside the existing one. Cordon the old nodes, migrate workloads, validate, remove the old group. You get the rollback safety of A/B without the external DNS orchestration of a full cluster switch.</p><p>The networking consideration here moves inward. During the transition, both old and new nodes are active cluster members, and services will route to pods on either. If your workloads use node selectors, affinity rules, or topology spread constraints tied to node group labels - the midnight 502 scenario from the opening - review and update them before migration. Confirm pods are fully healthy on new nodes before removing the old group.</p><h3>The Upgrade Itself</h3><h4>Upgrading the control plane</h4><p>AWS manages the underlying upgrade - etcd, API server, controller manager, scheduler. You initiate it; they handle the mechanics. Expect 15&#8211;25 minutes.</p><p>During this window, the API server will be briefly unavailable as it cycles. Any process making continuous Kubernetes API calls - CI/CD pipelines, operators, monitoring agents - will see transient errors. Well-written operators handle this with exponential backoff and recover automatically. Know which of your operators are well-written before the upgrade, not after.</p><p>After the control plane finishes: stop. Validate. Check that all system pods are healthy, all nodes show Ready, nothing is stuck in a restart loop. No node work begins until this check passes.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;cedb23b7-d79f-4949-9783-f04beb0b3f32&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">kubectl get nodes
kubectl get pods -A | grep -v Running | grep -v Completed</code></pre></div><h4>Draining nodes</h4><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;cbc1a61d-d729-445d-93b1-8ccf47c90bf4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">kubectl cordon &lt;node-name&gt;
kubectl drain &lt;node-name&gt; --ignore-daemonsets --delete-emptydir-data --timeout=300s</code></pre></div><p>The --timeout flag is not optional. Without it, a single stuck pod can block the drain indefinitely - and you won&#8217;t know until you&#8217;ve been waiting long enough to start questioning reality. Set a timeout that&#8217;s long enough for graceful shutdown but short enough to surface genuinely stuck pods.</p><h4>Upgrading add-ons - order matters</h4><p>Add-on ordering has a wrinkle that catches teams out. VPC CNI is the exception to &#8220;add-ons after node groups&#8221; - if your current VPC CNI version doesn&#8217;t support the target Kubernetes API, new pods will fail to get network interfaces before the control plane upgrade even completes. Check your VPC CNI compatibility first. If it needs updating, upgrade VPC CNI before the control plane, not after.</p><p>For everything else, the ordering after node groups is:</p><ul><li><p>kube-proxy first - it manages service routing rules on each node</p></li><li><p>CoreDNS last - DNS failures are visible but degrade more gracefully than networking failures</p></li></ul><p>The complete sequence, when VPC CNI needs a pre-upgrade bump, looks like this: <strong>VPC CNI &#8594; control plane &#8594; node groups &#8594; kube-proxy &#8594; CoreDNS &#8594; external operators</strong></p><p>VPC CNI handles pod networking. An incompatible version means new pods fail to get network interfaces. That&#8217;s upstream of everything else. kube-proxy manages service routing rules on each node. CoreDNS failures are visible and disruptive, but DNS can degrade gracefully in ways that networking cannot - hence the ordering.</p><p>Then: external operators. Every one of them, in the same planned window. Not as an afterthought. Not next week. An EKS upgrade is not complete until every component has been validated at the new version.</p><h3>The Rollback Question (And Why It&#8217;s the Wrong Question)</h3><p>Teams ask me: what&#8217;s the rollback plan for the control plane?</p><p>There isn&#8217;t one. In-place EKS upgrades cannot be rolled back. Once the control plane is upgraded, it stays upgraded. This is not a limitation to work around - it is the most important thing to communicate to your team before you begin.</p><p>The question to ask instead: what is our strategy for ensuring the upgrade succeeds?</p><p>That strategy is everything in this article. The pre-flight validation. The environment progression. The deliberate ordering. The willingness to stop and investigate rather than push through when something looks wrong.</p><p><em>Add-ons can be rolled back to previous versions even when the cluster version cannot. If a managed add-on update causes issues, roll it back while you investigate. The cluster version is independent.</em></p><p>I&#8217;ve seen teams push through warning signs because they were &#8220;almost done&#8221; and didn&#8217;t want to restart the window. That instinct - understandable, human, wrong - is what turns planned maintenance into incidents.</p><p>Stop when something looks wrong. Investigate. Decide deliberately. The cluster will wait.</p><h3>What Separates Teams That Upgrade Confidently</h3><p>After years of this, I&#8217;ve noticed the same patterns in teams that upgrade without drama - and in teams that dread every upgrade cycle.</p><p><strong>Automation with gates. </strong>Not &#8220;run everything and hope&#8221; - but scripted upgrade sequences with explicit validation checks between each step. The gate is the discipline. Without it, automation just makes mistakes faster.</p><p><strong>Upgrade frequency. </strong>Clusters upgraded every minor version are dramatically easier to manage than clusters that skip versions. The diff is smaller. The compatibility surface is narrower. The team stays familiar with the process. EKS supports each minor version for 14 months of standard support, followed by 12 months of extended support - 26 months total if you opt in. A new minor version arrives roughly every four months. That math still produces three or four upgrade cycles per year, but teams on extended support may feel less urgency than they should. Extended support is a cost - both financially and operationally. The longer you defer, the larger the diff, the wider the compatibility surface, and the harder the eventual upgrade. Treat 14 months as your real target window, not 26.</p><p><strong>Pre-production parity. </strong>If your staging cluster doesn&#8217;t resemble production in workload type, node configuration, and add-on versions - your staging upgrade tells you almost nothing about your production upgrade. Parity is the whole point of the progression.</p><p><strong>Living runbooks. </strong>A written runbook for your specific cluster configuration - updated after every upgrade cycle - is worth more than any generic guide, including this one. It&#8217;s what you reach for when something unexpected happens at 11pm. It&#8217;s what turns &#8220;we&#8217;ve done this before&#8221; from a feeling into a fact.</p><h3>Boring Upgrades Are Good Upgrades</h3><p>The opening story - the 40-minute degradation, the midnight Slack message, the forgotten node affinity rule - didn&#8217;t have to happen. It happened because the upgrade process had never been fully documented, because the environment progression was skipped to save time, because the pre-flight checks didn&#8217;t include workload-level configuration review.</p><p>None of those failures were dramatic. None of them were unavoidable. They were the accumulated cost of treating upgrades as events rather than operations.</p><p>The teams I respect most in this space don&#8217;t talk about their EKS upgrades. Not because the upgrades are secret - because they&#8217;re unremarkable. Planned maintenance window. Environment progression. Proven runbook. Boring outcome.</p><p><em>That&#8217;s the goal. Not heroic upgrades. Boring ones.</em></p><p>If this helped you think through your next upgrade, share it with whoever runs your cluster. And if you have a war story of your own - a PDB that blocked you, a node affinity rule that bit you, an add-on that fell out of compatibility mid-window - I&#8217;d genuinely like to hear it.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://sidh4u.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading TOPOLOGY! </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><br>Subscribe for free to receive new posts and support my work - or go paid for early access, deeper dives, and the occasional piece that never goes public.</p><p></p>]]></content:encoded></item><item><title><![CDATA[The .pem File in the Slack Channel]]></title><description><![CDATA[Every team starts the same way &#8212; a .pem file dropped in Slack, quietly forgotten, never revoked. This is the SSH Security Maturity Model: three levels from hardened VPS to signed certificates with short-lived access. Know which level you're at. Know why.]]></description><link>https://sidh4u.substack.com/p/ssh-security-maturity-model</link><guid isPermaLink="false">https://sidh4u.substack.com/p/ssh-security-maturity-model</guid><dc:creator><![CDATA[Sidhartha Mandal (sidh4u)]]></dc:creator><pubDate>Thu, 16 Apr 2026 16:39:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ACMB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ACMB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ACMB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 424w, https://substackcdn.com/image/fetch/$s_!ACMB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 848w, https://substackcdn.com/image/fetch/$s_!ACMB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 1272w, https://substackcdn.com/image/fetch/$s_!ACMB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ACMB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png" width="1200" height="619" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:619,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:86735,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://sidh4u.substack.com/i/194417667?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ACMB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 424w, https://substackcdn.com/image/fetch/$s_!ACMB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 848w, https://substackcdn.com/image/fetch/$s_!ACMB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 1272w, https://substackcdn.com/image/fetch/$s_!ACMB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F097a3929-dc0c-4d91-9118-73276c322dc1_1200x619.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I have seen this exact sequence play out at three different companies.</p><p>A new engineer joins. On their first day, someone drops a .pem file into Slack. &#8220;<em>Here&#8217;s the key to the dev server. Don&#8217;t share it.</em>&#8221; Everyone laughs a little because everyone already has the same key. The engineer downloads it, tucks it into their ~/Downloads folder, and promptly forgets about it.</p><p>Six months later, that engineer moves on. The offboarding checklist says &#8220;revoke access&#8221; - but no one quite knows what that means for a shared key. The ticket stays open. The .pem file stays valid. Somewhere on a MacBook that&#8217;s been passed to someone else, it still exists.</p><p>Two years later, a security audit asks: who currently has access to your production instances? The honest answer is: everyone who has ever worked here, and possibly their personal laptops.</p><p><em>SSH security is not a configuration problem. It is an architecture problem. And most teams are solving it at the wrong level.</em></p><p>This post is a maturity model - three layers of SSH security, each appropriate for a different scale and risk profile. You do not have to reach Level 3 to be secure. But you should consciously know which level you are at, and why.</p><h3>First: Know What You Are Protecting</h3><p>Before choosing your approach, be honest about your context. The threat model changes completely depending on where your servers live.</p><p><strong>A VPS with a public IP </strong>- DigitalOcean, Linode, a single EC2 instance with port 22 open to the internet. The attack surface is the server itself. Harden it.</p><p><strong>A VPC with private subnets </strong>- AWS, GCP, Azure. Your instances are not directly reachable from the internet. You need a controlled entry point. This is the bastion host model.</p><p><strong>A VPC at scale with many engineers and many instances </strong>- shared .pem files become operationally unmanageable and auditably indefensible. You need SSH certificates with short-lived, signed access. This is zero-trust SSH.</p><p>Most teams operate at Level 1 regardless of which context they are actually in. Let us fix that.</p><h3>Level 1 - Hardening a Public-Facing Server</h3><p>If you have a VPS or a single EC2 instance with SSH exposed to the internet, start here. The goal is to reduce the attack surface of the server itself, layer by layer.</p><h4>a. Kill Root Login. Create a Named User.</h4><p>There is no legitimate reason for direct root SSH access in 2026. Every action taken as root is unattributable and unauditable. Create a named user, give it sudo privileges, then lock root out entirely.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;2447a4b4-4661-4374-9f9e-9a6bc86b45ba&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Create the user
adduser &lt;username&gt;

# Grant sudo privileges
usermod -aG sudo &lt;username&gt;

# Then in /etc/ssh/sshd_config:
PermitRootLogin no

# Restart SSH after every sshd_config change
systemctl restart</code></pre></div><p>The -aG flag is important - &#8216;a&#8217; appends the user to the group rather than replacing their existing group memberships. Without it, you can silently strip a user of other group access.</p><h4>b. Keys Over Passwords - and Choose Your Algorithm</h4><p>Passwords are brute-forceable. SSH keys are not. But the algorithm you choose matters.</p><p><strong>Ed25519 is the right choice today.</strong> It is faster, shorter, and cryptographically stronger than RSA. Unless you have a specific compatibility constraint with legacy systems, use it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e30e6cb5-033a-4877-81a2-1cf709dbd536&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ssh-keygen -t ed25519 -C &#8220;user@domain.com&#8221;</code></pre></div><p>If you are on a system that does not support Ed25519, RSA with 4096 bits is the fallback. Avoid DSA entirely - it is limited to 1024-bit keys by the standard, which is considered broken. Avoid ECDSA unless you fully understand the curve parameters and trust them.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;d0ac87ae-f14c-4e20-8ad3-ed6e834bf372&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Fallback only - prefer Ed25519
ssh-keygen -t rsa -b 4096 -C &#8220;user@domain.com&#8221;

# Copy the public key to the server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@remote-host

# Then disable password auth in /etc/ssh/sshd_config
PasswordAuthentication no
GSSAPIAuthentication no</code></pre></div><h4>c. File Permissions - SSH Is Strict and Rightfully So</h4><p>SSH will silently refuse to use your keys if permissions are wrong. This catches more people out than it should.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e293472a-864b-40db-935f-75ac7f7a86b6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># On your workstation
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519.       # private key - only you
chmod 644 ~/.ssh/id_ed25519.pub    # public key - readable

# On the server
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys</code></pre></div><p>The private key at 600 is non-negotiable. If it is world-readable, SSH refuses to use it. The error message - &#8220;WARNING: UNPROTECTED PRIVATE KEY FILE&#8221; - is SSH doing you a favour.</p><h4>d. Rate-Limit the Knock with fail2ban</h4><p>Port 22 on a public IP will be knocked on constantly - automated scanners, credential-stuffing bots, slow-and-low probes. fail2ban watches the auth logs and bans IPs that exceed a failure threshold. The defaults are too lenient. Tune them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;toml&quot;,&quot;nodeId&quot;:&quot;91806956-b9d7-4378-bf34-a4c85ab9a10c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-toml"># /etc/fail2ban/jail.local
[sshd]
enabled = true
banaction = iptables-multiport
maxretry = 3
findtime = 1d
bantime = 4w

# Add to Boot and start the daemon service
systemctl enable fail2ban &amp;&amp; systemctl start fail2ban

# Check who is currently banned
fail2ban-client status sshd</code></pre></div><p>Three failed attempts in a day earns a four-week ban. That stops brute-force and slow-and-low attacks alike. The key insight: an attacker who gets banned on the third attempt will move on. There are easier targets.</p><h4>e. Two-Factor Auth - Because Keys Can Be Stolen Too</h4><p>A compromised laptop means a compromised private key. Adding a second factor means an attacker needs your private key AND your phone. For any server with a public IP, this friction is worth it.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;61eb6231-4e89-415e-b086-3be9c9600d36&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Install the package
apt install libpam-google-authenticator

# Run it
google-authenticator

# Answer: y y y n y to the prompts
# Scan the QR code with your authenticator app

# Add to the TOP of /etc/pam.d/sshd:
auth required pam_google_authenticator.so

# In /etc/ssh/sshd_config:
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive</code></pre></div><p>After this, login requires your private key first, then the OTP. Both must succeed. An attacker with your key but not your phone goes nowhere.</p><p>The principle underlying all of Level 1 is layered defence. Each measure is independent - a failure in one does not collapse the others. Root login disabled, key-only auth, rate limiting, and 2FA are four separate gates. An attacker has to defeat all of them, not just one.</p><h3>Level 2 - Bastion Host Architecture</h3><p>The day you move your application servers into a VPC with private subnets, the threat model shifts. Your instances should not be reachable from the internet at all - not even on port 22. The bastion host becomes the single controlled entry point into the private network.</p><p style="text-align: center;"><em>[ Internet ] &#8594; [ Bastion - public subnet ] &#8594; [ Private instances - private subnet ]</em></p><p>The bastion itself should be hardened with everything in Level 1. The key architectural decisions beyond that are about how keys flow - or more precisely, about ensuring they don&#8217;t flow to the wrong place.</p><h4>Never Store the Private Key on the Bastion</h4><p>This is the most common Level 2 mistake I see. The bastion is a proxy, not a key store. If it is compromised and your .pem is sitting on it, every instance it can reach is now compromised too.</p><p>Use SSH agent forwarding instead. Your workstation holds the key. The agent handles authentication end-to-end. The private key never leaves your machine/laptop - it is used to sign the authentication challenge locally, and only the signature travels over the wire.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;95f1e631-7aa0-457b-8fc2-6616f41dd605&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># ~/.ssh/config

# Bastion entry point
Host bastion-vpc1
  Hostname &lt;bastion-public-ip&gt;
  User ec2-user
  IdentityFile ~/.ssh/id_ed25519
  ForwardAgent yes

# Route any private IP in VPC-1 through the bastion transparently
Host 10.1.*.*
  User ec2-user
  IdentityFile ~/.ssh/id_ed25519
  ProxyCommand ssh bastion-vpc1 -W %h:%p

# Sensible defaults - keep this at the END of ~/.ssh/config
Host *
  ServerAliveInterval 30
  ServerAliveCountMax 2
  StrictHostKeyChecking accept-new</code></pre></div><p>With this config, <em><strong>ssh 10.1.1.45</strong></em> routes through the bastion automatically. Engineers type a single command. The hop is invisible to them.</p><p><strong>One note on StrictHostKeyChecking: </strong>the original version of this article used StrictHostKeyChecking no paired with UserKnownHostsFile /dev/null. Do not do this. Those two settings together disable host key verification entirely and discard all verification state - which opens the door to man-in-the-middle attacks. accept-new is the safe default: it automatically trusts new hosts on first connection, but rejects any subsequent change to a known host&#8217;s key. That is the behaviour you actually want.</p><p>The gap Level 2 does not close: the shared key problem. Everyone on the team uses the same key-pair. When someone leaves, you either rotate across every instance or accept the risk that they still technically have access. Most teams accept the risk. Most teams should not.</p><h3>Level 3 - Signed SSH Certificates: Zero-Trust at Scale</h3><p>I want you to think about what trust actually means in the key-based model. You add a public key to authorized_keys on a server. That key now has access to that server indefinitely - until someone manually removes it. There is no expiry. No central revocation. No audit trail of who used it when.</p><p>Now multiply that across fifty engineers and two hundred instances. You have a trust graph that nobody fully understands, that grows with every hire and never fully shrinks with every departure.</p><p><em>SSH certificates solve this by introducing a Certificate Authority. Instead of distributing keys to servers, you issue short-lived signed certificates. Access expires automatically. Trust is centralized.</em></p><p>The architecture has three roles: a CA server that holds the signing keys and issues certificates, target servers that trust the CA rather than individual keys, and engineers who present a signed certificate to authenticate.</p><h4>Setting Up the CA</h4><p>On your CA server, generate two signing key-pairs - one for hosts, one for users. Keep these keys offline or in a secure secrets manager. They are the root of trust for your entire fleet.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;61091285-411a-46b4-a685-5f23711ba878&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Host CA - proves the server is legitimate (prevents MITM)
ssh-keygen -t ed25519 -N &#8216;&#8217; -C HOST_CA -f /etc/ssh/ca/host_ca

# User CA - proves the engineer is legitimate
ssh-keygen -t ed25519 -N &#8216;&#8217; -C USER_CA -f /etc/ssh/ca/user_ca</code></pre></div><h4>Signing a Host Certificate</h4><p>Each server gets a signed host certificate. This is the part most teams skip - and it is important. Without it, engineers are still vulnerable to MITM attacks on their first connection to a new host. With it, the SSH client can cryptographically verify the server&#8217;s identity without relying on the trust-on-first-use model.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;127aed99-6c10-492d-b23d-bcb63c0d63fe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Create Host Certificate
ssh-keygen -s /etc/ssh/ca/host_ca \
-I host_server01 \
-h \
-n server01.internal \
-V +52w \
/etc/ssh/ssh_host_ed25519_key.pub

# Reference in /etc/ssh/sshd_config on the server:
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub</code></pre></div><h4>Signing a User Certificate</h4><p>An engineer submits their public key. The CA signs it with a short validity window and an explicit list of allowed usernames - called principals. The certificate expires. The engineer has to come back for a new one.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;3c312d06-8e46-4461-b48d-33525205d90b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Create User Certificate
ssh-keygen -s /etc/ssh/ca/user_ca \
-I &lt;username&gt;_laptop \
-n ec2-user,ubuntu \
-V +16h \
~/.ssh/id_ed25519.pub

# Produces: id_ed25519-cert.pub
# Engineer copies it to ~/.ssh/ - SSH picks it up automatically</code></pre></div><p><strong>Note on validity: </strong>the right TTL for user certificates in a zero-trust model is hours, not weeks. +16h to +24h is the production standard - long enough for a working day, short enough that a stolen certificate has a very narrow window. A 5-week certificate is not short-lived; it is just a key with a distant expiry date.</p><p>On every server, configure sshd to trust the user CA instead of managing authorized_keys:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;bf9e89d8-6d66-4c06-9fea-3c071b8a6640&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/ca/user_ca.pub</code></pre></div><p>You can inspect any certificate to verify its principals and expiry:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;f898b0be-ddc6-4e25-b7fb-2ba2a4c64ae9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub
# Output shows:
# Valid: from 2025-05-06T08:00:00 to 2025-05-07T00:00:00
# Principals: ec2-user, ubuntu
# Key ID: &#8220;alice_laptop&#8221;</code></pre></div><h4>What This Changes Operationally</h4><p><strong>Onboarding: </strong>engineer submits public key &#8594; CA signs it &#8594; done. No touching authorized_keys on any server. No Slack messages with .pem files.</p><p><strong>Offboarding: </strong>the certificate expires on its own - within 24 hours if you are using short TTLs. For immediate revocation, add the certificate serial to a Key Revocation List (KRL) and distribute it. One operation, fleet-wide effect.</p><p><strong>Audit: </strong>every certificate carries an identity (the key ID) and a serial number. Your access logs now show not just which IP connected, but which engineer and from which machine.</p><p><strong>Rotation: </strong>when you rotate the CA key, you issue new certificates to all hosts and users. One change, everything re-issues naturally. Compare this to rotating a shared .pem across two hundred instances.</p><h4>The Automation Gap</h4><p>Signing certificates manually works for a team of five. It does not work for a team of fifty. HashiCorp Vault&#8217;s SSH secrets engine handles this natively - engineers request a certificate via Vault, it is signed and returned with a configured TTL, and Vault maintains the full audit log. Engineers never see the CA private key. That is the production-grade implementation of this model, and the right destination for any team with compliance requirements.</p><h3>Choosing Your Level</h3><p><strong>Single VPS with public IP</strong> &#8594; Level 1. Harden the server. Layers of independent controls.</p><p><strong>Small team, VPC, stable headcount</strong> &#8594; Level 2. Bastion host, SSH config, agent forwarding. No keys on the bastion.</p><p><strong>Growing team, frequent joiners and leavers</strong> &#8594; Level 3. Signed certificates with short TTLs. Centralized trust.</p><p><strong>Compliance requirements (SOC2, ISO 27001)</strong> &#8594; Level 3, non-negotiable. The audit trail is the requirement.</p><p><strong>Multi-cloud with many instances</strong> &#8594; Level 3. Shared keys do not scale operationally or auditably.</p><p>The levels are cumulative, not alternatives. Level 3 still uses a hardened bastion. Level 2 still applies the server hardening from Level 1. Each layer builds on the one below.</p><h3>The .pem File Is Still Out There</h3><p>The Slack message from the opening of this post - &#8220;Here&#8217;s the key, don&#8217;t share it&#8221; - is not a security failure by any individual. It is a systems failure. When your process makes the insecure thing the easy thing, the insecure thing is what happens.</p><p>SSH certificates make the secure thing the easy thing. Onboarding is a single signing operation. Offboarding is automatic. The audit trail is built in. The blast radius of a compromised credential is bounded by its TTL.</p><p>Most teams wait for a security incident, an audit finding, or an ex-employee&#8217;s name appearing in an access log before they rethink their SSH model. The better time is before any of those things happen.</p><p><em>The .pem file in the Slack channel is not a starting point. It is a liability with a countdown.</em></p><p>If this helped you think through where your team sits on this model, share it with whoever owns your security posture. And if you have already made the journey to signed certificates - I&#8217;d genuinely like to hear what your team&#8217;s implementation looks like.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://sidh4u.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading TOPOLOGY! </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><br>Subscribe for free to receive new posts and support my work - or go paid for early access, deeper dives, and the occasional piece that never goes public..</p><p></p>]]></content:encoded></item></channel></rss>