<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Wireguard on Serhii Chechun - Developer&#39;s Blog</title>
    <link>https://blog.chechun.org/tags/wireguard/</link>
    <description>Recent content in Wireguard on Serhii Chechun - Developer&#39;s Blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <copyright>© 2026 Serhii Chechun</copyright>
    <lastBuildDate>Sat, 25 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.chechun.org/tags/wireguard/index.xml" rel="self" type="application/rss+xml" />
    <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>
