<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Serhii Chechun - Developer&#39;s Blog</title>
    <link>https://blog.chechun.org/</link>
    <description>Recent content on Serhii Chechun - Developer&#39;s Blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <copyright>© 2026 Serhii Chechun</copyright>
    <lastBuildDate>Sun, 26 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.chechun.org/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>About</title>
      <link>https://blog.chechun.org/about/</link>
      <pubDate>Sun, 26 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid>https://blog.chechun.org/about/</guid>
      <description>&lt;h1 id=&#34;hi-im-serhii-&#34;&gt;Hi, I&amp;rsquo;m Serhii 👋&lt;/h1&gt;
&lt;p&gt;I&amp;rsquo;m a &lt;strong&gt;Software Engineer/Architect&lt;/strong&gt; based in &lt;strong&gt;London, UK&lt;/strong&gt;, with 20+ years of experience in software engineering and networking. I specialise in designing and building scalable, distributed systems, cloud-native platforms, and microservices - primarily in Go.&lt;/p&gt;
&lt;h2 id=&#34;career-highlights&#34;&gt;Career highlights&lt;/h2&gt;
&lt;p&gt;Over two decades in tech, spanning networking, infrastructure, and software engineering. I started out maintaining computer labs and teaching students while studying, then spent over a decade building and running ISP networks - fibre-optic infrastructure, BGP routing, billing systems, cloud hosting, and VoIP. Since then I&amp;rsquo;ve designed and delivered gRPC/REST microservices, event-driven pipelines, and platform infrastructure across a wide range of industries: fintech, AdTech, healthcare, food delivery, social networking, and car rental. Along the way I&amp;rsquo;ve authored payment integrations from scratch, built real-time proxies, led backend teams, contributed to Kubernetes operators, and migrated legacy monoliths to Go.&lt;/p&gt;</description>
      <content>&lt;h1 id=&#34;hi-im-serhii-&#34;&gt;Hi, I&amp;rsquo;m Serhii 👋&lt;/h1&gt;
&lt;p&gt;I&amp;rsquo;m a &lt;strong&gt;Software Engineer/Architect&lt;/strong&gt; based in &lt;strong&gt;London, UK&lt;/strong&gt;, with 20+ years of experience in software engineering and networking. I specialise in designing and building scalable, distributed systems, cloud-native platforms, and microservices - primarily in Go.&lt;/p&gt;
&lt;h2 id=&#34;career-highlights&#34;&gt;Career highlights&lt;/h2&gt;
&lt;p&gt;Over two decades in tech, spanning networking, infrastructure, and software engineering. I started out maintaining computer labs and teaching students while studying, then spent over a decade building and running ISP networks - fibre-optic infrastructure, BGP routing, billing systems, cloud hosting, and VoIP. Since then I&amp;rsquo;ve designed and delivered gRPC/REST microservices, event-driven pipelines, and platform infrastructure across a wide range of industries: fintech, AdTech, healthcare, food delivery, social networking, and car rental. Along the way I&amp;rsquo;ve authored payment integrations from scratch, built real-time proxies, led backend teams, contributed to Kubernetes operators, and migrated legacy monoliths to Go.&lt;/p&gt;
&lt;h2 id=&#34;this-blog&#34;&gt;This Blog&lt;/h2&gt;
&lt;p&gt;I write about things I build and learn - covering software engineering, distributed systems, cloud-native infrastructure, databases, design of programming languages, and whatever problem I&amp;rsquo;m currently solving or find interesting.&lt;/p&gt;
&lt;p style=&#34;color: color-mix(in srgb, var(--color) 40%, transparent); font-size: 0.85em; margin-top: 1.5em;&#34;&gt;Built with &lt;a href=&#34;https://gohugo.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; style=&#34;color: color-mix(in srgb, var(--color) 40%, transparent);&#34;&gt;Hugo&lt;/a&gt; and &lt;a href=&#34;https://github.com/mirus-ua/hugo-theme-re-terminal&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34; style=&#34;color: color-mix(in srgb, var(--color) 40%, transparent);&#34;&gt;re-Terminal&lt;/a&gt;&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Links</title>
      <link>https://blog.chechun.org/links/</link>
      <pubDate>Sun, 26 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid>https://blog.chechun.org/links/</guid>
      <description>&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://github.com/serhii-chechun&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;GitHub&lt;/a&gt;
