<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gaganpreet Singh</title>
    <description>The latest articles on DEV Community by Gaganpreet Singh (@gaganpreet_singh_1c3033cf).</description>
    <link>https://dev.to/gaganpreet_singh_1c3033cf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2772772%2Fb9c181b2-3e7c-4e60-9a7e-8edcb63ecc10.jpg</url>
      <title>DEV Community: Gaganpreet Singh</title>
      <link>https://dev.to/gaganpreet_singh_1c3033cf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaganpreet_singh_1c3033cf"/>
    <language>en</language>
    <item>
      <title>From single-master kubeadm to HA, AD logins and Gateway API: 7 things that broke</title>
      <dc:creator>Gaganpreet Singh</dc:creator>
      <pubDate>Wed, 23 Sep 2026 21:06:01 +0000</pubDate>
      <link>https://dev.to/gaganpreet_singh_1c3033cf/from-single-master-kubeadm-to-ha-ad-logins-and-gateway-api-7-things-that-broke-2n3n</link>
      <guid>https://dev.to/gaganpreet_singh_1c3033cf/from-single-master-kubeadm-to-ha-ad-logins-and-gateway-api-7-things-that-broke-2n3n</guid>
      <description>&lt;p&gt;Turning an on-prem kubeadm lab into a production-style platform: kube-vip HA, Velero restore drills, Active Directory logins for kubectl via Dex and structured authentication, and HAProxy Gateway API. With the mistakes.``&lt;/p&gt;




&lt;p&gt;title: "From single-master kubeadm to HA, AD logins and Gateway API: 7 things that broke"&lt;br&gt;
published: false&lt;br&gt;
description: "Turning an on-prem kubeadm lab into a production-style platform: kube-vip HA, Velero restore drills, Active Directory logins for kubectl via Dex and structured authentication, and HAProxy Gateway API. With the mistakes."&lt;br&gt;
tags: kubernetes, devops, security, tutorial&lt;/p&gt;

&lt;h2&gt;
  
  
  cover_image: &lt;a href="https://raw.githubusercontent.com/Willey2003/k8s-ha-ad-gateway-lab/main/docs/lab-infrastructure.png" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/Willey2003/k8s-ha-ad-gateway-lab/main/docs/lab-infrastructure.png&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Our lab Kubernetes cluster started the way many do: &lt;code&gt;kubeadm init&lt;/code&gt; on one VM, a few workers, and a kubeconfig copied around.&lt;br&gt;
It worked, until people started depending on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The starting point&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one control-plane node, so one reboot meant no API;&lt;/li&gt;
&lt;li&gt;no backups;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin.conf&lt;/code&gt; shared around, with local Linux accounts on every VM;&lt;/li&gt;
&lt;li&gt;clocks drifting by up to &lt;strong&gt;6 minutes&lt;/strong&gt;, which is enough to break TLS and Kerberos in creative ways.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The goal:&lt;/strong&gt; something a whole team can use safely, on plain VMware VMs, with no cloud load balancer.&lt;br&gt;
Everything below is scripted and published (sanitized) here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/Willey2003/k8s-ha-ad-gateway-lab" rel="noopener noreferrer"&gt;https://github.com/Willey2003/k8s-ha-ad-gateway-lab&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls5dlxwarj3yo09yycb5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fls5dlxwarj3yo09yycb5.png" alt="Architecture" width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Kubernetes 1.32 (kubeadm) · cri-o · Calico · external etcd 3.5 · kube-vip · Velero 1.18 · versitygw (S3) ·&lt;br&gt;
Dex 2.45 · MetalLB 0.16 · HAProxy Unified Gateway · Gateway API 1.3 · Active Directory (SSSD, Kerberos, LDAPS) · chrony&lt;/p&gt;




&lt;h2&gt;
  
  
  1. High availability: moving a &lt;em&gt;running&lt;/em&gt; cluster to a VIP
&lt;/h2&gt;

