<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kubernetes on houdeshell.dev</title><link>https://houdeshell.dev/tags/kubernetes/</link><description>Recent content in Kubernetes on houdeshell.dev</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>© CRH</copyright><lastBuildDate>Sat, 04 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://houdeshell.dev/tags/kubernetes/index.xml" rel="self" type="application/rss+xml"/><item><title>Software that I &lt;span style='color:var(--accent-bright);font-size:1.3em'>❤&lt;/span></title><link>https://houdeshell.dev/software/</link><pubDate>Mon, 01 Jun 2020 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/software/</guid><description>&lt;p>I bounce between Mac, Windows, and Linux daily — and honestly, the tools matter more than the OS at this point. Great software is great software, and I&amp;rsquo;ve been lucky enough to build a career on top of things that other people built well.&lt;/p>
&lt;p>This is the stuff I actually reach for. Not a &amp;ldquo;best of&amp;rdquo; list, not sponsored, not comprehensive. Just software that makes me better at what I do — or at least makes the work more fun.&lt;/p>
&lt;style>
.sw-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin: 1.5em 0;
}
.sw-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.2em;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.sw-card:hover {
border-color: var(--accent);
box-shadow: 0 0 16px rgba(57, 255, 110, 0.08);
}
.sw-card h3 {
margin: 0 0 0.3em !important;
font-size: 1em !important;
border: none !important;
padding: 0 !important;
}
.sw-card h3 a {
text-decoration: none;
}
.sw-card p {
margin: 0;
font-size: 0.85em;
color: var(--text-secondary);
line-height: 1.5;
}
.sw-card .sw-tag {
display: inline-block;
font-size: 0.7em;
color: var(--accent);
border: 1px solid var(--accent);
border-radius: 3px;
padding: 1px 6px;
margin-bottom: 0.5em;
opacity: 0.7;
}
&lt;/style>
&lt;h1 id="software-development">Software Development&lt;/h1>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">IDE&lt;/span>
&lt;h3 id="jetbrains-rider">&lt;a href="https://www.jetbrains.com/rider/"target="_blank" rel="noopener noreferrer">JetBrains Rider&lt;/a>&lt;/h3>
&lt;p>Cross-platform .NET IDE. Fast, smart, and doesn&amp;rsquo;t need Visual Studio&amp;rsquo;s weight to get the job done.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">IDE&lt;/span>
&lt;h3 id="visual-studio">&lt;a href="https://visualstudio.microsoft.com/"target="_blank" rel="noopener noreferrer">Visual Studio&lt;/a>&lt;/h3>
&lt;p>The OG. Heavy, but when you need the full .NET debugging experience, nothing else comes close.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">editor&lt;/span>
&lt;h3 id="vs-code">&lt;a href="https://code.visualstudio.com/"target="_blank" rel="noopener noreferrer">VS Code&lt;/a>&lt;/h3>
&lt;p>Extension ecosystem is unmatched. Somehow Electron done right.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">editor&lt;/span>
&lt;h3 id="neovim">&lt;a href="https://neovim.io/"target="_blank" rel="noopener noreferrer">Neovim&lt;/a>&lt;/h3>
&lt;p>Vim reborn. Lua config, LSP native, and a plugin ecosystem that won&amp;rsquo;t quit. &lt;code>:wq&lt;/code> is a lifestyle.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">AI&lt;/span>
&lt;h3 id="claude-code">&lt;a href="https://docs.anthropic.com/en/docs/claude-code"target="_blank" rel="noopener noreferrer">Claude Code&lt;/a>&lt;/h3>
&lt;p>AI pair programmer in your terminal. It&amp;rsquo;s writing this page right now.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">version control&lt;/span>
&lt;h3 id="git">&lt;a href="https://git-scm.com/"target="_blank" rel="noopener noreferrer">Git&lt;/a>&lt;/h3>
&lt;p>The version control system that won. Love it or hate it, you can&amp;rsquo;t ship without it.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">version control&lt;/span>
&lt;h3 id="lazygit">&lt;a href="https://github.com/jesseduffield/lazygit"target="_blank" rel="noopener noreferrer">lazygit&lt;/a>&lt;/h3>
&lt;p>Terminal UI for git that makes interactive rebases feel like cheating.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">JSON&lt;/span>
&lt;h3 id="jq">&lt;a href="https://jqlang.github.io/jq/"target="_blank" rel="noopener noreferrer">jq&lt;/a>&lt;/h3>
&lt;p>&lt;code>sed&lt;/code> for JSON. Once you learn the syntax, you&amp;rsquo;ll pipe everything through it.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="jetbrains-datagrip">&lt;a href="https://www.jetbrains.com/datagrip/"target="_blank" rel="noopener noreferrer">JetBrains DataGrip&lt;/a>&lt;/h3>
&lt;p>SQL IDE that actually understands your schema. Autocomplete that works across joins.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="dbeaver">&lt;a href="https://dbeaver.io/"target="_blank" rel="noopener noreferrer">DBeaver&lt;/a>&lt;/h3>
&lt;p>Universal database tool that actually works. Connect to anything, query everything.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="ssms">&lt;a href="https://learn.microsoft.com/en-us/sql/ssms/"target="_blank" rel="noopener noreferrer">SSMS&lt;/a>&lt;/h3>
&lt;p>Microsoft SQL Management Studio. If you&amp;rsquo;re in SQL Server land, you already know.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">database&lt;/span>
&lt;h3 id="postgresql">&lt;a href="https://www.postgresql.org/"target="_blank" rel="noopener noreferrer">PostgreSQL&lt;/a>&lt;/h3>
&lt;p>The database that keeps getting better. Extensions, JSON support, and rock-solid reliability.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">containers&lt;/span>
&lt;h3 id="docker">&lt;a href="https://www.docker.com/"target="_blank" rel="noopener noreferrer">Docker&lt;/a>&lt;/h3>
&lt;p>&amp;ldquo;Works on my machine&amp;rdquo; became &amp;ldquo;works on every machine.&amp;rdquo; Changed how we ship software.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h1 id="infrastructure">Infrastructure&lt;/h1>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">orchestration&lt;/span>
&lt;h3 id="kubernetes">&lt;a href="https://kubernetes.io/"target="_blank" rel="noopener noreferrer">Kubernetes&lt;/a>&lt;/h3>
&lt;p>Container orchestration that makes you mass produce YAML for a living.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">orchestration&lt;/span>
&lt;h3 id="k3s">&lt;a href="https://k3s.io/"target="_blank" rel="noopener noreferrer">K3s&lt;/a>&lt;/h3>
&lt;p>All of Kubernetes in a single binary. Perfect for homelab, edge, and production deployments.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">IaC&lt;/span>
&lt;h3 id="terraform--opentofu">&lt;a href="https://www.terraform.io/"target="_blank" rel="noopener noreferrer">Terraform&lt;/a> / &lt;a href="https://opentofu.org/"target="_blank" rel="noopener noreferrer">OpenTofu&lt;/a>&lt;/h3>
&lt;p>Infrastructure as code. OpenTofu if you prefer the open-source fork without the license drama.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">networking&lt;/span>
&lt;h3 id="tailscale">&lt;a href="https://tailscale.com/"target="_blank" rel="noopener noreferrer">Tailscale&lt;/a>&lt;/h3>
&lt;p>WireGuard-based mesh VPN that just works. Connect everything without opening a single port.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">observability&lt;/span>
&lt;h3 id="grafana">&lt;a href="https://grafana.com/"target="_blank" rel="noopener noreferrer">Grafana&lt;/a>&lt;/h3>
&lt;p>Dashboards for everything. Pairs with Prometheus, Loki, Tempo — the whole observability stack.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">Kubernetes&lt;/span>
&lt;h3 id="k9s">&lt;a href="https://k9scli.io/"target="_blank" rel="noopener noreferrer">k9s&lt;/a>&lt;/h3>
&lt;p>Terminal UI for Kubernetes. Makes cluster management feel like a video game. Way faster than raw &lt;code>kubectl&lt;/code>.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">Kubernetes&lt;/span>
&lt;h3 id="lens">&lt;a href="https://k8slens.dev/"target="_blank" rel="noopener noreferrer">Lens&lt;/a>&lt;/h3>
&lt;p>The Kubernetes IDE. When you want to point and click your way through a cluster without shame.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">it's complicated&lt;/span>
&lt;h3 id="cloudflare">&lt;a href="https://www.cloudflare.com/"target="_blank" rel="noopener noreferrer">Cloudflare&lt;/a>&lt;/h3>
&lt;p>You love them. You hate them. Your DNS is already there. Their edge network is everywhere, their pricing is unbeatable, and their product sprawl is terrifying. Stockholm syndrome as a service.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h1 id="shell--terminal">Shell &amp;amp; Terminal&lt;/h1>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">shell&lt;/span>
&lt;h3 id="bash">&lt;a href="https://www.gnu.org/software/bash/"target="_blank" rel="noopener noreferrer">Bash&lt;/a>&lt;/h3>
&lt;p>The shell that&amp;rsquo;s been there since before you were born. Simple, portable, everywhere.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">shell&lt;/span>
&lt;h3 id="zsh">&lt;a href="https://www.zsh.org/"target="_blank" rel="noopener noreferrer">Zsh&lt;/a>&lt;/h3>
&lt;p>Bash&amp;rsquo;s cooler sibling. Tab completion, globbing, and plugin support that actually makes the terminal enjoyable.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">prompt&lt;/span>
&lt;h3 id="starship">&lt;a href="https://github.com/starship/starship"target="_blank" rel="noopener noreferrer">Starship&lt;/a>&lt;/h3>
&lt;p>Cross-shell prompt written in Rust. Fast, pretty, and infinitely configurable.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">multiplexer&lt;/span>
&lt;h3 id="tmux">&lt;a href="https://github.com/tmux/tmux"target="_blank" rel="noopener noreferrer">tmux&lt;/a>&lt;/h3>
&lt;p>Terminal multiplexer. SSH into a box, detach, come back tomorrow. Your session is still there.&lt;/p>
&lt;/div>
&lt;div class="sw-card">
&lt;span class="sw-tag">coreutils&lt;/span>
&lt;h3 id="eza">&lt;a href="https://github.com/eza-community/eza"target="_blank" rel="noopener noreferrer">eza&lt;/a>&lt;/h3>
&lt;p>Modern replacement for &lt;code>ls&lt;/code>. Colors, git status, and tree view out of the box. Written in Rust, obviously.&lt;/p>
&lt;/div>
&lt;/div>
&lt;h1 id="utilities--apps">Utilities / Apps&lt;/h1>
&lt;div class="sw-grid">
&lt;div class="sw-card">
&lt;span class="sw-tag">macOS&lt;/span>
&lt;h3 id="alttab">&lt;a href="https://alt-tab-macos.netlify.app/"target="_blank" rel="noopener noreferrer">AltTab&lt;/a>&lt;/h3>
&lt;p>Why does macOS think a minimized window is an inactive window and not let you switch to it? This fixes that.&lt;/p>
&lt;/div>
&lt;/div></description></item><item><title>About</title><link>https://houdeshell.dev/about/</link><pubDate>Sat, 11 Apr 2020 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/about/</guid><description>&lt;div class="hero-card" style="margin-bottom:2em">
&lt;div class="hero-accent">&lt;/div>
&lt;div class="hero-content">
&lt;p class="hero-greeting">whoami&lt;/p>
&lt;p class="hero-name">Chris &lt;span class="hero-hl">Houdeshell&lt;/span>&lt;/p>
&lt;p class="hero-tagline">bit herder. pointer wrecker. yaml apologist.&lt;/p>
&lt;div class="hero-divider">&lt;/div>
&lt;p class="hero-currently">&lt;span class="hero-label">stack:&lt;/span> dotnet, postgres, k8s, azure, too-much-sql&lt;/p>
&lt;p class="hero-currently">&lt;span class="hero-label">location:&lt;/span> State College, PA&lt;/p>
&lt;p class="hero-currently">&lt;span class="hero-label">coffee intake:&lt;/span> dangerously high&lt;/p>
&lt;div class="hero-links">
&lt;a href="https://github.com/choudeshell">github&lt;/a>
&lt;span class="hero-sep">/&lt;/span>
&lt;a href="https://www.linkedin.com/in/choudeshell/">linkedin&lt;/a>
&lt;span class="hero-sep">/&lt;/span>
&lt;a href="https://sessionize.com/choudeshell/">talks&lt;/a>
&lt;span class="hero-sep">/&lt;/span>
&lt;a href="mailto:chris@houdeshell.dev">email&lt;/a>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;p>I&amp;rsquo;m a bit herder, a pointer wrecker, and HTTP is my best friend. &lt;code>0xA3&lt;/code> &amp;amp; &lt;code>0x7C&lt;/code> are also my friends, but not &lt;code>0x08&lt;/code>.&lt;/p>
&lt;p>I spend my days somewhere between writing code and making sure other people can write better code. I love distributed systems, low-level embedded devices, and anything that has a &lt;code>content-type&lt;/code>. I also have an unhealthy relationship with YAML, but that&amp;rsquo;s a Kubernetes problem.&lt;/p>
&lt;h2 id="the-day-job">The day job&lt;/h2>
&lt;p>During the day, I lead engineering and operations at &lt;a href="https://www.energycap.com"target="_blank" rel="noopener noreferrer">EnergyCAP&lt;/a> — a SaaS platform that helps organizations track utility spend, energy consumption, and sustainability goals across massive building portfolios. We&amp;rsquo;re talking &lt;strong>$100B+ in utility bill spend&lt;/strong> tracked across &lt;strong>350,000+ sites&lt;/strong>. Government, education, healthcare, commercial real estate — if it has a meter, we probably manage it.&lt;/p>
&lt;p>I started here as a developer in 2009 and have been building ever since — through senior roles, into leadership, and eventually into the VP seat where I get to shape both the tech and the teams behind it. Some days that means architecture decisions. Most days it means removing blockers so smart people can do their best work. And yes, sometimes it&amp;rsquo;s just meetings about meetings.&lt;/p>
&lt;p>My philosophy is simple: &lt;strong>technology should serve people&lt;/strong>. Build teams where people feel safe to be wrong, give them the tools to be great, and get out of the way.&lt;/p>
&lt;div class="about-section">
&lt;h3 id="things-i-geek-out-about">Things I geek out about&lt;/h3>
&lt;p>Distributed systems, container orchestration, database performance tuning, developer tooling, process dumps, leadership that doesn&amp;rsquo;t suck, making engineers&amp;rsquo; lives better, and the eternal quest for a perfect terminal setup.&lt;/p>
&lt;/div>
&lt;h2 id="i-also-talk-at-things">I also talk at things&lt;/h2>
&lt;p>I like getting on stage and nerding out about the things I&amp;rsquo;ve learned the hard way. My talks tend to fall into a few buckets — and yes, I have strong opinions about all of them.&lt;/p>
&lt;h3 id="ai--the-developer-experience">AI &amp;amp; the developer experience&lt;/h3>
&lt;p>The AI wave hit and I leaned in. I&amp;rsquo;ve talked about using LLMs as debugging partners, building RAG pipelines that actually know your business context, and the ethical lines we should be drawing as we hand more work to machines.&lt;/p>
&lt;ul class="talk-list">
&lt;li>My Rubber Duck is a Large Language Model&lt;/li>
&lt;li>Unleashing the Power of the AI Wizards: Retrieval-Augmented Generation Spells&lt;/li>
&lt;/ul>
&lt;h3 id="kubernetes--cloud-native">Kubernetes &amp;amp; cloud native&lt;/h3>
&lt;p>I was late to the Kubernetes party — and I&amp;rsquo;ll tell you all about it. From lightweight K3s workshops to navigating the 900+ services in the CNCF landscape, to making the case that cloud-native principles work even when you&amp;rsquo;re not in the cloud.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Kubernetes Chronicles: Late to the Party, Big on Adventure!&lt;/li>
&lt;li>K3s — Half the Size, Twice as Awesome (workshop)&lt;/li>
&lt;li>Orchestrating Machine Learning Workloads with Kubernetes&lt;/li>
&lt;li>Exploring the Cloud Native Landscape&lt;/li>
&lt;li>Cloud Native is Only for the Cloud, Right?&lt;/li>
&lt;li>Nomad — Orchestration Doesn't Start with a K&lt;/li>
&lt;/ul>
&lt;h3 id="performance--databases">Performance &amp;amp; databases&lt;/h3>
&lt;p>I&amp;rsquo;ve spent an unreasonable amount of time staring at query plans. These talks cover squeezing performance out of SQL Server, using DMVs like cheat codes, and that one time we took a 10-hour process down to 10 minutes.&lt;/p>
&lt;ul class="talk-list">
&lt;li>ReArchitecting Data: 10 Hours to 10 Minutes&lt;/li>
&lt;li>Mastering SQL Server Performance Optimization (workshop)&lt;/li>
&lt;li>SQL Server DMVs That Give Me Superpowers&lt;/li>
&lt;li>Achieving Continuous High Performance with Query Store&lt;/li>
&lt;li>Practical High Performance: C# Edition&lt;/li>
&lt;li>Intrinsics in .NET: Start Somewhere&lt;/li>
&lt;/ul>
&lt;h3 id="production-war-stories--leadership">Production war stories &amp;amp; leadership&lt;/h3>
&lt;p>Production breaks. Systems fail. The interesting part is what you do next. I also talk about the human side — what I&amp;rsquo;ve learned (and gotten wrong) leading engineering teams.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Bug Squashing with Process Dumps&lt;/li>
&lt;li>Recovery by Design: A Postmortem Adventure&lt;/li>
&lt;li>Joining the Cloud: Our Journey&lt;/li>
&lt;li>TIL as a CTO&lt;/li>
&lt;/ul>
&lt;h3 id="the-wildcard">The wildcard&lt;/h3>
&lt;p>Sometimes you just want to build something fun with your hands.&lt;/p>
&lt;ul class="talk-list">
&lt;li>Paper Circuits: Origami for a New Generation&lt;/li>
&lt;/ul>
&lt;h2 id="lets-talk">Let&amp;rsquo;s talk&lt;/h2>
&lt;p>I drink a bit too much coffee while rambling on about technology. If you&amp;rsquo;re willing to have a conversation — I&amp;rsquo;m ready to buy you a cup.&lt;/p>
&lt;div class="about-terminal">
&lt;span class="prompt">~&lt;/span> &lt;span class="cmd">echo $CONTACT&lt;/span>&lt;br>
&lt;span class="output">
email: &lt;a href="mailto:chris@houdeshell.dev">chris@houdeshell.dev&lt;/a>&lt;br>
github: &lt;a href="https://github.com/choudeshell">choudeshell&lt;/a>&lt;br>
linkedin: &lt;a href="https://www.linkedin.com/in/choudeshell/">choudeshell&lt;/a>&lt;br>
twitter: &lt;a href="https://twitter.com/choudeshell">@choudeshell&lt;/a>&lt;br>
&lt;/span>
&lt;/div></description></item><item><title>Kubernetes for Cloud Engineers: Automating TLS with cert-manager</title><link>https://houdeshell.dev/post/2026-04-04_k8s-cert-manager/k8s-cert-manager/</link><pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/post/2026-04-04_k8s-cert-manager/k8s-cert-manager/</guid><description>&lt;p>&lt;em>This is part two of a series for new operations and cloud engineers getting into Kubernetes. In &lt;a href="https://houdeshell.dev/post/2026-04-04_k8s-tls-default-cert/k8s-tls-default-cert/">part one&lt;/a> we covered how to set default TLS certificates on your ingress controllers. That works, but it means you&amp;rsquo;re managing certs by hand. Let&amp;rsquo;s fix that.&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="why-cert-manager">Why cert-manager?&lt;/h2>
&lt;p>In the last post, we created TLS secrets manually. That&amp;rsquo;s fine for getting started. It&amp;rsquo;s not fine for production. Certificates expire. People forget to renew them. And then you&amp;rsquo;re getting paged at 3am because your site is serving a browser warning.&lt;/p>
&lt;p>&lt;a href="https://cert-manager.io/"target="_blank" rel="noopener noreferrer">cert-manager&lt;/a> is a Kubernetes-native certificate management controller. It watches your cluster for resources that need certificates, talks to a Certificate Authority (usually Let&amp;rsquo;s Encrypt), handles the challenge/response dance, stores the resulting cert as a Kubernetes secret, and renews it before it expires. All automatically. You configure it once and move on with your life.&lt;/p>
&lt;p>It&amp;rsquo;s one of those tools that, once installed, you forget it&amp;rsquo;s there. That&amp;rsquo;s the highest compliment I can give infrastructure software.&lt;/p>
&lt;h2 id="installing-cert-manager">Installing cert-manager&lt;/h2>
&lt;p>Two options. Pick whichever matches your deployment style.&lt;/p>
&lt;h3 id="option-1-helm-recommended">Option 1: Helm (recommended)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install cert-manager oci://quay.io/jetstack/charts/cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v1.20.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set crds.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># NAME: cert-manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># NAMESPACE: cert-manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># STATUS: deployed&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="option-2-static-manifests">Option 2: Static manifests&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.0/cert-manager.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Both methods install the same thing: the cert-manager controller, webhook, cainjector, and six CRDs. Those CRDs are the building blocks you&amp;rsquo;ll use for everything else.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify everything is running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n cert-manager
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># NAME READY STATUS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># cert-manager-5c4b5f7b9-xk2lq 1/1 Running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># cert-manager-cainjector-7f694c-m8p 1/1 Running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># cert-manager-webhook-7cd8c8-9tn2f 1/1 Running&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three pods running. That&amp;rsquo;s what you want to see.&lt;/p>
&lt;h2 id="issuers-telling-cert-manager-where-to-get-certificates">Issuers: telling cert-manager where to get certificates&lt;/h2>
&lt;p>cert-manager doesn&amp;rsquo;t know anything about Let&amp;rsquo;s Encrypt out of the box. You need to tell it where to go and how to prove you own your domains. That&amp;rsquo;s what Issuers do.&lt;/p>
&lt;p>There are two flavors:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Issuer&lt;/strong> is namespace-scoped. It can only issue certs for resources in the same namespace.&lt;/li>
&lt;li>&lt;strong>ClusterIssuer&lt;/strong> is cluster-scoped. It works across all namespaces.&lt;/li>
&lt;/ul>
&lt;p>For most setups, you want a ClusterIssuer. One config, whole cluster. If you have teams that need isolated certificate management per namespace, use an Issuer. But start simple.&lt;/p>
&lt;h2 id="staging-first-always">Staging first. Always.&lt;/h2>
&lt;p>Let&amp;rsquo;s Encrypt has two environments: staging and production. They work identically, but staging issues certificates signed by a fake CA that browsers don&amp;rsquo;t trust. The certificates themselves are structurally real. They just won&amp;rsquo;t show a green lock.&lt;/p>
&lt;p>Why does this matter? Because Let&amp;rsquo;s Encrypt production has &lt;a href="https://letsencrypt.org/docs/rate-limits/"target="_blank" rel="noopener noreferrer">rate limits&lt;/a>. Tight ones. 50 certificates per registered domain per week. 5 duplicate certificates per week. If you mess up your config and keep retrying, you can lock yourself out for days.&lt;/p>
&lt;p>Staging has much more generous limits. So you should always get your pipeline working against staging first, then switch to production once you know everything is wired up correctly.&lt;/p>
&lt;h3 id="create-a-staging-clusterissuer">Create a staging ClusterIssuer&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># staging-issuer.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-staging&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">acme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://acme-staging-v02.api.letsencrypt.org/directory&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">you@example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKeySecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-staging-account-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">solvers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">http01&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingressClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f staging-issuer.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># clusterissuer.cert-manager.io/letsencrypt-staging created&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check that it registered successfully&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get clusterissuer letsencrypt-staging
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># NAME READY AGE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># letsencrypt-staging True 30s&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>READY: True&lt;/code> means cert-manager registered an account with Let&amp;rsquo;s Encrypt staging. If you see &lt;code>False&lt;/code>, run &lt;code>kubectl describe clusterissuer letsencrypt-staging&lt;/code> and read the events.&lt;/p>
&lt;h3 id="create-the-production-clusterissuer">Create the production ClusterIssuer&lt;/h3>
&lt;p>Same thing, different ACME server URL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># production-issuer.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">acme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://acme-v02.api.letsencrypt.org/directory&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">you@example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKeySecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod-account-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">solvers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">http01&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingressClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Use separate &lt;code>privateKeySecretRef&lt;/code> names for staging and production. The ACME accounts are completely independent. You can&amp;rsquo;t reuse keys between environments.&lt;/p>
&lt;h2 id="http-01-vs-dns-01-how-you-prove-domain-ownership">HTTP-01 vs DNS-01: how you prove domain ownership&lt;/h2>
&lt;p>When you request a certificate, Let&amp;rsquo;s Encrypt needs proof that you actually own the domain. There are two ways to do this, and which one you pick depends on your setup.&lt;/p>
&lt;h3 id="http-01">HTTP-01&lt;/h3>
&lt;p>Let&amp;rsquo;s Encrypt sends an HTTP request to &lt;code>http://yourdomain.com/.well-known/acme-challenge/&amp;lt;token&amp;gt;&lt;/code>. cert-manager spins up a temporary pod and Ingress (or HTTPRoute) to serve the response. Once validated, the temp resources get cleaned up.&lt;/p>
&lt;p>This is the simpler option. It works if:&lt;/p>
&lt;ul>
&lt;li>Port 80 is publicly reachable on your cluster&lt;/li>
&lt;li>You don&amp;rsquo;t need wildcard certificates&lt;/li>
&lt;/ul>
&lt;p>It does not work if:&lt;/p>
&lt;ul>
&lt;li>You&amp;rsquo;re behind a firewall with no public HTTP access&lt;/li>
&lt;li>You need &lt;code>*.example.com&lt;/code> certs&lt;/li>
&lt;/ul>
&lt;h3 id="dns-01">DNS-01&lt;/h3>
&lt;p>Instead of an HTTP request, Let&amp;rsquo;s Encrypt checks for a specific TXT record at &lt;code>_acme-challenge.yourdomain.com&lt;/code>. cert-manager talks to your DNS provider&amp;rsquo;s API to create and clean up the record.&lt;/p>
&lt;p>This is the only way to get wildcard certificates. It&amp;rsquo;s also the right choice for internal domains that aren&amp;rsquo;t publicly accessible. The tradeoff is more setup. You need to give cert-manager API credentials for your DNS provider, and DNS propagation delays can slow things down.&lt;/p>
&lt;p>&lt;strong>Built-in DNS providers:&lt;/strong> Cloudflare, Route53, Google Cloud DNS, Azure DNS, DigitalOcean, Akamai, ACMEDNS, and RFC-2136. There are webhook extensions for 30+ more.&lt;/p>
&lt;h2 id="wiring-it-up-with-ingress">Wiring it up with Ingress&lt;/h2>
&lt;p>If you&amp;rsquo;re using the traditional Ingress API, cert-manager makes this really clean. Add an annotation to your Ingress resource, include a &lt;code>tls&lt;/code> block, and cert-manager handles the rest.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># my-app-ingress.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cert-manager.io/cluster-issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-staging&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingressClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">paths&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pathType&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Prefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app-example-com-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. cert-manager sees the annotation, reads the &lt;code>tls&lt;/code> block, creates a Certificate resource, kicks off the ACME challenge, and stores the signed certificate in the &lt;code>app-example-com-tls&lt;/code> secret. Your ingress controller picks up the secret and starts serving HTTPS.&lt;/p>
&lt;p>When the cert is 30 days from expiration, cert-manager renews it automatically.&lt;/p>
&lt;div class="k8s-callout">
&lt;p>&lt;strong>Staging first, remember.&lt;/strong> Point the annotation at &lt;code>letsencrypt-staging&lt;/code> until you see a valid (but untrusted) cert in your browser. Then swap it to &lt;code>letsencrypt-prod&lt;/code>. You&amp;rsquo;ll need to delete the old secret so cert-manager issues a fresh one from production.&lt;/p>
&lt;/div>
&lt;p>Use &lt;code>cert-manager.io/cluster-issuer&lt;/code> for ClusterIssuers and &lt;code>cert-manager.io/issuer&lt;/code> for namespace-scoped Issuers. Mix them up and nothing will happen. No error, no cert. Just silence. Ask me how I know.&lt;/p>
&lt;h2 id="wiring-it-up-with-gateway-api">Wiring it up with Gateway API&lt;/h2>
&lt;p>If you followed the first post in this series, you know Gateway API is where things are heading. cert-manager supports it, but it needs a little extra configuration.&lt;/p>
&lt;h3 id="enable-gateway-api-support">Enable Gateway API support&lt;/h3>
&lt;p>Gateway API support is not on by default. You need to opt in.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set crds.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set config.enableGatewayAPI&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Make sure the Gateway API CRDs are installed in your cluster too:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply --server-side &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you installed the CRDs after cert-manager was already running, restart it so it picks them up:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl rollout restart deployment cert-manager -n cert-manager
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="annotate-your-gateway">Annotate your Gateway&lt;/h3>
&lt;p>The pattern is similar to Ingress. Add the annotation, reference a secret in the listener&amp;rsquo;s &lt;code>certificateRefs&lt;/code>, and cert-manager fills in the rest.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cert-manager.io/cluster-issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostname&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app.example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Terminate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">certificateRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app-example-com-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>cert-manager sees the annotation and the listener config, creates a Certificate for &lt;code>app.example.com&lt;/code>, and stores the result in the &lt;code>app-example-com-tls&lt;/code> secret. The Gateway picks it up and starts terminating TLS.&lt;/p>
&lt;p>Three things must be true for cert-manager to act on a Gateway listener:&lt;/p>
&lt;ol>
&lt;li>The &lt;code>hostname&lt;/code> is not empty&lt;/li>
&lt;li>The TLS &lt;code>mode&lt;/code> is &lt;code>Terminate&lt;/code> (not &lt;code>Passthrough&lt;/code>)&lt;/li>
&lt;li>There&amp;rsquo;s at least one entry in &lt;code>certificateRefs&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>Miss any of those and cert-manager quietly ignores the listener.&lt;/p>
&lt;h2 id="wildcard-certificates-with-dns-01">Wildcard certificates with DNS-01&lt;/h2>
&lt;p>This is where DNS-01 earns its keep. Let&amp;rsquo;s say you want a single cert that covers &lt;code>*.example.com&lt;/code> so every subdomain is automatically secured. HTTP-01 can&amp;rsquo;t do this. Only DNS-01 can.&lt;/p>
&lt;p>I&amp;rsquo;ll use Cloudflare as the DNS provider since it&amp;rsquo;s common (and because I have a love/hate relationship with them that we&amp;rsquo;ve already discussed on the &lt;a href="https://houdeshell.dev/software/">software page&lt;/a>).&lt;/p>
&lt;h3 id="step-1-create-a-cloudflare-api-token">Step 1: Create a Cloudflare API token&lt;/h3>
&lt;p>In the Cloudflare dashboard, create an API token with these permissions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Zone / DNS / Edit&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Zone / Zone / Read&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>Scope it to the zone you need. Don&amp;rsquo;t use a global API key if you can avoid it.&lt;/p>
&lt;h3 id="step-2-store-the-token-in-your-cluster">Step 2: Store the token in your cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># The secret MUST be in the cert-manager namespace for ClusterIssuers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cloudflare-api-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api-token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">your-cloudflare-api-token-here&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That namespace bit is important. When you use a ClusterIssuer, cert-manager looks for referenced secrets in the namespace where cert-manager itself is installed. Not the namespace of your Certificate. Not the namespace of your app. The cert-manager namespace. This trips up everyone at least once.&lt;/p>
&lt;h3 id="step-3-create-a-dns-01-clusterissuer">Step 3: Create a DNS-01 ClusterIssuer&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># dns-issuer.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod-dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">acme&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://acme-v02.api.letsencrypt.org/directory&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">email&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">you@example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">privateKeySecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod-dns-account-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">solvers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">dns01&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cloudflare&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiTokenSecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cloudflare-api-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-4-request-the-wildcard-cert">Step 4: Request the wildcard cert&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># wildcard-cert.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cert-manager.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Certificate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wildcard-example-com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wildcard-example-com-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuerRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod-dns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIssuer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dnsNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;*.example.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">example.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Include both &lt;code>*.example.com&lt;/code> and &lt;code>example.com&lt;/code> in the &lt;code>dnsNames&lt;/code>. The wildcard only covers subdomains, not the bare domain itself.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f wildcard-cert.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># certificate.cert-manager.io/wildcard-example-com created&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Watch it work&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get certificate wildcard-example-com -w
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># NAME READY SECRET AGE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># wildcard-example-com False wildcard-example-com-tls 5s&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># wildcard-example-com True wildcard-example-com-tls 47s&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DNS-01 is slower than HTTP-01 because of DNS propagation. Give it a minute or two. If it takes more than five minutes, start troubleshooting (see below).&lt;/p>
&lt;h2 id="when-things-go-wrong">When things go wrong&lt;/h2>
&lt;p>They will. Here&amp;rsquo;s how to figure out what happened.&lt;/p>
&lt;p>cert-manager has a chain of resources that it creates when processing a certificate request. Follow the chain from top to bottom:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Is the Certificate ready?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get certificates -A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. What does the CertificateRequest say?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get certificaterequest -A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe certificaterequest &amp;lt;name&amp;gt; -n &amp;lt;namespace&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. Is the Issuer healthy?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get clusterissuer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe clusterissuer &amp;lt;name&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. For Let&amp;#39;s Encrypt: check the ACME Order and Challenge&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get orders -A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get challenges -A
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe challenge &amp;lt;name&amp;gt; -n &amp;lt;namespace&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 5. Check the cert-manager logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -n cert-manager deployment/cert-manager --tail&lt;span class="o">=&lt;/span>&lt;span class="m">100&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Most problems fall into a handful of categories:&lt;/p>
&lt;p>&lt;strong>Challenge not reachable (HTTP-01).&lt;/strong> Port 80 isn&amp;rsquo;t open, or the temporary solver Ingress isn&amp;rsquo;t being picked up by your controller. Check that &lt;code>ingressClassName&lt;/code> in your solver config matches your actual ingress controller.&lt;/p>
&lt;p>&lt;strong>DNS propagation timeout (DNS-01).&lt;/strong> The TXT record was created but Let&amp;rsquo;s Encrypt can&amp;rsquo;t see it yet. If your cluster&amp;rsquo;s DNS resolver is slow, you can point cert-manager at public resolvers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade cert-manager oci://quay.io/jetstack/charts/cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace cert-manager &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="s1">&amp;#39;extraArgs={--dns01-recursive-nameservers-only,--dns01-recursive-nameservers=1.1.1.1:53\,9.9.9.9:53}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Secret in the wrong namespace.&lt;/strong> The number one DNS-01 headache. ClusterIssuers look for API token secrets in the cert-manager namespace. Not your app namespace. Not default.&lt;/p>
&lt;p>&lt;strong>Rate limited.&lt;/strong> You hit Let&amp;rsquo;s Encrypt production limits. Switch back to staging, wait for the rate limit window to pass (usually 7 days), fix whatever caused the excessive requests, and try again.&lt;/p>
&lt;p>&lt;strong>Issuer not ready.&lt;/strong> &lt;code>kubectl describe clusterissuer&lt;/code> will tell you why. Usually it&amp;rsquo;s a bad email address, an unreachable ACME server, or a malformed &lt;code>privateKeySecretRef&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Nothing happens at all.&lt;/strong> Check the annotation name. &lt;code>cert-manager.io/cluster-issuer&lt;/code> is not the same as &lt;code>cert-manager.io/issuer&lt;/code>. Using the wrong one for your Issuer type produces zero errors and zero certificates. It&amp;rsquo;s the most frustrating failure mode because there&amp;rsquo;s nothing in the logs.&lt;/p>
&lt;h2 id="a-note-on-certificate-lifetimes">A note on certificate lifetimes&lt;/h2>
&lt;p>Let&amp;rsquo;s Encrypt has been shortening certificate lifetimes. They started at 90 days, moved to 64, and are now pushing toward 45-day certificates. This doesn&amp;rsquo;t matter much if you&amp;rsquo;re using cert-manager because it handles renewal automatically (default is 30 days before expiration). But it does mean that if cert-manager breaks and you don&amp;rsquo;t notice, you have less runway before things go red.&lt;/p>
&lt;p>Monitor your certificates. At minimum, set up an alert on cert-manager pod health. Ideally, also alert on certificates that haven&amp;rsquo;t renewed within their expected window.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping up&lt;/h2>
&lt;p>Here&amp;rsquo;s the workflow once everything is configured:&lt;/p>
&lt;ol>
&lt;li>Deploy an Ingress or Gateway with the cert-manager annotation&lt;/li>
&lt;li>cert-manager creates a Certificate resource&lt;/li>
&lt;li>cert-manager talks to Let&amp;rsquo;s Encrypt, completes the challenge&lt;/li>
&lt;li>The signed certificate lands in a Kubernetes secret&lt;/li>
&lt;li>Your ingress controller or gateway picks it up&lt;/li>
&lt;li>cert-manager renews it before expiration&lt;/li>
&lt;/ol>
&lt;p>No cron jobs. No manual &lt;code>certbot renew&lt;/code>. No calendar reminders. Just certificates that work.&lt;/p>
&lt;p>If you&amp;rsquo;re running through this series from the beginning, you now have default TLS certs on your ingress controllers and automated certificate management with Let&amp;rsquo;s Encrypt. That&amp;rsquo;s a solid foundation.&lt;/p>
&lt;p>Next up in this series: &lt;strong>DNS automation with external-dns.&lt;/strong> Because if cert-manager removes the manual work from certificates, external-dns does the same thing for DNS records.&lt;/p></description></item><item><title>Kubernetes for Cloud Engineers: Default TLS Certificates</title><link>https://houdeshell.dev/post/2026-04-04_k8s-tls-default-cert/k8s-tls-default-cert/</link><pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/post/2026-04-04_k8s-tls-default-cert/k8s-tls-default-cert/</guid><description>&lt;p>&lt;em>This is the first post in a series for new operations and cloud engineers getting started with Kubernetes. Whether you&amp;rsquo;re running K3s on a Raspberry Pi, EKS in AWS, AKS in Azure, or anything in between — the concepts are the same. Let&amp;rsquo;s get into it.&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="the-problem">The problem&lt;/h2>
&lt;p>You&amp;rsquo;ve got a Kubernetes cluster. You&amp;rsquo;ve got an ingress controller routing traffic. You hit your app over HTTPS and&amp;hellip; you get a browser warning about an untrusted self-signed certificate. Or worse, you&amp;rsquo;ve got ten services and you&amp;rsquo;re copy-pasting the same TLS secret into every single Ingress resource.&lt;/p>
&lt;p>The fix: &lt;strong>set a default TLS certificate&lt;/strong> on your ingress controller. One cert to rule them all. Any request that doesn&amp;rsquo;t match a more specific TLS config falls back to this default. Clean, simple, done.&lt;/p>
&lt;p>Every controller does this slightly differently. Let&amp;rsquo;s walk through each one.&lt;/p>
&lt;hr>
&lt;h2 id="first--create-your-tls-secret">First — create your TLS secret&lt;/h2>
&lt;p>This part is the same regardless of which controller you&amp;rsquo;re running. You need a Kubernetes TLS secret containing your certificate and private key.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create the TLS secret from your cert and key files&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create secret tls default-tls &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --cert&lt;span class="o">=&lt;/span>tls.crt &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --key&lt;span class="o">=&lt;/span>tls.key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># secret/default-tls created&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Your cert file should contain the full chain: leaf → intermediate → root. If you&amp;rsquo;re using cert-manager or Let&amp;rsquo;s Encrypt, this is handled for you. If you&amp;rsquo;re doing it by hand — get the order right or you&amp;rsquo;ll be debugging trust chain errors at 2am.&lt;/p>
&lt;hr>
&lt;h2 id="ingress-controllers-legacy-ingress-api">Ingress Controllers (Legacy Ingress API)&lt;/h2>
&lt;div class="k8s-callout warning">
&lt;p>&lt;strong>Heads up: Ingress NGINX is retired.&lt;/strong> The &lt;code>kubernetes/ingress-nginx&lt;/code> project — the one most of us grew up on — officially reached end-of-life on &lt;strong>March 24, 2026&lt;/strong>. The repository is now read-only. No more releases, no bug fixes, &lt;strong>no security patches&lt;/strong>. Your existing deployments won&amp;rsquo;t break overnight, but you&amp;rsquo;re flying without a safety net.&lt;/p>
&lt;p>The Kubernetes Steering Committee and Security Response Committee issued a &lt;a href="https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement/"target="_blank" rel="noopener noreferrer">joint statement&lt;/a> in January 2026 urging migration. The recommended path forward is &lt;strong>Gateway API&lt;/strong>, which has been GA since October 2023 and now has 20+ conformant implementations.&lt;/p>
&lt;p>If you&amp;rsquo;re starting fresh — skip to the &lt;a href="#gateway-api">Gateway API section&lt;/a> below. If you&amp;rsquo;re migrating, check out &lt;code>ingress2gateway&lt;/code> — the official migration tool that now supports 30+ annotation conversions.&lt;/p>
&lt;p>&lt;strong>Note:&lt;/strong> NGINX Inc.&amp;rsquo;s commercial ingress controller (F5/NGINX) is a completely separate codebase and remains actively maintained. Don&amp;rsquo;t confuse the two.&lt;/p>
&lt;/div>
&lt;h3 id="ingress-nginx-kubernetesingress-nginx">Ingress NGINX (kubernetes/ingress-nginx)&lt;/h3>
&lt;p>Even though it&amp;rsquo;s retired, you&amp;rsquo;ll encounter this in the wild for a while. The default cert is set via a controller argument.&lt;/p>
&lt;p>&lt;strong>With Helm:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extraArgs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default-ssl-certificate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;default/default-tls&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade ingress-nginx ingress-nginx/ingress-nginx &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values.yaml &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n ingress-nginx
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Without Helm&lt;/strong> — patch the controller deployment directly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Add to the container args in the ingress-nginx-controller Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">controller&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">/nginx-ingress-controller&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">default-ssl-certificate=default/default-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The format is &lt;code>namespace/secret-name&lt;/code>. Without this flag, NGINX generates a self-signed certificate for the catch-all server — that&amp;rsquo;s the browser warning you&amp;rsquo;re seeing.&lt;/p>
&lt;p>&lt;strong>Gotcha:&lt;/strong> If your Ingress resource has a &lt;code>tls:&lt;/code> section but no &lt;code>secretName&lt;/code>, NGINX uses this default cert and forces an HTTPS redirect. If the &lt;code>tls:&lt;/code> section is missing entirely, it still serves the default cert but does &lt;em>not&lt;/em> redirect. Use &lt;code>force-ssl-redirect: &amp;quot;true&amp;quot;&lt;/code> in the ConfigMap if you want to enforce HTTPS everywhere.&lt;/p>
&lt;hr>
&lt;h3 id="haproxy-ingress">HAProxy Ingress&lt;/h3>
&lt;p>There are two HAProxy ingress projects — the community &lt;a href="https://haproxy-ingress.github.io/"target="_blank" rel="noopener noreferrer">haproxy-ingress&lt;/a> and the one from &lt;a href="https://github.com/haproxytech/kubernetes-ingress"target="_blank" rel="noopener noreferrer">HAProxy Technologies&lt;/a>. Both support the same flag pattern.&lt;/p>
&lt;p>&lt;strong>With Helm (HAProxy Technologies):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaultTLSSecret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretNamespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>With controller args (both projects):&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">--default-ssl-certificate=default/default-tls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade haproxy-ingress haproxytech/kubernetes-ingress &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.defaultTLSSecret.secretNamespace&lt;span class="o">=&lt;/span>default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.defaultTLSSecret.secret&lt;span class="o">=&lt;/span>default-tls &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n haproxy-ingress
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Without a default cert configured, both HAProxy implementations generate a self-signed fake certificate — same story as NGINX.&lt;/p>
&lt;p>&lt;strong>Note:&lt;/strong> HAProxy Technologies controller v1.11+ automatically enables QUIC (HTTP/3) when you set a default TLS cert. If you don&amp;rsquo;t want that yet, add &lt;code>--disable-quic&lt;/code>.&lt;/p>
&lt;hr>
&lt;h3 id="istio-ingress-gateway">Istio Ingress Gateway&lt;/h3>
&lt;p>Istio doesn&amp;rsquo;t use the Kubernetes Ingress API at all. It has its own &lt;code>Gateway&lt;/code> CRD (not the same as Gateway API — I know, the naming is painful).&lt;/p>
&lt;p>&lt;strong>Important:&lt;/strong> The TLS secret &lt;strong>must live in the same namespace as the Istio ingress gateway pod&lt;/strong> — typically &lt;code>istio-system&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret tls default-tls &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --cert&lt;span class="o">=&lt;/span>tls.crt &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --key&lt;span class="o">=&lt;/span>tls.key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n istio-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># secret/default-tls created&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then create the Gateway resource:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># istio-gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.istio.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">istio&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ingressgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">servers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">number&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SIMPLE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">credentialName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;*.example.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f istio-gateway.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># gateway.networking.istio.io/default-gateway created&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Gotchas:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>credentialName&lt;/code> must exactly match the Kubernetes secret name. No &lt;code>namespace/name&lt;/code> format here — it just looks in its own namespace.&lt;/li>
&lt;li>Istio doesn&amp;rsquo;t have a single &amp;ldquo;default certificate&amp;rdquo; concept. You configure TLS per-server block on the Gateway. To make it act as a default, use a wildcard host like &lt;code>*.example.com&lt;/code>.&lt;/li>
&lt;li>Wrong namespace is the #1 debugging headache. If TLS isn&amp;rsquo;t working, check the secret namespace first.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="gateway-api">Gateway API&lt;/h2>
&lt;p>Gateway API is the future. It&amp;rsquo;s the Kubernetes-native standard from SIG-Network, GA since October 2023, and every major ingress controller is building an implementation. If you&amp;rsquo;re starting fresh — start here.&lt;/p>
&lt;p>The big difference: there&amp;rsquo;s no &lt;code>--default-ssl-certificate&lt;/code> flag. TLS is configured &lt;strong>declaratively&lt;/strong> on the Gateway resource itself, per-listener. Each HTTPS listener explicitly references a certificate via &lt;code>certificateRefs&lt;/code>.&lt;/p>
&lt;h3 id="nginx-gateway-fabric">NGINX Gateway Fabric&lt;/h3>
&lt;p>NGINX Gateway Fabric is NGINX&amp;rsquo;s Gateway API implementation — completely separate from the retired Ingress NGINX project.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostname&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*.example.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Terminate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">certificateRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># gateway.gateway.networking.k8s.io/default-gateway created&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then attach your HTTPRoutes to the HTTPS listener:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># route.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sectionName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostnames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;app.example.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">my-app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>With cert-manager&lt;/strong> — add an annotation and cert-manager handles the rest:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml — cert-manager will create and manage the secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cert-manager.io/cluster-issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">letsencrypt-prod&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostname&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;app.example.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Terminate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">certificateRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h3 id="haproxy-gateway-api">HAProxy (Gateway API)&lt;/h3>
&lt;p>The beauty of Gateway API is that it&amp;rsquo;s a standard. The Gateway resource looks almost identical regardless of the underlying implementation — you just swap the &lt;code>gatewayClassName&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml — same pattern, different class&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">haproxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostname&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*.example.com&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Terminate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">certificateRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-tls&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># gateway.gateway.networking.k8s.io/default-gateway created&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Same spec. Same structure. The &lt;code>gatewayClassName&lt;/code> tells Kubernetes which controller reconciles the resource. This is exactly why Gateway API is the future — you can swap implementations without rewriting your config.&lt;/p>
&lt;p>&lt;strong>Gotchas for both implementations:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Gateway API doesn&amp;rsquo;t have a global &amp;ldquo;default certificate&amp;rdquo; flag. Each HTTPS listener must explicitly reference a cert via &lt;code>certificateRefs&lt;/code>. To cover everything, use a wildcard hostname.&lt;/li>
&lt;li>The TLS secret must be in the &lt;strong>same namespace&lt;/strong> as the Gateway by default. For cross-namespace references, create a &lt;code>ReferenceGrant&lt;/code>.&lt;/li>
&lt;li>TLS modes: &lt;code>Terminate&lt;/code> means the gateway decrypts traffic (most common). &lt;code>Passthrough&lt;/code> forwards encrypted traffic as-is — use this with &lt;code>TLSRoute&lt;/code>, not &lt;code>HTTPRoute&lt;/code>.&lt;/li>
&lt;li>NGINX Gateway Fabric currently supports only a single &lt;code>certificateRef&lt;/code> per listener. If you need multiple certs, create multiple listeners.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="wrapping-up">Wrapping up&lt;/h2>
&lt;p>Here&amp;rsquo;s the cheat sheet:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Controller&lt;/th>
&lt;th>Method&lt;/th>
&lt;th>Format&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Ingress NGINX &lt;em>(retired)&lt;/em>&lt;/td>
&lt;td>&lt;code>--default-ssl-certificate&lt;/code>&lt;/td>
&lt;td>&lt;code>namespace/secret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HAProxy Ingress&lt;/td>
&lt;td>&lt;code>--default-ssl-certificate&lt;/code>&lt;/td>
&lt;td>&lt;code>namespace/secret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Istio&lt;/td>
&lt;td>&lt;code>credentialName&lt;/code> on Gateway server&lt;/td>
&lt;td>secret name (same namespace)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway API (any impl)&lt;/td>
&lt;td>&lt;code>certificateRefs&lt;/code> on listener&lt;/td>
&lt;td>secret reference&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you&amp;rsquo;re starting fresh — go straight to Gateway API. Pick an implementation (NGINX Gateway Fabric, HAProxy, Envoy Gateway, whatever fits your stack), define your Gateway with a TLS listener, and move on.&lt;/p>
&lt;p>If you&amp;rsquo;re migrating from Ingress NGINX, take a breath. Your cluster won&amp;rsquo;t explode tomorrow. But start planning the move. The &lt;code>ingress2gateway&lt;/code> tool can automate most of the conversion, and the Gateway API spec is stable and well-documented.&lt;/p>
&lt;p>Next up in this series: &lt;strong>cert-manager and automated TLS&lt;/strong> — because creating secrets by hand is so 2024.&lt;/p></description></item><item><title>Engineering Managers Should Make Engineers Better</title><link>https://houdeshell.dev/post/2026-03-07_em-help/em-help/</link><pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/post/2026-03-07_em-help/em-help/</guid><description>&lt;p>During my regularly scheduled lurking on Hacker News, I came across &lt;a href="https://newsletter.manager.dev/p/dont-become-an-engineering-manager"target="_blank" rel="noopener noreferrer">Don&amp;rsquo;t Become an Engineering Manager&lt;/a>, making the case that senior engineers should stay IC. The ladder is flattening, Staff pays better, tech is moving too fast to step away. Probably right, but it bugs me.&lt;/p>
&lt;p>It treats the EM role like a career trade. A thing you optimize for or against. And when you look at it that way, sure, the math is shaky right now. But that skips the question I actually care about: what does a good engineering manager &lt;em>do&lt;/em> for the engineers on their team?&lt;/p>
&lt;h3 id="the-actual-job">The actual job&lt;/h3>
&lt;p>The best EMs I&amp;rsquo;ve worked with spend most of their time on the unglamorous stuff. Not &amp;ldquo;shielding the team&amp;rdquo; in the vague r/LinkedInLunatics way. The specific stuff. Why has the deploy pipeline been flaky for two weeks and nobody&amp;rsquo;s fixed it? Two teams are about to build the same thing, so get them in a room. Product is asking for something unreasonable and the engineers are too polite to say so.&lt;/p>
&lt;p>None of that shows up on a career ladder. It barely shows up in performance reviews. But it&amp;rsquo;s the difference between a team that ships and a team that fights its own org.&lt;/p>
&lt;h3 id="making-people-better">Making people better&lt;/h3>
&lt;p>The part of the job I care about most doesn&amp;rsquo;t come up in any of these &amp;ldquo;should you become an EM&amp;rdquo; debates. Are the engineers on your team getting better? Not in the promotion-packet sense. Is the person who joined six months ago actually understanding the system now? Is the senior who&amp;rsquo;s great at heads-down execution starting to think about what happens &lt;em>after&lt;/em> they ship?&lt;/p>
&lt;p>That work is mostly invisible. It&amp;rsquo;s a one-on-one that looks like a casual conversation. It&amp;rsquo;s asking &amp;ldquo;what would you do?&amp;rdquo; instead of giving the answer. It&amp;rsquo;s putting someone slightly past their comfort zone and making sure they don&amp;rsquo;t eat it.&lt;/p>
&lt;h3 id="so-should-you-become-an-em">So should you become an EM?&lt;/h3>
&lt;p>If you care about people, and you want to make those people better engineers, then yes. Engineers need you.&lt;/p></description></item><item><title>On being 'wrong'...</title><link>https://houdeshell.dev/post/2026-01-10_wrong/wrong/</link><pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/post/2026-01-10_wrong/wrong/</guid><description>&lt;p>Every bug I’ve ever shipped happened because the system did exactly what I told it to, not what I meant. That gap — between what I intended and what I actually built — is where most of the interesting lessons live.&lt;/p>
&lt;p>Last week, on three separate occasions, sharp engineers held back their ideas in meetings because they were afraid of being wrong. I only heard those ideas afterward, in side conversations. Every single one of them would have moved the discussion forward, regardless of whether they turned out to be “right.”&lt;/p>
&lt;p>That stuck with me.&lt;/p>
&lt;p>&lt;del>Engineers&lt;/del> People who can say “I was wrong” out loud tend to work better together. They pull in feedback earlier, course-correct faster, and make fewer of the same mistakes twice. The teams I’ve seen do the best work aren’t the ones where everyone is right all the time — they’re the ones where nobody is penalized for being wrong.&lt;/p>
&lt;p>We don’t get to correctness by avoiding mistakes. We get there by making them, catching them, and iterating. Most of the progress I’ve seen in my career has really just been a series of well-aimed misses.&lt;/p>
&lt;p>So say the wrong thing. Propose the bad idea. You might be surprised how often “wrong” turns out to be the fastest way forward.&lt;/p></description></item><item><title>Let's Blog Again</title><link>https://houdeshell.dev/post/houdeshell/</link><pubDate>Sat, 11 Apr 2020 00:00:00 +0000</pubDate><guid>https://houdeshell.dev/post/houdeshell/</guid><description>&lt;p>&lt;img loading="lazy" src="https://houdeshell.dev/static/images/eng_blogging.png"
alt="Software Engineer Blogging"/>&lt;/p>
&lt;p>Here’s the thing: most developers don’t blog. I haven&amp;rsquo;t in a long time. And that’s a shame — because it’s one of the easiest ways to level up your skills, stand out, and open doors you didn’t even know were there.&lt;/p>
&lt;p>I’m not talking about some corporate PR thing or a perfectly polished tech journal. I mean your own little corner of the internet where you write about what you’ve learned, what’s broken, and how you fixed it.&lt;/p>
&lt;h3 id="you-learn-better-when-you-explain-things">You learn better when you explain things&lt;/h3>
&lt;p>The moment you try to write about a tricky bug or a new library, you realize all the gaps in your own understanding. Filling those gaps? That’s where the real learning happens.&lt;/p>
&lt;h3 id="its-proof-you-know-your-stuff">It’s proof you know your stuff&lt;/h3>
&lt;p>Anyone can say “I work with Docker.” But if you’ve got posts about container networking quirks or real-world deployment tips, people can &lt;em>see&lt;/em> your expertise instead of just taking your word for it.&lt;/p>
&lt;h3 id="you-stand-out">You stand out&lt;/h3>
&lt;p>There are a lot of devs out there. Not many share their thinking publicly. Blogging makes you one of the people who contributes, not just consumes.&lt;/p>
&lt;h3 id="your-code-gets-a-backstory">Your code gets a backstory&lt;/h3>
&lt;p>GitHub shows &lt;em>what&lt;/em> you built. Your blog can show &lt;em>why&lt;/em> you built it that way — the trade-offs, the failed attempts, and the weird edge cases. That’s the stuff that’s interesting.&lt;/p>
&lt;h3 id="opportunities-find-you">Opportunities find you&lt;/h3>
&lt;p>A blog post you wrote in 2025 might get you a job lead in 2026. Seriously. The internet has a long memory.&lt;/p>
&lt;h3 id="you-get-better-at-communicating">You get better at communicating&lt;/h3>
&lt;p>Being able to explain complex ideas clearly is just as valuable as writing clean code. Blogging is practice you’ll feel in code reviews, design docs, and meetings.&lt;/p>
&lt;h3 id="you-see-your-own-growth">You see your own growth&lt;/h3>
&lt;p>Reading your old posts is like looking at old code — a little cringe, a lot of “wow, I’ve improved.” It’s motivating.&lt;/p>
&lt;h3 id="you-help-the-next-person">You help the next person&lt;/h3>
&lt;p>That dumb error message that cost you two hours? Write it up. Someone else will Google it tomorrow or use ChatGPT in 6 months.&lt;/p></description></item></channel></rss>