&lt;/p&gt;</description>
      <content>&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://github.com/serhii-chechun&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;GitHub&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://www.linkedin.com/in/serhii-chechun/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;LinkedIn&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://x.com/cogitarium&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Twitter / X&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://bsky.app/profile/chechun.org&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Bluesky&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://www.youtube.com/@cogitarium&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;YouTube&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://instagram.com/serhii_chechun&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Instagram&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://www.facebook.com/serhii.chechun/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Facebook&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;svg width=&#34;18&#34; height=&#34;18&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;currentColor&#34; style=&#34;vertical-align: middle; margin-right: 4px;&#34;&gt;&lt;path d=&#34;M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z&#34;/&gt;&lt;/svg&gt; &lt;a href=&#34;https://www.reddit.com/user/cogitarium/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Reddit&lt;/a&gt;
&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Part 1: Zero to DBaaS: Building a Multi-Cloud Kubernetes Platform with eBPF &amp; WireGuard</title>
      <link>https://blog.chechun.org/posts/zero-to-dbaas-part-1-multi-cloud-k8s-ebpf/</link>
      <pubDate>Sat, 25 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid>https://blog.chechun.org/posts/zero-to-dbaas-part-1-multi-cloud-k8s-ebpf/</guid>
      <description>&lt;p&gt;This is the first post in my &lt;strong&gt;Zero to DBaaS&lt;/strong&gt; series, where I&amp;rsquo;m building a global, multi-tenant Database-as-a-Service platform from scratch. By the end of this post, we&amp;rsquo;ll have an 8-node Kubernetes cluster spanning three independent providers - two Fasthosts VPS servers running a high-availability control plane, three AWS EC2 instances, and three GCP Compute Engine instances - all connected by Cilium eBPF with WireGuard encrypting every packet that crosses the public internet.&lt;/p&gt;</description>
      <content>&lt;p&gt;This is the first post in my &lt;strong&gt;Zero to DBaaS&lt;/strong&gt; series, where I&amp;rsquo;m building a global, multi-tenant Database-as-a-Service platform from scratch. By the end of this post, we&amp;rsquo;ll have an 8-node Kubernetes cluster spanning three independent providers - two Fasthosts VPS servers running a high-availability control plane, three AWS EC2 instances, and three GCP Compute Engine instances - all connected by Cilium eBPF with WireGuard encrypting every packet that crosses the public internet.&lt;/p&gt;
&lt;p&gt;No kube-proxy. No cloud VPNs. Just the Linux kernel doing the heavy lifting.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;architecture&#34;&gt;Architecture&lt;/h2&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;                      ┌────────────────────────────────────────────────────────┐
                      │              FASTHOSTS (Management Hub)                │
                      │  ┌──────────────────────┐    ┌──────────────────────┐  │
                      │  │  k8s-cp-01 (Primary) │    │  k8s-cp-02 (HA Sync) │  │
                      │  └──────────┬───────────┘    └──────────┬───────────┘  │
                      └─────────────┼───────────────────────────┼──────────────┘
                                    │                           │
====================================│===========================│======================
                                    │ Cilium WireGuard Tunnel   │  (Public Internet)
                                    │      (UDP Port 51871)     │
====================================│===========================│======================
                                    │                           │
             ┌──────────────────────┴─────────────┬─────────────┘
             │                                    │ 
             ▼                                    ▼ 