&lt;p&gt;Adding control-plane nodes is easy. The hard part is that a kubeadm cluster built on one node has &lt;strong&gt;that node's IP&lt;br&gt;
baked in everywhere&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;controlPlaneEndpoint&lt;/code&gt; in the &lt;code&gt;kubeadm-config&lt;/code&gt; ConfigMap;&lt;/li&gt;
&lt;li&gt;the API server certificate SANs;&lt;/li&gt;
&lt;li&gt;every kubelet's kubeconfig;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;kube-proxy&lt;/code&gt; ConfigMap;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;cluster-info&lt;/code&gt; ConfigMap in &lt;code&gt;kube-public&lt;/code&gt;, which new nodes use to join;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;admin.conf&lt;/code&gt; / &lt;code&gt;super-admin.conf&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used &lt;strong&gt;kube-vip&lt;/strong&gt; as a static pod in ARP mode: a floating IP that moves between control-plane nodes, with no external&lt;br&gt;
load balancer. The migration runs in three scripts, each with a backup and a rollback path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vip-1-manager.sh&lt;/code&gt;&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;adds the VIP and its DNS name to &lt;code&gt;certSANs&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;re-issues the API server certificate;&lt;/li&gt;
&lt;li&gt;rewrites &lt;code&gt;controlPlaneEndpoint&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;points &lt;code&gt;kube-proxy&lt;/code&gt;, &lt;code&gt;cluster-info&lt;/code&gt; and the admin kubeconfigs at the VIP;&lt;/li&gt;
&lt;li&gt;restarts kube-proxy.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vip-2-kubelet.sh&lt;/code&gt;&lt;/strong&gt;: on every node, switches the kubelet to the VIP and checks that the node stays &lt;code&gt;Ready&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vip-3-join.sh&lt;/code&gt;&lt;/strong&gt;: joins the new control-plane nodes against the VIP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;`bash&lt;br&gt;
kubectl get cm kube-proxy -n kube-system -o yaml \&lt;br&gt;
  | sed "s|https://$OLD:6443|https://$VIP:6443|" | kubectl replace -f -&lt;br&gt;
kubectl get cm cluster-info -n kube-public -o yaml \&lt;br&gt;
  | sed "s|https://$OLD:6443|https://$VIP:6443|" | kubectl replace -f -&lt;br&gt;
kubectl -n kube-system rollout restart ds kube-proxy&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A failover test script temporarily stops kube-vip on the node holding the VIP, checks that another control-plane node&lt;br&gt;
takes over the address and the API keeps answering, then puts kube-vip back as a standby.&lt;br&gt;
Result: &lt;strong&gt;about 1 second&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Backups you can trust, and a drill that proves it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;etcd snapshots:&lt;/strong&gt; weekly, from the bastion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Velero:&lt;/strong&gt; with the node agent (kopia), so persistent volume &lt;em&gt;data&lt;/em&gt; is backed up too, not just manifests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it goes:&lt;/strong&gt; a self-hosted &lt;strong&gt;S3 endpoint (versitygw)&lt;/strong&gt; on a dedicated disk, run by a systemd timer every Sunday.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part is the &lt;strong&gt;restore drill&lt;/strong&gt;. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;creates a namespace with a ConfigMap, a Secret and a pod that has a volume;&lt;/li&gt;
&lt;li&gt;writes a random proof string;&lt;/li&gt;
&lt;li&gt;backs it all up, deletes the namespace, and restores it;&lt;/li&gt;
&lt;li&gt;checks that every piece came back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My first version "passed"… for the wrong reason. More on that in lesson 7.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Active Directory for Linux &lt;em&gt;and&lt;/em&gt; for kubectl
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt; every node is joined to AD with &lt;code&gt;realmd&lt;/code&gt; + SSSD.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH access is limited by AD group: &lt;code&gt;simple_allow_groups&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo&lt;/code&gt; comes from an AD group.&lt;/li&gt;
&lt;li&gt;Kerberos gives single sign-on between nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regular users can SSH to the bastion only; admins can reach everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes:&lt;/strong&gt; kubectl logins with AD accounts need three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;LDAPS on the domain controllers:&lt;/strong&gt; a small internal CA signs a certificate for each DC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dex&lt;/strong&gt; as the OIDC provider, with an LDAP connector against AD and the password grant, so a CLI user just types their
AD password.&lt;/li&gt;
&lt;li&gt;The API server's &lt;strong&gt;structured authentication config&lt;/strong&gt; (&lt;code&gt;AuthenticationConfiguration&lt;/code&gt;, beta since 1.30). It replaces
the old &lt;code&gt;--oidc-*&lt;/code&gt; flags and lets you map claims with &lt;strong&gt;CEL&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;yaml&lt;br&gt;
apiVersion: apiserver.config.k8s.io/v1beta1&lt;br&gt;
kind: AuthenticationConfiguration&lt;br&gt;
jwt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;issuer:
url: &lt;a href="https://dex.corp.example:32000" rel="noopener noreferrer"&gt;https://dex.corp.example:32000&lt;/a&gt;
audiences: [kubernetes]
certificateAuthority: |
  -----BEGIN CERTIFICATE-----
  ...
claimMappings:
username:
  expression: "'ad:' + claims.preferred_username"
groups:
  expression: "(has(claims.groups) ? dyn(claims.groups).map(g, 'ad:' + string(g)) : []) + ['ad:all-users']"
uid:
  claim: sub
claimValidationRules:

&lt;ul&gt;
&lt;li&gt;expression: "has(claims.preferred_username) &amp;amp;&amp;amp; claims.preferred_username != ''"
message: "token has no preferred_username"
&lt;code&gt;&lt;/code&gt;`&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trick is in the last part of &lt;code&gt;groups&lt;/code&gt;. &lt;strong&gt;Every&lt;/strong&gt; authenticated AD user also gets the synthetic group&lt;br&gt;
&lt;code&gt;ad:all-users&lt;/code&gt;, so RBAC can grant a baseline to all of them without anyone maintaining an AD group:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;Kubernetes permissions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ad:all-users&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;view&lt;/code&gt; cluster-wide (no Secrets) + &lt;code&gt;edit&lt;/code&gt; in a &lt;code&gt;playground&lt;/code&gt; namespace with a ResourceQuota and Pod Security &lt;em&gt;baseline&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ad:K8s-Admins&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cluster-admin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;break-glass&lt;/td&gt;
&lt;td&gt;local &lt;code&gt;admin.conf&lt;/code&gt; on the control plane&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the bastion, &lt;strong&gt;kubelogin&lt;/strong&gt; is preconfigured on first login. The first &lt;code&gt;kubectl&lt;/code&gt; prompts for the AD password once, and&lt;br&gt;
tokens refresh for up to 7 days.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Ingress for 2026: Gateway API instead of Ingress-NGINX
&lt;/h2&gt;

&lt;p&gt;Ingress-NGINX has been retired, so this was a good moment to go straight to &lt;strong&gt;Gateway API&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MetalLB&lt;/strong&gt; in L2 mode hands out LoadBalancer IPs on the VM network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HAProxy Unified Gateway&lt;/strong&gt; implements the &lt;code&gt;haproxy&lt;/code&gt; GatewayClass.&lt;/li&gt;
&lt;li&gt;One shared &lt;code&gt;Gateway&lt;/code&gt; with a wildcard certificate serves &lt;code&gt;*.apps.corp.example&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;yaml&lt;br&gt;
apiVersion: gateway.networking.k8s.io/v1&lt;br&gt;
kind: Gateway&lt;br&gt;
metadata: {name: apps, namespace: haproxy-unified-gateway}&lt;br&gt;
spec:&lt;br&gt;
  gatewayClassName: haproxy&lt;br&gt;
  listeners:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: https
protocol: HTTPS
port: 443
hostname: "*.apps.corp.example"
tls:
  mode: Terminate
  certificateRefs: [{kind: Secret, name: apps-wildcard-tls}]
allowedRoutes: {namespaces: {from: All}}
&lt;code&gt;&lt;/code&gt;`&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To publish an app, a user needs only an &lt;code&gt;HTTPRoute&lt;/code&gt;. There's no certificate, IP or LoadBalancer to manage:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;yaml&lt;br&gt;
apiVersion: gateway.networking.k8s.io/v1&lt;br&gt;
kind: HTTPRoute&lt;br&gt;
metadata: {name: hello}&lt;br&gt;
spec:&lt;br&gt;
  parentRefs: [{name: apps, namespace: haproxy-unified-gateway}]&lt;br&gt;
  hostnames: [hello.apps.corp.example]&lt;br&gt;
  rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backendRefs: [{name: hello, port: 80}]
&lt;code&gt;&lt;/code&gt;`&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This split of roles is the real win of Gateway API: platform admins own the &lt;code&gt;Gateway&lt;/code&gt;, and app teams own their routes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part worth reading: 7 things that broke
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Check the router's subnet, not just your hosts'
&lt;/h3&gt;