┌─────────────────────────┐          ┌─────────────────────────┐ 
│       AWS POOL          │          │        GCP POOL         │ 
│  (Tenant A Workers)     │          │   (Tenant B Workers)    │ 
│  • k8s-worker-aws-01    │          │   • k8s-worker-gcp-01   │ 
│  • k8s-worker-aws-02    │          │   • k8s-worker-gcp-02   │ 
│  • k8s-worker-aws-03    │          │   • k8s-worker-gcp-03   │ 
└─────────────────────────┘          └─────────────────────────┘ 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The control plane lives on cost-effective VPS infrastructure. Each tenant&amp;rsquo;s worker pool is isolated within its own cloud provider&amp;rsquo;s VPC - database replication and client traffic never leave that provider&amp;rsquo;s network, so there are zero cross-cloud data-plane egress costs. The only traffic crossing the public internet is control-plane communication: API server &amp;lt;-&amp;gt; kubelet heartbeats, Cilium policy sync, Hubble telemetry. All of it encrypted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Node inventory:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Role&lt;/th&gt;
					&lt;th&gt;Provider&lt;/th&gt;
					&lt;th&gt;Count&lt;/th&gt;
					&lt;th&gt;Type&lt;/th&gt;
					&lt;th&gt;Labels&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Control Plane&lt;/td&gt;
					&lt;td&gt;Fasthosts VPS&lt;/td&gt;
					&lt;td&gt;2&lt;/td&gt;
					&lt;td&gt;2 vCPU / 4 GB&lt;/td&gt;
					&lt;td&gt;-&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Tenant A Workers&lt;/td&gt;
					&lt;td&gt;AWS EC2&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;t3.small (2 vCPU / 2 GB)&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;cloud=aws,tenant=tenant-a&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Tenant B Workers&lt;/td&gt;
					&lt;td&gt;GCP Compute Engine&lt;/td&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;e2-small (2 vCPU / 2 GB)&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;cloud=gcp,tenant=tenant-b&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Port matrix:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Port&lt;/th&gt;
					&lt;th&gt;Protocol&lt;/th&gt;
					&lt;th&gt;Scope&lt;/th&gt;
					&lt;th&gt;Purpose&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;6443&lt;/td&gt;
					&lt;td&gt;TCP&lt;/td&gt;
					&lt;td&gt;Control plane nodes&lt;/td&gt;
					&lt;td&gt;Kubernetes API&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2379–2380&lt;/td&gt;
					&lt;td&gt;TCP&lt;/td&gt;
					&lt;td&gt;Fasthosts CP nodes&lt;/td&gt;
					&lt;td&gt;etcd HA sync&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;51871&lt;/td&gt;
					&lt;td&gt;UDP&lt;/td&gt;
					&lt;td&gt;All nodes&lt;/td&gt;
					&lt;td&gt;Cilium WireGuard&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;10250&lt;/td&gt;
					&lt;td&gt;TCP&lt;/td&gt;
					&lt;td&gt;All worker nodes&lt;/td&gt;
					&lt;td&gt;Kubelet API&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;prerequisites&#34;&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before we touch Kubernetes, eight machines need to exist. I provisioned them using the native cloud CLIs - here&amp;rsquo;s how:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fasthosts (2 nodes):&lt;/strong&gt; Manual provisioning via their console. Two standard VPS instances running Ubuntu 26.04 LTS, 2 vCPU / 4 GB each. Record both public IPs - we&amp;rsquo;ll need them throughout.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AWS (3 nodes):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ SG_ID&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;aws ec2 create-security-group --group-name k8s-multicloud-workers &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --description &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;K8s multi-cloud workers&amp;#34;&lt;/span&gt; --query &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;GroupId&amp;#34;&lt;/span&gt; --output text&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol tcp --port &lt;span style=&#34;color:#ae81ff&#34;&gt;10250&lt;/span&gt; --cidr 0.0.0.0/0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ aws ec2 authorize-security-group-ingress --group-id $SG_ID --protocol udp --port &lt;span style=&#34;color:#ae81ff&#34;&gt;51871&lt;/span&gt; --cidr 0.0.0.0/0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i in &lt;span style=&#34;color:#f92672&#34;&gt;{&lt;/span&gt;1..3&lt;span style=&#34;color:#f92672&#34;&gt;}&lt;/span&gt;; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   aws ec2 run-instances --image-id &amp;lt;UBUNTU_26.04_AMI&amp;gt; --count &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$     --instance-type t3.small --key-name k8s-multicloud-key &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$     --security-group-ids $SG_ID --associate-public-ip-address &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$     --tag-specifications &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ResourceType=instance,Tags=[{Key=Name,Value=k8s-worker-aws-0&lt;/span&gt;$i&lt;span style=&#34;color:#e6db74&#34;&gt;},{Key=Tenant,Value=tenant-a}]&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ &lt;span style=&#34;color:#66d9ef&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Use &lt;code&gt;--security-group-ids&lt;/code&gt; (plural, with the ID), not &lt;code&gt;--security-groups&lt;/code&gt; (singular, with the name). Mixing the two gives you &lt;code&gt;InvalidParameterCombination&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;GCP (3 nodes):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ gcloud compute firewall-rules create k8s-allow-kubelet &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --direction&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;INGRESS --action&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ALLOW --rules&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tcp:10250 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --source-ranges&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;0.0.0.0/0 --target-tags&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;k8s-multicloud-workers
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ gcloud compute firewall-rules create k8s-allow-wireguard &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --direction&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;INGRESS --action&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ALLOW --rules&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;udp:51871 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --source-ranges&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;0.0.0.0/0 --target-tags&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;k8s-multicloud-workers
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ PUB_KEY&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;cat ~/.ssh/k8s-multicloud-key.pub&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i in &lt;span style=&#34;color:#f92672&#34;&gt;{&lt;/span&gt;1..3&lt;span style=&#34;color:#f92672&#34;&gt;}&lt;/span&gt;; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   gcloud compute instances create k8s-worker-gcp-0$i &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$     --machine-type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;e2-small --image-family&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ubuntu-2604-lts-amd64 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$     --image-project&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ubuntu-os-cloud --tags&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;k8s-multicloud-workers &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$     --labels&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant-b,cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;gcp --metadata&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ssh-keys&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ubuntu:&lt;/span&gt;$PUB_KEY&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ &lt;span style=&#34;color:#66d9ef&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-1-os-preparation-all-8-nodes&#34;&gt;Step 1: OS Preparation (All 8 Nodes)&lt;/h2&gt;
&lt;p&gt;Every node needs the same kernel tweaks. SSH into each one and run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Kernel modules required by Cilium eBPF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ cat &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;lt;&amp;lt;EOF | sudo tee /etc/modules-load.d/k8s.conf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;$ overlay
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;$ br_netfilter
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;$ EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo modprobe overlay
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo modprobe br_netfilter
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# IP forwarding - mandatory for pod networking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ cat &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/k8s.conf
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;$ net.bridge.bridge-nf-call-iptables  = 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;$ net.bridge.bridge-nf-call-ip6tables = 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;$ net.ipv4.ip_forward                 = 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;$ EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo sysctl --system
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Kubernetes requires swap disabled&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo swapoff -a
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo sed -i &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/swap/d&amp;#39;&lt;/span&gt; /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-2-install-containerd-all-8-nodes&#34;&gt;Step 2: Install containerd (All 8 Nodes)&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt-get update -qq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt-get install -y -qq containerd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo mkdir -p /etc/containerd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ containerd config default | sudo tee /etc/containerd/config.toml &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Cilium requires the systemd cgroup driver&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo sed -i &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;s/SystemdCgroup = false/SystemdCgroup = true/&amp;#39;&lt;/span&gt; /etc/containerd/config.toml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo systemctl restart containerd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo systemctl enable containerd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-3-install-kubeadm-kubelet-kubectl&#34;&gt;Step 3: Install kubeadm, kubelet, kubectl&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;On control plane nodes&lt;/strong&gt; (kubectl is only needed here):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ K8S_VERSION&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1.36&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ K8S_MAJOR&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$K8S_VERSION&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; | cut -d. -f1-2&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo mkdir -p /etc/apt/keyrings
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl -fsSL &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://pkgs.k8s.io/core:/stable:/v&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;K8S_MAJOR&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/deb/Release.key&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$&lt;span style=&#34;color:#e6db74&#34;&gt;   https://pkgs.k8s.io/core:/stable:/v&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;K8S_MAJOR&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/deb/ /&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   | sudo tee /etc/apt/sources.list.d/kubernetes.list &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt-get update -qq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt-get install -y -qq kubelet kubeadm kubectl
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt-mark hold kubelet kubeadm kubectl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;On worker nodes&lt;/strong&gt; (no kubectl needed):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Same steps, but:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt-get install -y -qq kubelet kubeadm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;configure-kubelet-to-advertise-the-public-ip&#34;&gt;Configure kubelet to advertise the public IP&lt;/h3&gt;
&lt;p&gt;On Fasthosts CP nodes, the public IP is on a local interface, so &lt;code&gt;--node-ip&lt;/code&gt; works:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ LOCAL_IP&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;curl -s ifconfig.me&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;KUBELET_EXTRA_ARGS=--node-ip=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;LOCAL_IP&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; | sudo tee /etc/default/kubelet
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo systemctl restart kubelet
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On cloud workers, the public IP is NAT-based - not on any local interface - so kubelet silently ignores &lt;code&gt;--node-ip&lt;/code&gt;. This means &lt;code&gt;kubectl get nodes -o wide&lt;/code&gt; will show &lt;code&gt;&amp;lt;none&amp;gt;&lt;/code&gt; for &lt;code&gt;INTERNAL-IP&lt;/code&gt; on AWS and GCP nodes. That&amp;rsquo;s fine. Cilium maintains its own node registry with the correct public IPs for WireGuard. The Kubernetes Node object&amp;rsquo;s &lt;code&gt;INTERNAL-IP&lt;/code&gt; field is cosmetic.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;step-4-initialize-the-control-plane-k8s-cp-01&#34;&gt;Step 4: Initialize the Control Plane (k8s-cp-01)&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s where we diverge from every Kubernetes tutorial: we&amp;rsquo;re skipping kube-proxy entirely. Cilium eBPF replaces it at the socket level.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ CP1_PUBLIC_IP&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;curl -s ifconfig.me&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo kubeadm init &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --apiserver-advertise-address&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;CP1_PUBLIC_IP&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --control-plane-endpoint&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;CP1_PUBLIC_IP&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:6443&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --pod-network-cidr&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;10.244.0.0/16 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --kubernetes-version&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;v1.36.0 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --skip-phases&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;addon/kube-proxy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output includes the join commands we&amp;rsquo;ll need. Save everything.&lt;/p&gt;
&lt;h3 id=&#34;set-up-kubectl&#34;&gt;Set up kubectl&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ mkdir -p $HOME/.kube
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo chown &lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;id -u&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;:&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;id -g&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt; $HOME/.kube/config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;get-join-tokens&#34;&gt;Get join tokens&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Worker join command&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubeadm token create --print-join-command
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Certificate key for HA control plane join (needs sudo - this caught me out)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo kubeadm init phase upload-certs --upload-certs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My cluster&amp;rsquo;s values:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# Worker join:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;kubeadm join 74.208.24.234:6443 --token &amp;lt;TOKEN&amp;gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --discovery-token-ca-cert-hash sha256:&amp;lt;HASH&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# CP join cert key:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;64-CHAR-HEX-KEY&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-5-install-helm&#34;&gt;Step 5: Install Helm&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ helm version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-6-install-cilium-cni-with-wireguard&#34;&gt;Step 6: Install Cilium CNI with WireGuard&lt;/h2&gt;
&lt;p&gt;This is the centerpiece of the post. Cilium replaces kube-proxy for service routing, and WireGuard transparently encrypts ALL node-to-node traffic crossing the public internet.&lt;/p&gt;
&lt;p&gt;There are two gotchas here that cost me a couple of hours.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #1:&lt;/strong&gt; On a single-node cluster, Cilium tries to reach the API server via the Kubernetes ClusterIP (&lt;code&gt;10.96.0.1:443&lt;/code&gt;). But Cilium IS the CNI - it hasn&amp;rsquo;t routed that IP yet. Deadlock. The fix is &lt;code&gt;--set k8sServiceHost&lt;/code&gt; and &lt;code&gt;--set k8sServicePort&lt;/code&gt;, which tell Cilium to connect to the API server directly via the public IP until it&amp;rsquo;s ready to route the ClusterIP itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #2:&lt;/strong&gt; The default 2-operator-replica deployment can&amp;rsquo;t schedule on a single node. Scale to 1 immediately after install.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ helm repo add cilium https://helm.cilium.io/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ CP1_PUBLIC_IP&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;curl -s ifconfig.me&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ helm install cilium cilium/cilium &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --namespace kube-system &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set kubeProxyReplacement&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;true &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set encryption.enabled&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;true &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set encryption.type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;wireguard &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set encryption.nodeEncryption&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;true &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set cluster.name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;multi-cloud-k8s &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set ipam.mode&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;cluster-pool &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set ipam.operator.clusterPoolIPv4PodCIDRList&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;0&lt;span style=&#34;color:#f92672&#34;&gt;]=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;10.244.0.0/16&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set k8sServiceHost&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;CP1_PUBLIC_IP&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set k8sServicePort&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;6443&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Scale operator for single-node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl -n kube-system scale deployment cilium-operator --replicas&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wait for Cilium to come up:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ cilium status --wait
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;encryption.nodeEncryption=true&lt;/code&gt; matters:&lt;/strong&gt; By default, Cilium&amp;rsquo;s WireGuard only encrypts pod-to-pod traffic. Node encryption ensures ALL traffic - kubelet health checks, host-network daemons, Hubble telemetry - gets encrypted when crossing the public internet.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;step-7-join-the-second-control-plane-k8s-cp-02&#34;&gt;Step 7: Join the Second Control Plane (k8s-cp-02)&lt;/h2&gt;
&lt;p&gt;On cp-02, run Steps 1–3 (OS prep, containerd, kubeadm/kubelet) first. Then:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo kubeadm join 74.208.24.234:6443 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --token &amp;lt;TOKEN&amp;gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --discovery-token-ca-cert-hash sha256:&amp;lt;HASH&amp;gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --control-plane &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --certificate-key &amp;lt;CERT_KEY&amp;gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --node-name k8s-cp-02
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both CP nodes have a &lt;code&gt;control-plane&lt;/code&gt; taint that blocks regular pods from scheduling. Since we don&amp;rsquo;t have worker nodes yet, remove the taint from cp-02 so Prometheus and Grafana can land:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl taint node k8s-cp-02 node-role.kubernetes.io/control-plane:NoSchedule-
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl get nodes
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;NAME        STATUS   ROLES           AGE     VERSION
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-cp-02   Ready    control-plane   10m     v1.36.3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ubuntu      Ready    control-plane   142m    v1.36.3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-8-verify-wireguard-encryption&#34;&gt;Step 8: Verify WireGuard Encryption&lt;/h2&gt;
&lt;p&gt;With two nodes, WireGuard has a peer to talk to:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Install tools&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo apt-get install -y wireguard-tools
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Check the WireGuard interface&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo wg show cilium_wg0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;interface: cilium_wg0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  listening port: 51871
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;peer: jPHq6ZW1kD4i1zrbIaDbafi/SVtV7Yy3wrhZ5PWMAhg=
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  endpoint: 198.251.78.107:51871
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  allowed ips: 198.251.78.107/32
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  latest handshake: 32 seconds ago
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  transfer: 6.95 KiB received, 6.74 KiB sent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A peer with &lt;code&gt;latest handshake: X seconds ago&lt;/code&gt; and actual transfer bytes - the encrypted tunnel is live. Now let&amp;rsquo;s prove it with tcpdump.&lt;/p&gt;
&lt;p&gt;Find the network interface (Ubuntu 26.04 uses predictable names - &lt;code&gt;ens6&lt;/code&gt; in my case, not &lt;code&gt;eth0&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ ip link show | grep -E &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;^[0-9]+:&amp;#39;&lt;/span&gt; | grep -v lo | head -3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo tcpdump -n -i ens6 udp port &lt;span style=&#34;color:#ae81ff&#34;&gt;51871&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;00:01:12.292923 IP 198.251.78.107.51871 &amp;gt; 74.208.24.234.51871: UDP, length 144
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;00:01:12.293187 IP 74.208.24.234.51871 &amp;gt; 198.251.78.107.51871: UDP, length 144
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;00:01:24.709815 IP 198.251.78.107.51871 &amp;gt; 74.208.24.234.51871: UDP, length 144
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;00:01:24.710198 IP 74.208.24.234.51871 &amp;gt; 198.251.78.107.51871: UDP, length 144
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Only encrypted UDP on port 51871. No plain-text pod traffic anywhere. Every packet between these two VPS instances - sitting on different physical hosts, crossing the open internet - is scrambled inside a WireGuard tunnel.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;step-9-prometheus--grafana-observability-stack&#34;&gt;Step 9: Prometheus + Grafana Observability Stack&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;kube-prometheus-stack&lt;/code&gt; Helm chart is powerful but heavy. With only 4 GB per CP node, we need to tune it way down. Learned this the hard way - the default install includes AlertManager (we don&amp;rsquo;t need it for a demo), admission webhooks (need TLS certs that break on small clusters), and a 10-day data retention policy that eats RAM for breakfast.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ helm repo update
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ helm install prometheus prometheus-community/kube-prometheus-stack &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --namespace monitoring &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --create-namespace &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set alertmanager.enabled&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;false &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set prometheus.prometheusSpec.retention&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;24h &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set prometheus.prometheusSpec.resources.requests.memory&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;512Mi &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set prometheus.prometheusSpec.resources.requests.cpu&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;150m &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;false &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set grafana.adminPassword&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;admin &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set grafana.resources.requests.memory&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;128Mi &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set nodeExporter.resources.requests.memory&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;64Mi &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set kube-state-metrics.resources.requests.memory&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;128Mi &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set prometheusOperator.admissionWebhooks.enabled&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;false &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set prometheusOperator.admissionWebhooks.patch.enabled&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;false &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --set prometheusOperator.tls.enabled&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;false
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The last three flags - &lt;code&gt;admissionWebhooks.enabled=false&lt;/code&gt;, &lt;code&gt;admissionWebhooks.patch.enabled=false&lt;/code&gt;, and &lt;code&gt;tls.enabled=false&lt;/code&gt; - prevent the operator from trying to start a TLS-secured webhook server that would need certificates we haven&amp;rsquo;t provisioned. For a demo cluster, this is fine.&lt;/p&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl -n monitoring get pods
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;NAME                                                   READY   STATUS    RESTARTS   AGE
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;prometheus-grafana-554f4fbc7b-ftbgq                    3/3     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;          2m
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;prometheus-kube-prometheus-operator-644d8979cd-xrnft   1/1     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;          2m
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;prometheus-kube-state-metrics-8995b68d6-gb68d          1/1     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;          2m
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;prometheus-prometheus-kube-prometheus-prometheus-0     2/2     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;          2m
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;prometheus-prometheus-node-exporter-r9t6d              1/1     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;          2m
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;prometheus-prometheus-node-exporter-rqvqk              1/1     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;          2m
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Access Grafana:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl -n monitoring port-forward svc/prometheus-grafana 3000:80
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# From your local machine: ssh -L 3000:localhost:3000 clusteradmin@&amp;lt;CP1_IP&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Open http://localhost:3000, login admin/admin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pre-built dashboards for Kubernetes Compute Resources and Networking are ready to go. For Cilium-specific eBPF metrics, import dashboard &lt;code&gt;16611&lt;/code&gt; from the Grafana community library and patch the Cilium agent service to expose metrics:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl -n kube-system patch svc cilium-agent &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   -p &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;{&amp;#34;spec&amp;#34;:{&amp;#34;ports&amp;#34;:[{&amp;#34;name&amp;#34;:&amp;#34;metrics&amp;#34;,&amp;#34;port&amp;#34;:9962,&amp;#34;protocol&amp;#34;:&amp;#34;TCP&amp;#34;,&amp;#34;targetPort&amp;#34;:9962}]}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-10-enable-hubble&#34;&gt;Step 10: Enable Hubble&lt;/h2&gt;
&lt;p&gt;Hubble gives you real-time eBPF flow visualization - pod-to-pod traffic, dropped packets, L4/L7 policy decisions, all rendered as a service map:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ cilium hubble enable --ui
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl -n kube-system get pods | grep hubble
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hubble-relay-6fb864ff5d-skgbf       1/1     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;    26s
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;hubble-ui-677bcd7f96-jg9d8          2/2     Running   &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;    26s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Access the UI:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ cilium hubble ui
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Or: kubectl -n kube-system port-forward svc/hubble-ui 12000:80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;step-11-join-the-worker-nodes&#34;&gt;Step 11: Join the Worker Nodes&lt;/h2&gt;
&lt;p&gt;Now for the real multi-cloud part. Each worker needs the OS prep (Step 1), containerd (Step 2), and kubeadm/kubelet (Step 3). Then:&lt;/p&gt;
&lt;h3 id=&#34;aws-workers-tenant-a&#34;&gt;AWS Workers (Tenant A)&lt;/h3&gt;
&lt;p&gt;On each AWS instance:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo kubeadm join 74.208.24.234:6443 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --token &amp;lt;TOKEN&amp;gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --discovery-token-ca-cert-hash sha256:&amp;lt;HASH&amp;gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --node-name k8s-worker-aws-0X    &lt;span style=&#34;color:#75715e&#34;&gt;# Replace X with 1, 2, 3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then on cp-01, label each:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl label node k8s-worker-aws-01 cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;aws tenant&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant-a
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl label node k8s-worker-aws-02 cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;aws tenant&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant-a
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl label node k8s-worker-aws-03 cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;aws tenant&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant-a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;gcp-workers-tenant-b&#34;&gt;GCP Workers (Tenant B)&lt;/h3&gt;
&lt;p&gt;On each GCP instance:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo kubeadm join 74.208.24.234:6443 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --token &amp;lt;TOKEN&amp;gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --discovery-token-ca-cert-hash sha256:&amp;lt;HASH&amp;gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$   --node-name k8s-worker-gcp-0X    &lt;span style=&#34;color:#75715e&#34;&gt;# Replace X with 1, 2, 3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On cp-01:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl label node k8s-worker-gcp-01 cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;gcp tenant&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant-b
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl label node k8s-worker-gcp-02 cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;gcp tenant&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant-b
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl label node k8s-worker-gcp-03 cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;gcp tenant&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;tenant-b
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;--node-labels&lt;/code&gt; is not a &lt;code&gt;kubeadm join&lt;/code&gt; flag (despite what some docs suggest). Labels are applied with &lt;code&gt;kubectl label node&lt;/code&gt; after join.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Cilium DaemonSets auto-deploy to every new node. Node exporters appear automatically. The WireGuard mesh expands - each new node generates a keypair, registers its public key as a Kubernetes annotation, and every other node adds it as a peer.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;step-12-full-validation&#34;&gt;Step 12: Full Validation&lt;/h2&gt;
&lt;p&gt;After all 6 workers join, let&amp;rsquo;s verify the complete platform:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ── Nodes ──&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl get nodes -o wide
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Expected: 8 nodes, all Ready&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ── Cilium DaemonSets ──&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ cilium status | grep -E &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Desired|Ready|Available&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Expected: Desired: 8, Ready: 8/8, Available: 8/8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ── WireGuard peers ──&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo wg show cilium_wg0 | grep -c &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;latest handshake&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Expected: 7 (one per other node)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ── Tenant labels ──&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl get nodes -l cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;aws --no-headers | wc -l  &lt;span style=&#34;color:#75715e&#34;&gt;# → 3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl get nodes -l cloud&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;gcp --no-headers | wc -l  &lt;span style=&#34;color:#75715e&#34;&gt;# → 3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ── Observability ──&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl -n monitoring get pods  &lt;span style=&#34;color:#75715e&#34;&gt;# all Running&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl -n kube-system get pods | grep hubble  &lt;span style=&#34;color:#75715e&#34;&gt;# relay + UI Running&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My final cluster:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ kubectl get nodes -o wide
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;NAME                STATUS   ROLES           AGE     VERSION   INTERNAL-IP
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-cp-02           Ready    control-plane   131m    v1.36.3   198.251.78.107
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-worker-aws-01   Ready    &amp;lt;none&amp;gt;          79m     v1.36.3   &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-worker-aws-02   Ready    &amp;lt;none&amp;gt;          61m     v1.36.3   &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-worker-aws-03   Ready    &amp;lt;none&amp;gt;          61m     v1.36.3   &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-worker-gcp-01   Ready    &amp;lt;none&amp;gt;          38m     v1.36.3   &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-worker-gcp-02   Ready    &amp;lt;none&amp;gt;          57m     v1.36.3   &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;k8s-worker-gcp-03   Ready    &amp;lt;none&amp;gt;          58m     v1.36.3   &amp;lt;none&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ubuntu              Ready    control-plane   4h33m   v1.36.3   74.208.24.234
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;&amp;lt;none&amp;gt;&lt;/code&gt; INTERNAL-IP on cloud workers is expected - their public IP is NAT-based, not on a local interface. The Kubernetes Node object doesn&amp;rsquo;t need it. Cilium manages its own node registry with the correct public IPs for WireGuard endpoints.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ sudo wg show cilium_wg0 | grep -c &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;latest handshake&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Seven peers, all with recent handshakes and active data transfer. The control plane on Fasthosts, the AWS workers in us-east-1, and the GCP workers in us-east1-b - all part of one encrypted mesh.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-we-built&#34;&gt;What We Built&lt;/h2&gt;
&lt;p&gt;A single Kubernetes cluster spanning three independent infrastructure providers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HA control plane&lt;/strong&gt; on Fasthosts VPS with etcd quorum across two nodes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 AWS workers&lt;/strong&gt; tagged for Tenant A, 3 GCP workers tagged for Tenant B&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cilium eBPF&lt;/strong&gt; replacing kube-proxy for all service routing and load balancing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WireGuard&lt;/strong&gt; encrypting every packet between every node - no plain-text traffic on the public internet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prometheus + Grafana&lt;/strong&gt; monitoring all 8 nodes with per-node exporters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hubble&lt;/strong&gt; providing real-time eBPF flow visualization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every design decision was intentional. The control plane lives on commodity VPS. The data plane is isolated per cloud provider - when we deploy databases in Post 2, Tenant A&amp;rsquo;s replication traffic never leaves AWS, Tenant B&amp;rsquo;s never leaves GCP. The only cross-cloud traffic is lightweight control-plane communication, and WireGuard ensures even that is encrypted.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;In the next post, I&amp;rsquo;ll write a custom Kubernetes operator in Go that manages Valkey database clusters. It&amp;rsquo;ll use those &lt;code&gt;cloud=aws&lt;/code&gt; and &lt;code&gt;cloud=gcp&lt;/code&gt; node labels for topology-aware scheduling, deploy Sentinel-based high availability, and handle master election and failover automatically. The Prometheus/Grafana stack from this post will show us exactly what&amp;rsquo;s happening when things go wrong - and I&amp;rsquo;ll make them go wrong on purpose.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