&lt;p&gt;Every VM had a &lt;code&gt;/24&lt;/code&gt;, so I gave MetalLB a pool high in the range. The gateway IP answered perfectly from inside the&lt;br&gt;
subnet… and &lt;strong&gt;silently timed out from every other network&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The router's interface was configured as a &lt;strong&gt;&lt;code&gt;/27&lt;/code&gt;&lt;/strong&gt;, so it never forwarded traffic for anything above &lt;code&gt;.31&lt;/code&gt;. Moving&lt;br&gt;
the Gateway into the routed range fixed it immediately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Test load-balancer IPs from a &lt;em&gt;different&lt;/em&gt; network before you announce them.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Validate the API-server auth config &lt;em&gt;before&lt;/em&gt; a rolling restart
&lt;/h3&gt;

&lt;p&gt;My first groups expression was &lt;code&gt;claims.groups.map(g, 'ad:' + g)&lt;/code&gt;. CEL type-checking rejects it, because &lt;code&gt;claims.groups&lt;/code&gt;&lt;br&gt;
is &lt;code&gt;any&lt;/code&gt;, and &lt;code&gt;any&lt;/code&gt; isn't iterable. It needs &lt;code&gt;dyn(claims.groups)&lt;/code&gt;. The API server refuses to start with an invalid&lt;br&gt;
config, so this crash-looped &lt;strong&gt;two&lt;/strong&gt; control-plane nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix for next time:&lt;/strong&gt; run the same &lt;code&gt;kube-apiserver&lt;/code&gt; binary locally with only &lt;code&gt;--authentication-config&lt;/code&gt; pointing at&lt;br&gt;
the file. It fails in seconds with the exact CEL error, and it doesn't touch the cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A health check must prove the &lt;em&gt;new&lt;/em&gt; container is healthy
&lt;/h3&gt;

&lt;p&gt;After editing a static pod manifest, &lt;code&gt;/readyz&lt;/code&gt; may still be answered by the &lt;strong&gt;old&lt;/strong&gt; API server process for a few seconds.&lt;br&gt;
So a naive "wait for readyz" passes, and then the new container crash-loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; record the container ID first, wait for a &lt;em&gt;different&lt;/em&gt; container ID, then require several consecutive&lt;br&gt;
healthy checks. If that fails, restore the backup manifest automatically.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;bash&lt;br&gt;
cid() { crictl ps --name '^kube-apiserver$' -q | head -1; }&lt;/p&gt;

&lt;h1&gt;
  
  
  ...wait until $(cid) != $OLD_CID AND /readyz is ok 6 times in a row, else roll back
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;h3&gt;
  
  
  4. HAProxy sizes its memory budget from the container limit
&lt;/h3&gt;

&lt;p&gt;With a 1 GiB memory limit, TLS stopped working and the log showed a &lt;code&gt;maxsslconn&lt;/code&gt;/&lt;code&gt;memmax&lt;/code&gt; alert. HAProxy derives &lt;code&gt;memmax&lt;/code&gt;&lt;br&gt;
from the cgroup limit and checks it against the default &lt;code&gt;maxconn&lt;/code&gt;. Raising the limit to ~2.5 GiB fixed it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When a proxy misbehaves only under TLS, look at memory settings before certificates.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5. Dex expands &lt;code&gt;$VAR&lt;/code&gt; in config values
&lt;/h3&gt;

&lt;p&gt;The LDAP bind password contained a &lt;code&gt;$&lt;/code&gt;. Dex expanded it as an environment variable, which turned it into a different&lt;br&gt;
password, and AD answered with "invalid credentials" for a password that was &lt;em&gt;definitely&lt;/em&gt; correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; set &lt;code&gt;DEX_EXPAND_ENV=false&lt;/code&gt; in the Dex deployment. It cost an afternoon.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Never make a repair tool depend on the thing it repairs
&lt;/h3&gt;

&lt;p&gt;The first password-rotation script for the Dex service account used &lt;code&gt;kubectl&lt;/code&gt;… logged in &lt;strong&gt;through Dex&lt;/strong&gt;. So when Dex&lt;br&gt;
couldn't bind to AD, the tool that fixes Dex couldn't run.&lt;/p&gt;

&lt;p&gt;The final version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authenticates to AD directly over LDAPS;&lt;/li&gt;
&lt;li&gt;resets the password, and verifies the new one with a real bind;&lt;/li&gt;
&lt;li&gt;writes the Dex secret over SSH, using the control plane's &lt;strong&gt;local&lt;/strong&gt; admin config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No copy-paste, and it works exactly when it's needed: when Dex is broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Prove the restore, not the backup
&lt;/h3&gt;

&lt;p&gt;My first restore drill "passed". But the proof string was in the pod's command&lt;br&gt;
(&lt;code&gt;echo $PROOF &amp;gt; /data/proof.txt&lt;/code&gt;), so the &lt;em&gt;restored pod spec&lt;/em&gt; simply wrote it again.&lt;/p&gt;

&lt;p&gt;The volume restore could have been completely broken, and the test would still have been green. Now the string is written&lt;br&gt;
only with &lt;code&gt;kubectl exec&lt;/code&gt; into the volume, after the pod has started. Only a real volume restore can bring it back.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A test that can't fail isn't a test. For backups, verify the &lt;em&gt;data&lt;/em&gt;, not the objects.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bonus: small things that saved hours
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time sync first.&lt;/strong&gt; Clocks were off by up to 6 minutes, which silently breaks Kerberos, TLS and even &lt;code&gt;kubeadm join&lt;/code&gt;
tokens. chrony against the domain controllers fixed it on every host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;realm join&lt;/code&gt; on a host whose &lt;code&gt;hostname -f&lt;/code&gt; is short&lt;/strong&gt; registers only short SPNs, and Kerberos SSO fails with "Server
not found in Kerberos database". Fix &lt;code&gt;/etc/hosts&lt;/code&gt; so the FQDN comes first, then register the &lt;code&gt;HOST/&amp;lt;fqdn&amp;gt;&lt;/code&gt; SPN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helm charts that install CRDs in a post-install job race the controller:&lt;/strong&gt; restart the controller once after the
first install.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The platform is solid, but there's plenty left:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitOps with Argo CD;&lt;/li&gt;
&lt;li&gt;Prometheus/Grafana observability;&lt;/li&gt;
&lt;li&gt;moving etcd onto dedicated VMs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What would you add first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All scripts, the architecture diagram (draw.io + SVG) and the run order are here:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/Willey2003/k8s-ha-ad-gateway-lab" rel="noopener noreferrer"&gt;https://github.com/Willey2003/k8s-ha-ad-gateway-lab&lt;/a&gt;&lt;/strong&gt;. Issues and suggestions are welcome.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
