<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Shubham Katara - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Shubham Katara - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 27 Aug 2026 22:36:37 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/katara/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Kubernetes Networking Without Kubernetes: Do What the CNI Does By Hand ]]>
                </title>
                <description>
                    <![CDATA[ In this article, you'll build an accurate mental model of what a Container Network Interface (CNI) actually does. Not by reading YAML, but by doing every single step it does by hand with raw Linux ker ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-kubernetes-networking-without-kubernetes-do-what-the-cni-does-by-hand/</link>
                <guid isPermaLink="false">6a614f114250f422f9a7be1d</guid>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ networking ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cni ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Shubham Katara ]]>
                </dc:creator>
                <pubDate>Wed, 22 Jul 2026 23:15:29 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8fc3683f-15c8-45ff-8424-bb1b427f68e2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this article, you'll build an accurate mental model of what a Container Network Interface (CNI) actually does. Not by reading YAML, but by doing every single step it does by hand with raw Linux kernel primitives.</p>
<p>The Container Network Interface (CNI) is one of the great black boxes of Kubernetes. Most people who run clusters every day have never once looked inside it. They know the <em>name</em> of their CNI ("we run Calico," "we're on Cilium") the way you know the brand of the alternator in your car: as a label, not as a thing you actually understand.</p>
<p>It lives at the very bottom of the stack, beneath the kubelet, beneath your pods, quietly moving every single packet. And precisely because it never fails loudly on a good day, almost nobody learns what it does.</p>
<p>We won't run <code>helm install cilium</code>. We won't apply a single manifest. Instead, we'll wire up pod networking from scratch, feel exactly where it breaks the moment traffic tries to leave a physical machine, and fix it manually.</p>
<p>By the end, you'll understand it in your bones, not just in theory, why tools like Cilium exist and what they're really solving under the hood.</p>
<p><strong>Who this is for:</strong></p>
<ul>
<li><p>Developers, platform engineers, and SREs who use Kubernetes every day but quietly treat pod-to-pod networking as magic.</p>
</li>
<li><p>Anyone who has ever watched a pod flip to <code>Running</code> and assumed the network "just works" and wants to know what's actually happening.</p>
</li>
</ul>
<p><strong>What you'll build with your own hands:</strong></p>
<ul>
<li><p>Two isolated network namespaces wired together with a virtual cable (<code>veth</code> pair).</p>
</li>
<li><p>A three-namespace virtual switch using a Linux bridge, the same trick legacy CNIs use on a single node.</p>
</li>
<li><p>A deliberately broken two-node setup where a packet gets dropped on the floor, plus the manual fix that makes it work.</p>
</li>
<li><p>A clear picture of the three jobs every CNI does, and why cloud providers force advanced CNIs like Cilium to use overlays and eBPF.</p>
</li>
</ul>
<p><strong>Note:</strong> Every command here needs a Linux host and <code>root</code>. Run this in a throwaway VM or lab environment, not on anything you care about. The whole point is to make a mess and learn from it.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-illusion-kubernetes-routes-zero-packets">The Illusion: Kubernetes Routes Zero Packets</a></p>
</li>
<li><p><a href="#heading-the-foundation-virtual-ethernet-veth-pairs">The Foundation: Virtual Ethernet (veth) Pairs</a></p>
</li>
<li><p><a href="#heading-how-to-scale-locally-with-a-linux-bridge">How to Scale Locally with a Linux Bridge</a></p>
</li>
<li><p><a href="#heading-the-multi-node-boundary-problem">The Multi-Node Boundary Problem</a></p>
</li>
<li><p><a href="#heading-how-to-fix-it-manually-with-direct-routing">How to Fix It Manually with Direct Routing</a></p>
</li>
<li><p><a href="#heading-so-what-is-a-cni-really">So What Is a CNI, Really?</a></p>
</li>
<li><p><a href="#heading-the-cloud-catch-and-why-cilium-changes-the-game">The Cloud Catch and Why Cilium Changes the Game</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before following along, you'll need:</p>
<ul>
<li><p>Two Linux VMs on the same network for the multi-node section so you can watch traffic cross a real machine boundary.</p>
</li>
<li><p>The <code>ip</code> command from the <code>iproute2</code> package (already installed on virtually every modern distro).</p>
</li>
<li><p>A basic comfort with IP addresses, subnets, and the word "gateway." You don't need to be a network engineer.</p>
</li>
<li><p><strong>No Kubernetes.</strong> That's not a typo. We're going underneath Kubernetes on purpose.</p>
</li>
</ul>
<h2 id="heading-the-illusion-kubernetes-routes-zero-packets">The Illusion: Kubernetes Routes Zero Packets</h2>
<p>Here's the uncomfortable truth most people never confront: <strong>Kubernetes can't route a single network packet.</strong></p>
<p>Not one. Kubernetes is a orchestrator. It schedules pods, watches their health, and updates state in etcd. But when it comes to actually moving a packet from one container to another, it has zero built-in capability. None.</p>
<p>So how do your pods talk to each other? They rely completely on an external agent to wire up the virtual network plumbing on every node. That agent is the <strong>Container Network Interface (CNI)</strong>. What the CNI does under the hood quietly, is what we would do ourselves to feel the pain and then the solution a CNI provides.</p>
<p>Here's the proof that it's load-bearing. Spin up a brand-new cluster with <code>kubeadm</code> and look at your nodes:</p>
<pre><code class="language-bash">$ kubectl get nodes
NAME       STATUS     ROLES                  AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION                        CONTAINER-RUNTIME
no-cni     NotReady   control-plane,master   52s   v1.35.0   192.168.117.2   &lt;none&gt;        Ubuntu 24.04.3 LTS   7.0.11-orbstack-00360-gc9bc4d96ac70   containerd://2.1.6
worker-1   NotReady   &lt;none&gt;                 46s   v1.35.0   192.168.117.3   &lt;none&gt;        Ubuntu 24.04.3 LTS   7.0.11-orbstack-00360-gc9bc4d96ac70   containerd://2.1.6
worker-2   NotReady   &lt;none&gt;                 40s   v1.35.0   192.168.117.4   &lt;none&gt;        Ubuntu 24.04.3 LTS   7.0.11-orbstack-00360-gc9bc4d96ac70   containerd://2.1.6
</code></pre>
<p><code>NotReady</code>. Every node. The control plane is healthy, etcd is up, the scheduler is alive, and the cluster still flatly refuses to be <code>Ready</code>. If you describe the node, it tells you precisely what's missing:</p>
<pre><code class="language-plaintext">Conditions:
  Type             Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----             ------  -----------------                 ------------------                ------                       -------
  Ready            False   Sat, 18 Jul 2026 10:25:51 +0200   Sat, 18 Jul 2026 10:25:20 +0200   KubeletNotReady              container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
</code></pre>
<p>Read that again: <strong>your cluster is not</strong> <code>Ready</code> <strong>until you install a CNI.</strong> Not "mostly ready." Not "ready except for networking." <code>NotReady</code>, full stop. Until an external plugin shows up and takes responsibility for the packets Kubernetes itself refuses to touch.</p>
<p>A cluster without a CNI is a telephone exchange with no lines plugged in: every operator is present and ready, but not a single call is able to connect.</p>
<p>So what do most people do at this exact moment? They copy one line from a getting-started page:</p>
<pre><code class="language-bash">kubectl apply -f https://.../calico.yaml
</code></pre>
<p>They watch the nodes flip to <code>Ready</code>, and they move on. That's the entire relationship most engineers have with the single component that makes their cluster work. They wing it. It works, so they never ask what "it" is.</p>
<p>This matters because "the network just works" is a dangerous story to tell yourself. The moment something breaks (a pod can't reach a service, cross-node traffic vanishes, a cloud migration mysteriously blackholes packets), you're standing in front of a system you never actually understood. So let's understand it. From the bottom up.</p>
<h2 id="heading-the-foundation-virtual-ethernet-veth-pairs">The Foundation: Virtual Ethernet (veth) Pairs</h2>
<p>To understand container networking, you first have to understand how Linux isolates it.</p>
<p>When a container (or a Kubernetes pod) is created, the kernel wraps it in an isolated <strong>Network Namespace</strong> (<code>netns</code>). Think of a fresh network namespace as an island with no bridges to the mainland. By default it's completely blind to the outside world: no interfaces, no IP addresses, and no routing tables. It can't talk to anything, and nothing can talk to it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63d79ac66a29d3450a1f08f7/9acf8795-df14-49e3-ad68-900308ae51a0.png" alt="A new network namespace is an island: disconnected from everything." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<p>So how do we get off the island? With a kernel primitive called a <strong>Virtual Ethernet (</strong><code>veth</code><strong>) pair</strong>.</p>
<p>A <code>veth</code> pair is a virtual network cable. Whatever packet enters one end immediately pops out the other end, even if the two ends live in different namespaces. Plug one end into the island and the other end into the mainland, and suddenly you have a connection.</p>
<p>Let's wire two isolated namespaces, <code>red</code> and <code>blue</code>, directly together.</p>
<pre><code class="language-bash"># Step 1: Create the isolated network namespaces
sudo ip netns add red
sudo ip netns add blue

# Step 2: Create the virtual ethernet cable (veth pair)
sudo ip link add veth-red type veth peer name veth-blue

# Step 3: Move each end of the cable into its namespace
sudo ip link set veth-red netns red
sudo ip link set veth-blue netns blue

# Step 4: Assign IP addresses and bring the interfaces UP
sudo ip netns exec red ip addr add 10.0.0.1/24 dev veth-red
sudo ip netns exec red ip link set veth-red up

sudo ip netns exec blue ip addr add 10.0.0.2/24 dev veth-blue
sudo ip netns exec blue ip link set veth-blue up
</code></pre>
<p>Now test the connection by pinging <code>blue</code> from inside <code>red</code>:</p>
<pre><code class="language-bash">sudo ip netns exec red ping -c 2 10.0.0.2
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/63d79ac66a29d3450a1f08f7/33c0c06b-60a2-4ad5-b2ec-2d6acf08b922.png" alt="A new network namespace is an island: now connected with mainland using veth pairs." style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<p>In the end, it would look something like this:</p>
<ul>
<li><p>Isolation broken safely: The container transitions from an unreachable, isolated namespace (no IP, no routing) to an addressable endpoint (10.1.1.2) linked directly to the host network.</p>
</li>
<li><p>Bi-directional traffic flow: Packets originating inside the container can reach external public IP networks, and incoming response packets from the internet can traverse back through the host's eth0 interface (192.168.1.10) directly into the container's veth pairs.</p>
</li>
<li><p>Zero-latency in-kernel bridging: The veth pair (veth-island &lt;--&gt; veth-mainland) acts as a direct virtual pipe, allowing instant packet transit between distinct Linux network namespaces (netns) without requiring external physical hardware .</p>
</li>
</ul>
<p><strong>The verdict:</strong> the ping succeeds. You just manually wired two isolated environments together with nothing but a virtual cable.</p>
<p>But here's the problem with this approach: it scales horribly. It does not scale well because a <code>veth</code> pair is strictly point to point.</p>
<p>Following is the number of pairs need to be configured for the number of containers:</p>
<ul>
<li><p>2 containers: 1 pair</p>
</li>
<li><p>3 containers: 3 pairs</p>
</li>
<li><p>4 containers: 6 cables</p>
</li>
</ul>
<p>For ten, you'd need 45 cables to connect every pair.</p>
<ul>
<li><p>Explodes in cable count: The number of veth pairs grow quadratically as containers increase</p>
</li>
<li><p>Complex to manage: Too many interfaces, routes and rules to configure and maintain.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/63d79ac66a29d3450a1f08f7/c20651dd-8932-46bb-8e1b-b99846f56854.png" alt="Image illustrating the problem with single veth pairs at scale" style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<p>This is the same reason data centers don't run a physical cable between every pair of servers. You need a switch.</p>
<h2 id="heading-how-to-scale-locally-with-a-linux-bridge">How to Scale Locally with a Linux Bridge</h2>
<p>When you need to connect more than two interfaces on a single host, you stop running cables between everything and plug everything into a central hub instead. In the Linux kernel, that hub is a <strong>Linux Bridge</strong>. It's a software Layer 2 virtual switch (you'll often see it named <code>br0</code> or <code>cni0</code>).</p>
<p>A bridge does exactly what a physical switch does: it learns MAC addresses and forwards frames across every connected interface in the same broadcast domain.</p>
<p>The pattern changes slightly. Instead of connecting namespaces directly to each other, you attach one end of a <code>veth</code> pair to the namespace, and plug the <em>other</em> end into the host's bridge.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63d79ac66a29d3450a1f08f7/c70ac344-0c3b-46ef-8e6f-f81bc52224df.png" alt="Image showing how veth pairs connect islands to mainland via Linux bridge" style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<p>Let's tear down the old setup and build a three-namespace switch: <code>red</code>, <code>blue</code>, and <code>green</code>. They'll all share one broadcast domain.</p>
<pre><code class="language-bash"># Clean up any previous configuration
sudo ip netns del red 2&gt;/dev/null || true
sudo ip netns del blue 2&gt;/dev/null || true
sudo ip netns del green 2&gt;/dev/null || true
sudo ip link del br0 2&gt;/dev/null || true

# Step 1: Create the host switch (bridge) and bring it up
sudo ip link add br0 type bridge
sudo ip link set br0 up

# Step 2: Wire namespace 1 (red) into the bridge
sudo ip netns add red
sudo ip link add veth-red type veth peer name veth-red-host
sudo ip link set veth-red netns red
sudo ip link set veth-red-host master br0
sudo ip link set veth-red-host up
sudo ip netns exec red ip addr add 10.0.0.1/24 dev veth-red
sudo ip netns exec red ip link set veth-red up

# Step 3: Wire namespace 2 (blue) into the bridge
sudo ip netns add blue
sudo ip link add veth-blue type veth peer name veth-blue-host
sudo ip link set veth-blue netns blue
sudo ip link set veth-blue-host master br0
sudo ip link set veth-blue-host up
sudo ip netns exec blue ip addr add 10.0.0.2/24 dev veth-blue
sudo ip netns exec blue ip link set veth-blue up

# Step 4: Wire namespace 3 (green) into the bridge
sudo ip netns add green
sudo ip link add veth-green type veth peer name veth-green-host
sudo ip link set veth-green netns green
sudo ip link set veth-green-host master br0
sudo ip link set veth-green-host up
sudo ip netns exec green ip addr add 10.0.0.3/24 dev veth-green
sudo ip netns exec green ip link set veth-green up
</code></pre>
<p>Because all three namespaces are connected to the shared <code>br0</code> device, they can communicate freely with each other across the virtual network switch. But how do they actually "find" each other on the network? This is where ARP comes in.</p>
<p><strong>ARP</strong> stands for Address Resolution Protocol. It's a fundamental part of local networking. When one computer (or namespace, in our case) wants to talk to another using an IP address, it needs to discover the other computer's hardware address (called a MAC address) to actually send packets on the network.</p>
<p>ARP is the system that allows this to happen — it sends out a broadcast asking "Who has IP address X? Please tell me your MAC address," and the right system answers back.</p>
<p>Thanks to ARP, all the namespaces plugged into <code>br0</code> can learn each other's MAC addresses automatically and send packets directly within their shared network segment. Let's prove it by pinging every pair, both ways:</p>
<pre><code class="language-bash"># red reaches blue and green
sudo ip netns exec red ping -c 1 10.0.0.2
sudo ip netns exec red ping -c 1 10.0.0.3

# blue reaches red and green
sudo ip netns exec blue ping -c 1 10.0.0.1
sudo ip netns exec blue ping -c 1 10.0.0.3

# green reaches red and blue
sudo ip netns exec green ping -c 1 10.0.0.1
sudo ip netns exec green ping -c 1 10.0.0.2
</code></pre>
<p>All six pings succeed. And notice there isn't a single routing rule involved anywhere. Every namespace lives in the same <code>10.0.0.0/24</code> subnet on the same Layer 2 switch, so the kernel resolves the whole mesh with plain ARP.</p>
<p>This is <em>exactly</em> how legacy single-node CNIs (the old <code>kubenet</code>) operate. On one machine, it's clean and simple.</p>
<p>But Kubernetes is a distributed system designed to scale across thousands of physical machines. So here's the question that breaks everything: what happens when our namespaces need to leave the host?</p>
<h2 id="heading-the-multi-node-boundary-problem">The Multi-Node Boundary Problem</h2>
<p>Everything so far has lived on one machine. Kubernetes doesn't. So let's do the honest thing: stand up two real VMs and watch the single-node trick fall apart. Don't take my word for it: build this and watch the packet die.</p>
<p>Here's the setup:</p>
<ul>
<li><p><strong>VM 1</strong> (host IP <code>10.1.44.216</code>): home to the <code>red</code> namespace, pod subnet <code>10.0.1.0/24</code>.</p>
</li>
<li><p><strong>VM 2</strong> (host IP <code>10.1.44.178</code>): home to the <code>blue</code> namespace, pod subnet <code>10.0.2.0/24</code>.</p>
</li>
</ul>
<p>Two things to notice before we start. First, each node gets its <strong>own</strong> pod subnet (<code>10.0.1.0/24</code> on VM 1, <code>10.0.2.0/24</code> on VM 2) because if both nodes handed out <code>10.0.0.x</code> addresses, you'd get IP collisions the instant two pods landed on the same number.</p>
<p>Second, because the subnets now differ, each namespace needs a <strong>gateway</strong> to route through, and that gateway is its own host's bridge.</p>
<p><strong>Note:</strong> this is the <a href="http://cleanup-multinode.sh">cleanup-multinode.sh</a> script that should only be used in case you make any errors while setting up the cross node routes and veth pairs.</p>
<pre><code class="language-shell">#!/usr/bin/env bash
#
# cleanup-multinode.sh
# Tears down the manual multi-node CNI lab (bridge + namespaces + veth
# pairs + cross-node static routes) from "Build a Mental Model for
# Kubernetes CNI by Doing It Manually."
#
# Safe to run on BOTH VMs. Every step is idempotent: anything that was
# never created on this host is skipped instead of erroring out, so
# re-running it is harmless.
#
# Usage:  sudo ./cleanup-multinode.sh
#
set -u

if [[ $EUID -ne 0 ]]; then
  echo "This script needs root. Run:  sudo $0" &gt;&amp;2
  exit 1
fi

echo "==&gt; Deleting network namespaces (this also destroys their veth pairs)..."
ip netns del red  2&gt;/dev/null &amp;&amp; echo "    - removed netns 'red'"  || true
ip netns del blue 2&gt;/dev/null &amp;&amp; echo "    - removed netns 'blue'" || true

echo "==&gt; Removing any orphaned host-side veth interfaces..."
ip link del veth-red-host  2&gt;/dev/null &amp;&amp; echo "    - removed veth-red-host"  || true
ip link del veth-blue-host 2&gt;/dev/null &amp;&amp; echo "    - removed veth-blue-host" || true

echo "==&gt; Deleting the bridge..."
ip link del br0 2&gt;/dev/null &amp;&amp; echo "    - removed bridge 'br0'" || true

echo "==&gt; Removing cross-node static routes..."
ip route del 10.0.1.0/24 2&gt;/dev/null &amp;&amp; echo "    - removed route to 10.0.1.0/24" || true
ip route del 10.0.2.0/24 2&gt;/dev/null &amp;&amp; echo "    - removed route to 10.0.2.0/24" || true

echo "==&gt; Disabling IP forwarding (non-persistent; resets on reboot anyway)..."
sysctl -w net.ipv4.ip_forward=0 &gt;/dev/null

# --- Optional: undo the 'Common Gotchas' tweaks, ONLY if you applied them ---
# On a throwaway lab VM, leaving FORWARD at ACCEPT or rp_filter at 0 is
# usually harmless, so these are opt-in. Uncomment whatever you changed.
# sysctl -w net.ipv4.conf.all.rp_filter=1 &gt;/dev/null
# iptables -P FORWARD DROP

echo
echo "==&gt; Teardown complete. Verifying nothing is left behind:"
echo "--- namespaces (expect: no red/blue) ---"
out=$(ip netns list);                          echo "${out:-  (none)}"
echo "--- bridges (expect: no br0) ---"
out=$(ip -br link show type bridge 2&gt;/dev/null); echo "${out:-  (none)}"
echo "--- lab routes (expect: none) ---"
out=$(ip route | grep -E '10\.0\.[12]\.0/24'); echo "${out:-  (none)}"
</code></pre>
<p><strong>On VM 1 (</strong><code>10.1.44.216</code><strong>)</strong>, build the bridge, wire up <code>red</code>, and turn the host into a router:</p>
<pre><code class="language-bash"># Make the host a router so it can transit packets that aren't its own
sudo sysctl -w net.ipv4.ip_forward=1

# Build the bridge and give it a gateway IP for VM 1's pod subnet
sudo ip link add br0 type bridge
sudo ip addr add 10.0.1.254/24 dev br0
sudo ip link set br0 up

# Wire the red namespace into the bridge
sudo ip netns add red
sudo ip link add veth-red type veth peer name veth-red-host
sudo ip link set veth-red netns red
sudo ip link set veth-red-host master br0
sudo ip link set veth-red-host up
sudo ip netns exec red ip addr add 10.0.1.1/24 dev veth-red
sudo ip netns exec red ip link set veth-red up

# Point the namespace's default route at its bridge gateway
sudo ip netns exec red ip route add default via 10.0.1.254
</code></pre>
<p><strong>On VM 2 (</strong><code>10.1.44.178</code><strong>)</strong>, do the mirror image for <code>blue</code>:</p>
<pre><code class="language-bash">sudo sysctl -w net.ipv4.ip_forward=1

sudo ip link add br0 type bridge
sudo ip addr add 10.0.2.254/24 dev br0
sudo ip link set br0 up

sudo ip netns add blue
sudo ip link add veth-blue type veth peer name veth-blue-host
sudo ip link set veth-blue netns blue
sudo ip link set veth-blue-host master br0
sudo ip link set veth-blue-host up
sudo ip netns exec blue ip addr add 10.0.2.1/24 dev veth-blue
sudo ip netns exec blue ip link set veth-blue up

sudo ip netns exec blue ip route add default via 10.0.2.254
</code></pre>
<p>Both hosts are routers now. Both namespaces are wired up. Ping <code>blue</code> on VM 2 from <code>red</code> on VM 1:</p>
<pre><code class="language-bash"># On VM 1
sudo ip netns exec red ping -c 3 10.0.2.1
</code></pre>
<pre><code class="language-plaintext">PING 10.0.2.1 (10.0.2.1) 56(84) bytes of data.

--- 10.0.2.1 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2043ms
</code></pre>
<p><strong>100% packet loss.</strong> The packet is dropped on the floor, exactly as promised, but now you've seen it with your own eyes.</p>
<p>Here's the part worth proving to yourself: the packet really does leave VM 1. It just never arrives at VM 2. Run <code>tcpdump</code> on both boxes and ping again:</p>
<pre><code class="language-bash"># Detect your physical NIC once (enp1s0, ens3, eth0, ...)
NIC=$(ip route get 1.1.1.1 | grep -oP 'dev \K\S+')

# On VM 1: the echo requests march out the door
sudo tcpdump -ni "$NIC" icmp
IP 10.0.1.1 &gt; 10.0.2.1: ICMP echo request, id 5, seq 1, length 64
IP 10.0.1.1 &gt; 10.0.2.1: ICMP echo request, id 5, seq 2, length 64

# On VM 2: dead silence. Nothing ever shows up.
sudo tcpdump -ni "$NIC" icmp
(no output)
</code></pre>
<p>So where does it die? Follow the life and death of that packet:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63d79ac66a29d3450a1f08f7/c2cea829-08c0-4b1d-a449-376791f9d070.png" alt="Death of the packet cross nodes" style="display:block;margin:0 auto" width="1714" height="918" loading="lazy">

<p>To keep it even simpler, the flow is as follows:</p>
<pre><code class="language-bash">Red Pod (10.0.1.1)
      │
      ▼
eth0
      │
      ▼
veth-red
      │
      ▼
br0 (10.0.1.254)
      │
      ▼
VM 1 Routing Table
(No route for 10.0.2.0/24)
      │
      ▼
Default Route (0.0.0.0/0)
      │
      ▼
Physical NIC (eth0)
      │
      ▼
LAN Gateway (192.168.1.1)
      │
      ▼
❌ No route for 10.0.2.0/24
(Packet Dropped)
      │
      ▼
VM 2 Never Receives the Packet
</code></pre>
<p>That's the multi-node boundary problem in one sentence: <strong>your per-node scripts are completely blind to the rest of the cluster's topology.</strong> VM 1 built its island, VM 2 built its island, and neither has any idea the other exists.</p>
<h2 id="heading-how-to-fix-it-manually-with-direct-routing">How to Fix It Manually with Direct Routing</h2>
<p>The fix is almost insultingly small. VM 1 doesn't need a smarter network. It needs a <em>map</em>. We just have to tell each host one fact it's missing: "the other node's pod subnet lives behind the other node's physical IP." That's a single static route per side. Nothing gets rebuilt: the bridges, namespaces, and forwarding you set up a moment ago all stay exactly as they are.</p>
<p><strong>On VM 1 (</strong><code>10.1.44.216</code><strong>)</strong>, teach it where VM 2's pods live:</p>
<pre><code class="language-bash"># VM 2's pods (10.0.2.0/24) are reachable via VM 2's physical IP
sudo ip route add 10.0.2.0/24 via 10.1.44.178
</code></pre>
<p><strong>On VM 2 (</strong><code>10.1.44.178</code><strong>)</strong>, teach it the way back:</p>
<pre><code class="language-bash"># VM 1's pods (10.0.1.0/24) are reachable via VM 1's physical IP
sudo ip route add 10.0.1.0/24 via 10.1.44.216
</code></pre>
<p>That's it. Two lines. Re-run the exact same ping from <code>red</code> on VM 1:</p>
<pre><code class="language-bash">sudo ip netns exec red ping -c 3 10.0.2.1
</code></pre>
<pre><code class="language-plaintext">PING 10.0.2.1 (10.0.2.1) 56(84) bytes of data.
64 bytes from 10.0.2.1: icmp_seq=1 ttl=62 time=0.412 ms
64 bytes from 10.0.2.1: icmp_seq=2 ttl=62 time=0.388 ms
64 bytes from 10.0.2.1: icmp_seq=3 ttl=62 time=0.401 ms

--- 10.0.2.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss
</code></pre>
<p>It works. (See <code>ttl=62</code>? Your packet started at 64 and lost one hop on each host it was forwarded through, proof it crossed two routers to get there.) The packet now completes the full journey:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63d79ac66a29d3450a1f08f7/7547a7e5-68d8-4f06-b396-d4d8c8cce15b.png" alt="Packet Journey cross nodes between namespaces" style="display:block;margin:0 auto" width="1536" height="1024" loading="lazy">

<p>The simpler flow looks like:</p>
<pre><code class="language-bash">1. Pod (10.0.1.1)
        │
        ▼
2. veth-red
        │
        ▼
3. VM 1 br0 (10.0.1.254)
        │
        ▼
4. VM 1 Routing Table
   ✅ Static route:
   10.0.2.0/24 → 10.1.44.178
        │
        ▼
5. VM 1 Physical NIC
        │
        ▼
6. Direct Link
   VM 1 → VM 2
        │
        ▼
7. VM 2 Physical NIC
        │
        ▼
8. VM 2 Routing Table
   ✅ 10.0.2.0/24 is directly connected
        │
        ▼
9. VM 2 br0
        │
        ▼
10. Blue Pod (10.0.2.1)
        │
        ▼
✅ Reply follows the same path back
</code></pre>
<p>That one line changed everything. Instead of dumping the packet at your LAN's clueless gateway, VM 1 now hands it <strong>directly</strong> to VM 2, which knows exactly which local namespace owns <code>10.0.2.1</code>. The reply follows the mirror route home. You just hand-built cross-node pod networking.</p>
<p>Now sit with how painful that was. Two nodes took a stack of careful commands and a hand-written route on each side.</p>
<p>Imagine a thousand nodes, pods being created and destroyed every second, each one needing a fresh IP and a route on <em>every other node</em> in the cluster. Doing that by hand isn't just tedious. It's impossible.</p>
<h2 id="heading-so-what-is-a-cni-really">So What Is a CNI, Really?</h2>
<p>Everything you just did by hand (the namespaces, the <code>veth</code> pairs, the bridges, the IP assignment, the routes) is exactly what a Container Network Interface automates dynamically, at scale, the instant a pod is scheduled.</p>
<p>When you apply a pod manifest, the CNI plugin intercepts the lifecycle event and performs three core jobs:</p>
<ol>
<li><p><strong>Namespace and interface provisioning:</strong> It creates the network namespace, generates the <code>veth</code> pair, and attaches it to the bridge (or its own datapath), cleanly, every time, with no fat-fingered typos.</p>
</li>
<li><p><strong>IP Address Management (IPAM):</strong> It hands out unique, non-colliding subnets per node and leases an individual IP to every single container in the cluster. That "unique Pod CIDR per node" rule you set up manually? IPAM enforces it automatically.</p>
</li>
<li><p><strong>Cluster-wide route distribution:</strong> It programs the routing so every node knows how to reach pods on every other node: the static routes you wrote by hand, generated and pushed everywhere, kept in sync as nodes and pods come and go.</p>
</li>
</ol>
<p>That's the mental model. A CNI is the thing that does your dozen-command lab a thousand times a second and never makes a mistake.</p>
<h2 id="heading-the-cloud-catch-and-why-cilium-changes-the-game">The Cloud Catch and Why Cilium Changes the Game</h2>
<p>Here's the part that surprises people. The manual direct-routing approach we just built works flawlessly in a bare-metal lab. In a modern public cloud (AWS, GCP, Azure), <strong>it breaks completely.</strong></p>
<p>Why? Cloud providers don't let arbitrary IP addresses roam across their network fabric. Your <code>10.0.1.0/24</code> pod subnet means nothing to the VPC. Unless those IPs are explicitly registered through heavyweight cloud-controller API calls, the underlying network sees pod traffic as illegitimate and drops it: the exact failure from the boundary-problem section, except now the cloud itself is the thing saying "no."</p>
<p>This is where advanced CNIs like <strong>Cilium</strong> stop playing by the old rules. Instead of leaning on fragile Linux bridges and hand-written host routes, Cilium reaches for two much stronger mechanisms.</p>
<ul>
<li><p><strong>Overlay networks (VXLAN / Geneve).</strong> Cilium takes your raw pod packet and <em>encapsulates</em> it, wrapping it inside an ordinary UDP packet addressed from Node 1's physical IP to Node 2's physical IP. To the cloud provider, it looks like completely normal node-to-node host traffic, so it sails straight through every VPC restriction. Your pod's real addresses are hidden inside the envelope.</p>
</li>
<li><p><strong>eBPF kernel programmability.</strong> Traditional CNIs push every packet through the full Linux bridge path and hundreds of sequential <code>iptables</code> rules: slow, and slower as your cluster grows. Cilium replaces that entire pipeline by loading compiled eBPF programs directly into the kernel at the network-interface level. Packets get short-circuited from the pod's <code>veth</code> straight toward the physical NIC, giving you near line-rate performance and deep security visibility for free.</p>
</li>
</ul>
<p>Here's the whole progression in one table:</p>
<table>
<thead>
<tr>
<th>Concern</th>
<th>Direct veth cables</th>
<th>Bridge + static routes</th>
<th>Advanced CNI (Cilium)</th>
</tr>
</thead>
<tbody><tr>
<td>Connects 2 endpoints</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Scales past a handful of pods</td>
<td>No</td>
<td>On one node only</td>
<td>Yes, cluster-wide</td>
</tr>
<tr>
<td>Crosses node boundaries</td>
<td>No</td>
<td>Manual routes per node</td>
<td>Automatic</td>
</tr>
<tr>
<td>Survives cloud VPC rules</td>
<td>No</td>
<td>No</td>
<td>Yes (VXLAN/Geneve overlay)</td>
</tr>
<tr>
<td>IP allocation</td>
<td>You, by hand</td>
<td>You, by hand</td>
<td>Automatic IPAM</td>
</tr>
<tr>
<td>Performance path</td>
<td>Kernel</td>
<td>Bridge + iptables</td>
<td>eBPF, near line-rate</td>
</tr>
<tr>
<td>Who maintains it</td>
<td>You, forever</td>
<td>You, forever</td>
<td>The CNI, automatically</td>
</tr>
</tbody></table>
<p>Look at that last column, then look at the last row. That's the entire value proposition of a CNI in two cells.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You didn't read about Kubernetes networking. You built it, broke it, and fixed it. Here's the mental model you now carry:</p>
<ol>
<li><p><strong>Kubernetes routes zero packets.</strong> It fully delegates the network to a CNI, and that CNI is doing real, physical plumbing on every node.</p>
</li>
<li><p><strong>A</strong> <code>veth</code> <strong>pair is a virtual cable</strong>, and it's the atom of container networking: great for two endpoints, useless at scale.</p>
</li>
<li><p><strong>A Linux bridge is a virtual switch</strong> that connects many namespaces on one host with nothing but Layer 2 and ARP. That's a single-node CNI in a nutshell.</p>
</li>
<li><p><strong>The node boundary is where naïve networking dies.</strong> Different subnets and an unaware physical network mean cross-node packets get dropped until <em>you</em> teach every host how to route.</p>
</li>
<li><p><strong>Static routes plus IP forwarding fix it manually</strong>, and doing that by hand for two nodes shows you instantly why nobody does it for a thousand.</p>
</li>
<li><p><strong>A CNI automates three jobs:</strong> interface provisioning, IPAM, and cluster-wide route distribution.</p>
</li>
<li><p><strong>The cloud breaks direct routing</strong>, which is precisely why Cilium leans on VXLAN/Geneve overlays and eBPF instead of bridges and <code>iptables</code>.</p>
</li>
</ol>
<p>The next time a pod flips to <code>Running</code> and the network "just works," you'll know the truth: nothing just works. A CNI just did (silently, and at a scale you now truly respect) everything you just did by hand.</p>
<p>From here, the natural next step is to tear down these scripts, deploy Cilium into a real cluster, and watch eBPF orchestrate this entire topology automatically. Having felt the manual pain first, you'll actually appreciate the elegance.</p>
<p><em>If this helped you build a clearer picture of Kubernetes networking, come say hi:</em></p>
<ul>
<li><p><em>LinkedIn:</em> <a href="https://www.linkedin.com/in/shubhamkatara/"><em>linkedin.com/in/shubhamkatara</em></a></p>
</li>
<li><p><em>YouTube:</em> <a href="https://www.youtube.com/@kubesimplify"><em>@kubesimplify</em></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Hybrid Cloud Platform with Google Cloud Services and On-Premise Kubernetes Infrastructure ]]>
                </title>
                <description>
                    <![CDATA[ In this article, you'll learn how to design and build a secure, scalable hybrid cloud platform that connects your on‑premises Kubernetes infrastructure to Google Cloud Platform. This allows on‑prem ap ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-hybrid-cloud-platform-with-google-cloud-services-and-on-premise-k8s-infra/</link>
                <guid isPermaLink="false">6a18c124782587548340fa90</guid>
                
                    <category>
                        <![CDATA[ google cloud ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloud native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CNCF ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Hybrid Cloud ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Shubham Katara ]]>
                </dc:creator>
                <pubDate>Thu, 28 May 2026 22:26:44 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a86db163-f513-48bd-8194-18c6cb894615.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this article, you'll learn how to design and build a secure, scalable hybrid cloud platform that connects your on‑premises Kubernetes infrastructure to Google Cloud Platform. This allows on‑prem apps can consume cloud services (notably GPUs) without brittle long‑lived keys, manual credential management, or risky network patterns.</p>
<p>Who this is for:</p>
<ul>
<li><p>Platform engineers, SREs, and security-focused cloud architects who operate mixed on‑prem and cloud Kubernetes estates.</p>
</li>
<li><p>Teams that need scalable, auditable access from on‑prem workloads to GCP resources (especially GPU instances) while minimizing operational overhead and blast radius.</p>
</li>
</ul>
<p>What you’ll get from this guide:</p>
<ul>
<li><p>The motivation and economics behind a hybrid approach (why GPUs often push workloads to the cloud).</p>
</li>
<li><p>Common pitfalls with service account keys and how “accidental air gaps” occur in real environments.</p>
</li>
<li><p>A practical, end‑to‑end pattern that uses Workload Identity Federation to give on‑prem pods short‑lived, auditable access to GCP without embedding keys.</p>
</li>
</ul>
<p>What’s included:</p>
<ul>
<li><p>Conceptual explanations, security tradeoffs, and operational best practices.</p>
</li>
<li><p>Concrete examples and Kubernetes/Terraform artifacts (linked in the GitHub repo at the end of this article) so you can reproduce the setup in your environment.</p>
</li>
</ul>
<p>Read on for the theory, then follow the hands‑on sections to provision GCP resources, configure federation, enforce policies with CEL and Kyverno, and validate secure, scalable GPU access from your on‑prem Kubernetes clusters.</p>
<p><strong>Note:</strong> Kubernetes and Terraform artifacts are linked in the GitHub repo at the end of this article.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-hybrid-cloud-matters">Why Hybrid Cloud Matters</a></p>
</li>
<li><p><a href="#heading-the-economics-of-hybrid-gpus-changed-everything">The Economics of Hybrid: GPUs Changed Everything</a></p>
</li>
<li><p><a href="#heading-why-service-account-keys-fail-at-scale">Why Service Account Keys Fail at Scale</a></p>
</li>
<li><p><a href="#heading-how-the-accidental-air-gap-happens">How the Accidental Air Gap Happens</a></p>
</li>
<li><p><a href="#heading-how-workload-identity-federation-bridges-the-gap">How Workload Identity Federation Bridges the Gap</a></p>
</li>
<li><p><a href="#heading-how-kubernetes-identity-works">How Kubernetes Identity Works</a></p>
</li>
<li><p><a href="#heading-how-to-prepare-google-cloud-platform-resources">How to prepare Google Cloud Platform resources</a></p>
</li>
<li><p><a href="#heading-how-to-use-cel-for-fine-grained-access-control">How to Use CEL for Fine-Grained Access Control</a></p>
</li>
<li><p><a href="#heading-how-to-inject-credentials-automatically-with-kyverno">How to Inject Credentials Automatically with Kyverno</a></p>
</li>
<li><p><a href="#heading-how-to-grant-iam-permissions-to-federated-identities">How to Grant IAM Permissions to Federated Identities</a></p>
</li>
<li><p><a href="#heading-how-to-verify-the-setup">How to Verify the Setup</a></p>
</li>
<li><p><a href="#heading-how-to-connect-on-prem-apps-to-cloud-gpus">How to Connect On-Prem Apps to Cloud GPUs</a></p>
</li>
<li><p><a href="#heading-how-to-scale-gpu-access-with-cel-conditions">How to Scale GPU Access with CEL Conditions</a></p>
</li>
<li><p><a href="#heading-the-security-properties-compared">The Security Properties Compared</a></p>
</li>
<li><p><a href="#heading-the-complete-infrastructure-as-code-layout">The Complete Infrastructure as Code Layout</a></p>
</li>
<li><p><a href="#heading-how-to-run-a-proof-of-concept-with-vcluster">How to Run a Proof of Concept with vCluster</a></p>
</li>
<li><p><a href="#heading-common-issues-and-how-to-solve-them">Common Issues and How to Solve Them</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before following along, you'll need:</p>
<ul>
<li><p>A Kubernetes cluster that is <strong>not</strong> GKE (on-premises, bare-metal, or a virtual cluster)</p>
</li>
<li><p>A Google Cloud project with the following APIs enabled: IAM, Security Token Service (STS), and Workload Identity</p>
</li>
<li><p><a href="https://developer.hashicorp.com/terraform/install">Terraform</a> installed and configured</p>
</li>
<li><p><a href="https://kyverno.io/docs/installation/">Kyverno</a> installed in your cluster</p>
</li>
<li><p>Python 3 with <code>google-cloud-secret-manager</code> and <code>google-cloud-aiplatform</code> libraries (for the verification steps. Code available in the github repository.)</p>
</li>
<li><p><code>kubectl</code> access to your cluster</p>
</li>
</ul>
<h2 id="heading-why-hybrid-cloud-matters">Why Hybrid Cloud Matters</h2>
<p>If everything goes right, a hybrid cloud platform lets your on-premises and cloud workloads talk to each other as if they were part of the same network.</p>
<p>There are many practical reasons to run a hybrid cloud setup:</p>
<ul>
<li><p><strong>Offloading analytics to BigQuery:</strong> You keep your analytics apps on-prem for data sovereignty, but pipe large datasets into BigQuery for world-class processing power — without buying extra servers.</p>
</li>
<li><p><strong>Creating a unified network with Cloud Interconnect:</strong> Using Cloud Interconnect or Cloud VPN, your on-premises datacenter becomes an extension of the Google Cloud Platform (GCP) Virtual Private Cloud (VPC). Your on-prem invoice apps can talk to cloud-based user services with low latency and no public internet exposure.</p>
</li>
<li><p><strong>Cost-effective scalability via Cloud Storage:</strong> You can use cloud storage as a backend for local apps, storing logs, backups, and historical data while paying only for what you use.</p>
</li>
<li><p><strong>Event-driven syncing with Pub/Sub:</strong> When something happens on-prem, a message through Cloud Pub/Sub lets cloud services react instantly — no manual polling required.</p>
</li>
</ul>
<h2 id="heading-the-economics-of-hybrid-gpus-changed-everything">The Economics of Hybrid: GPUs Changed Everything</h2>
<p>Before diving into the technical problem, it's worth understanding why hybrid clouds matter more than ever.</p>
<p>Your organization, like most enterprises, has made significant investments in on-premises datacenters. Servers are bought. Racks are filled. Network infrastructure is paid for. The marginal cost of running one more workload is essentially zero.</p>
<p>Then came the AI wave.</p>
<p>Suddenly every team needs Graphics Processing Units (GPUs). Not one or two — dozens of A100s for training, fleets of inference endpoints, vector databases that need to sit close to the models. GPUs are scarce. Lead times for on-prem GPU hardware stretch into months. Cloud providers have them available in minutes.</p>
<p>The architecture that actually makes economic sense looks like this:</p>
<ul>
<li><p><strong>The on-prem datacenter handles the bulk of compute</strong> — web servers, business logic, databases, batch processing. This is commodity compute you've already paid for.</p>
</li>
<li><p><strong>The cloud handles what's scarce</strong> — GPU-accelerated inference, model training, AI/ML endpoints. You pay per request, scale on demand, and don't wait six months for hardware.</p>
</li>
</ul>
<p>The cloud isn't a full migration destination — it's an extension for capabilities you can't easily build on-prem.</p>
<p>But those on-prem workloads need to authenticate to cloud services. Every API call from the datacenter to a Vertex AI endpoint, every request to a GPU-powered inference service, every write to Cloud Storage for model artifacts — all of it needs credentials. That's the problem this article solves.</p>
<h2 id="heading-why-service-account-keys-fail-at-scale">Why Service Account Keys Fail at Scale</h2>
<p>Here's a scenario that plays out in thousands of enterprises daily.</p>
<p>A development team needs their on-prem application to write to Google Cloud Storage. The "obvious" solution? Generate a GCP service account key, base64 encode it, store it in a Kubernetes Secret, and mount it in the pod:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: gcp-credentials
type: Opaque
data:
  key.json: eyJ0eXBlIjoic2VydmljZV9hY2NvdW50IiwicHJvamVjdF9pZCI6…
</code></pre>
<p>This works. It also introduces serious problems:</p>
<ul>
<li><p><strong>Never expires.</strong> That key is valid until someone remembers to rotate it (they won't) or it gets compromised (it will).</p>
</li>
<li><p><strong>Can be exfiltrated trivially.</strong> Anyone with read access to that namespace can run <code>kubectl get secret -o yaml</code> and walk away with permanent GCP access.</p>
</li>
<li><p><strong>Has no audit trail for the actual workload.</strong> GCP sees "service-account-xyz accessed this bucket" — not "pod frontend-abc-123 in namespace production."</p>
</li>
<li><p><strong>Scales terribly.</strong> 50 teams × 3 environments × 4 GCP projects = 600 keys to track, rotate, and hope haven't been committed to git.</p>
</li>
</ul>
<p>Security teams know this. That's why many organizations have done the only sensible thing: they have disabled service account key generation entirely.</p>
<h2 id="heading-how-the-accidental-air-gap-happens">How the Accidental Air Gap Happens</h2>
<p>When you disable key generation, you haven't solved the hybrid cloud platform problem — you've just made it someone else's problem. That someone is usually a platform team staring at a Jira ticket that says "cannot access GCP from on-prem, P1, blocking release."</p>
<p>The result? Your "hybrid cloud platform" isn't hybrid at all. It's two disconnected systems.</p>
<p>Teams resort to building intermediary services, API gateways that proxy requests, or finding creative ways to get keys anyway. None of this is a platform. It's duct tape.</p>
<h2 id="heading-how-workload-identity-federation-bridges-the-gap">How Workload Identity Federation Bridges the Gap</h2>
<p>Every Kubernetes cluster already issues cryptographically signed identity tokens to every pod. And Google Cloud has a service specifically designed to trust those tokens.</p>
<p>This is <strong>Workload Identity Federation</strong> — and combined with OpenID Connect (OIDC), it's the missing piece that makes hybrid platforms actually work.</p>
<p>The service is quite well named because of the word Federation. it means GCP doesn't store your identity — it agrees to trust identities issued by another system, as long as they can be cryptographically verified. This all works with a very well orchestrated set of steps in the following order:</p>
<ol>
<li><p>Pod presents its Kubernetes-issued JWT to GCP's STS endpoint.</p>
</li>
<li><p>STS verifies the signature against your cluster's public JWKS.</p>
</li>
<li><p>STS checks the JWT's claims against the Workload Identity Pool's rules (audience, issuer, CEL conditions).</p>
</li>
<li><p>STS returns a short-lived Google access token (typically 1 hour) that the pod uses for API calls.</p>
</li>
</ol>
<p>It is also worth mentioning that Workload Identity Federation is not Kubernetes specific. It works with AWS IAM, Azure AD, GitHub Actions OIDC, and any OIDC-compliant identity provider.</p>
<h2 id="heading-how-kubernetes-identity-works">How Kubernetes Identity Works</h2>
<p>Every pod with a ServiceAccount gets a JSON Web Token (JWT) automatically mounted at <code>/run/secrets/kubernetes.io/serviceaccount/token</code>. This isn't just an opaque blob — it's a signed assertion of identity:</p>
<pre><code class="language-json">{
  "iss": "https://kubernetes.default.svc.cluster.local",
  "sub": "system:serviceaccount:production:backend-api",
  "aud": ["https://iam.googleapis.com/..."],
  "kubernetes.io": {
    "namespace": "production",
    "serviceaccount": {
      "name": "backend-api"
    }
  },
  "exp": 1735689600
}
</code></pre>
<p>In a JWT, claims are just the key-value pairs inside the token's payload — each one is a claim the issuer is making about the subject. Think of them as facts the token is asserting, signed cryptographically so the verifier can trust them.</p>
<p>The critical insight: this token is created by a set of JSON Web Key Set (JWKS) and is verifiable by anyone who has your cluster's public keys, exposed via the JSON Web Key Set (JWKS) endpoint:</p>
<pre><code class="language-bash">kubectl get --raw /openid/v1/jwks
</code></pre>
<p>Google Cloud's Security Token Service (STS) can validate these tokens. No keys are exchanged. No secrets are stored. Just cryptographic proof of identity.</p>
<h2 id="heading-how-to-prepare-google-cloud-platform-resources">How to Prepare Google Cloud Platform resources</h2>
<p>The Workload Identity Pool is a trust boundary — a declaration that says "I accept identities from external sources." The OIDC Provider configures how to validate those identities.</p>
<pre><code class="language-hcl">resource "google_iam_workload_identity_pool" "pool" {
  workload_identity_pool_id = "hybrid-platform-pool"
  project                   = "my-project"
}

resource "google_iam_workload_identity_pool_provider" "k8s_provider" {
  project                            = "my-project"
  workload_identity_pool_id          = google_iam_workload_identity_pool.pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "on-prem-cluster"

  attribute_mapping = {
    "google.subject"      = "assertion.sub"
    "attribute.namespace" = "assertion['kubernetes.io']['namespace']"
  }

  attribute_condition = "attribute.namespace in [\"production\", \"staging\"]"

  oidc {
    issuer_uri = "https://kubernetes.default.svc.cluster.local"
    jwks_json  = file("jwks.json")  # Your cluster's public keys
  }
}
</code></pre>
<p>Two things to note here:</p>
<ol>
<li><p><code>attribute_mapping</code> extracts claims from the Kubernetes JWT and makes them available as GCP attributes. By using `assertion['kubernetes.io']['namespace']`, the namespace is pulled out so you can use it for access control.</p>
</li>
<li><p><code>attribute_condition</code> is where security policy lives. More on this in the next section.</p>
</li>
</ol>
<h2 id="heading-how-to-use-cel-for-fine-grained-access-control">How to Use CEL for Fine-Grained Access Control</h2>
<p>The <code>attribute_condition</code> field uses Common Expression Language (CEL). This single line of policy can replace dozens of Identity and Access Management (IAM) bindings:</p>
<pre><code class="language-plaintext">attribute.namespace in ["production", "staging"]
</code></pre>
<p>With this condition, a pod in the <code>kube-system</code> namespace cannot authenticate to GCP at all — the token exchange is rejected before IAM is even consulted.</p>
<p>You can get more sophisticated:</p>
<pre><code class="language-plaintext">// Only production namespace, and only specific service accounts
attribute.namespace == "production" &amp;&amp;
  attribute.service_account in ["payment-processor", "order-service"]

// Allow staging, but only during business hours
attribute.namespace == "staging" &amp;&amp;
  request.time.getHours("America/New_York") &gt;= 9 &amp;&amp;
  request.time.getHours("America/New_York") &lt; 17
</code></pre>
<p>This is defense in depth. Even if someone creates a rogue ServiceAccount or has <code>kubectl</code> access, they cannot authenticate to GCP unless the CEL condition passes. The security boundary is enforced by Google's infrastructure, not by hoping developers follow policy.</p>
<h2 id="heading-how-to-inject-credentials-automatically-with-kyverno">How to Inject Credentials Automatically with Kyverno</h2>
<p>Having a working identity federation is only half the battle. Your customers and developers shouldn't need to understand OIDC, STS, or credential configuration files. They should deploy their app and have it work.</p>
<p>Before we get to the automation, it's worth pausing on what a <em>credential configuration file</em> actually is — because the name is a little misleading.</p>
<p>A credential configuration file (sometimes called an "external account config" or "ADC config") is a small JSON document that tells Google's client libraries <strong>how to obtain</strong> a credential at runtime. It is <strong>not</strong> itself a credential. You'll see the actual file later in this article — it contains no secrets. Just metadata: the Workload Identity Pool audience, the STS token-exchange endpoint, the source token type, and the path on the pod's filesystem where the real (short-lived) Kubernetes ServiceAccount token lives.</p>
<p>Compare that to a traditional service account key:</p>
<table>
<thead>
<tr>
<th></th>
<th>Service Account Key (<code>key.json</code>)</th>
<th>Credential Config (<code>credential-configuration.json</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>What's inside the file</td>
<td>An RSA private key that <em>is</em> the credential</td>
<td>Instructions for exchanging an external token</td>
</tr>
<tr>
<td>Lifetime of the secret material</td>
<td>Forever, until manually rotated</td>
<td>Source token rotates automatically (~1h TTL)</td>
</tr>
<tr>
<td>If the file leaks</td>
<td>Long-lived access to a GCP service account</td>
<td>Useless on its own — points to a token only the pod can read</td>
</tr>
<tr>
<td>Identity model</td>
<td>Impersonates a GCP service account directly</td>
<td>Federates an external identity into GCP via STS</td>
</tr>
<tr>
<td>Who handles rotation</td>
<td>A human (or no one)</td>
<td>The Kubernetes API server, transparently</td>
</tr>
</tbody></table>
<p>Both files end up referenced by <code>GOOGLE_APPLICATION_CREDENTIALS</code> and look interchangeable from the application's point of view — but only one of them is dangerous to lose. The credential config file is safe to ship in a ConfigMap precisely because there's nothing to steal.</p>
<p>Having this file in the ConfigMap is half the solution. It actually needs to end up in the workload pods that need access to GCP services. This is where Kyverno comes in. A single ClusterPolicy automatically injects everything a pod needs:</p>
<pre><code class="language-yaml">apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: workload-identity-federation
spec:
  rules:
    - name: inject-gcp-credentials
      match:
        any:
          - resources:
              kinds:
                - Deployment
              selector:
                matchLabels:
                  workload-identity-federation: "enabled"
      mutate:
        patchStrategicMerge:
          spec:
            template:
              spec:
                volumes:
                  - name: workload-identity-credential-configuration
                    configMap:
                      name: workload-identity-federation-config
                containers:
                  - (name): "*"
                    volumeMounts:
                      - name: workload-identity-credential-configuration
                        mountPath: /etc/workload-identity
                        readOnly: true
                    env:
                      - name: GOOGLE_APPLICATION_CREDENTIALS
                        value: "/etc/workload-identity/credential-configuration.json"
</code></pre>
<p>The above cluster policy does three things:</p>
<ol>
<li><p>Mounts the configmap inside the containers in the deployment at <code>/etc/workload-identity</code>.</p>
</li>
<li><p>Injects an environment variable called <code>GOOGLE_APPLICATION_CREDENTIALS</code> that points to the absolute path of the credential config file.</p>
</li>
</ol>
<p>From a developer's perspective, this is their entire integration:</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    workload-identity-federation: "enabled" # That's it.
spec:
  # ... normal deployment spec
</code></pre>
<p>The credential configuration file (created by Terraform as a ConfigMap) tells Google's client libraries how to exchange tokens:</p>
<pre><code class="language-json">{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "file": "/run/secrets/kubernetes.io/serviceaccount/token"
  }
}
</code></pre>
<p>This JSON file is a credential configuration for Google's Workload Identity Federation. It instructs Google Cloud client libraries to obtain cloud access tokens by exchanging a Kubernetes ServiceAccount token (located at <code>/run/secrets/kubernetes.io/serviceaccount/token</code>) for a Google Cloud access token, using an external identity provider configured via a Workload Identity Pool. This allows workloads running outside of GCP, such as on-premises Kubernetes clusters, to authenticate to Google Cloud services without needing to manage long-lived service account keys.</p>
<p>Every Google Cloud SDK and client library understands this format. Python, Go, Java, and Node.js all just work.</p>
<h2 id="heading-how-to-grant-iam-permissions-to-federated-identities">How to Grant IAM Permissions to Federated Identities</h2>
<p>The service account token that has been trusted by the STS service, also known as a federated identity, need permissions to access resources. You bind IAM roles to the identity pool attributes:</p>
<pre><code class="language-hcl">resource "google_project_iam_member" "secret_access" {
  for_each = toset(["production", "staging"])
  project  = "my-project"
  role     = "roles/secretmanager.secretAccessor"
  member   = "principalSet://iam.googleapis.com/projects/\({PROJECT_NUMBER}/locations/global/workloadIdentityPools/\){POOL_ID}/attribute.namespace/${each.value}"
}
</code></pre>
<p>This grants Secret Manager access to all pods authenticated from the <code>production</code> or <code>staging</code> namespaces. The <code>principalSet</code> syntax allows matching on attributes. You can also restrict to specific service accounts:</p>
<pre><code class="language-plaintext">member = "principal://iam.googleapis.com/.../subject/system:serviceaccount:production:payment-processor"
</code></pre>
<h2 id="heading-how-to-verify-the-setup">How to Verify the Setup</h2>
<p>You can verify the setup with a simple Python script that lists secrets from Secret Manager. This runs inside a pod on your on-premises cluster:</p>
<pre><code class="language-python"># list_secrets.py - running on-prem, accessing GCP Secret Manager
from google.cloud import secretmanager

def list_secrets(project_id: str):
    """
    List all secrets in a GCP project.

    No credentials are passed explicitly. The google-cloud-secret-manager
    library automatically:
    1. Reads GOOGLE_APPLICATION_CREDENTIALS env var (set by Kyverno)
    2. Loads the credential configuration JSON
    3. Reads the K8s ServiceAccount token from /run/secrets/...
    4. Exchanges it for a GCP access token via STS
    5. Uses that token to call the Secret Manager API
    """
    client = secretmanager.SecretManagerServiceClient()
    parent = f"projects/{project_id}"

    print(f"Secrets in {project_id}:")
    print("-" * 40)

    for secret in client.list_secrets(request={"parent": parent}):
        secret_name = secret.name.split("/")[-1]
        print(f"  - {secret_name}")

    print("-" * 40)
    print("Authentication: Workload Identity Federation")
    print("Credentials: None stored, token exchanged at runtime")

if __name__ == "__main__":
    list_secrets("my-project-id")
</code></pre>
<p>Run this inside your labeled pod:</p>
<pre><code class="language-bash">$ kubectl exec -it my-app-xyz -- python list_secrets.py

Secrets in my-project-id:
----------------------------------------
  - database-password
  - api-key-stripe
  - oauth-client-secret
  - ml-model-api-key
----------------------------------------
Authentication: Workload Identity Federation
Credentials: None stored, token exchanged at runtime
</code></pre>
<p>No service account key. No secret mounted. Just a Kubernetes ServiceAccount token exchanged for GCP credentials at runtime.</p>
<p>This same pattern works for any GCP service — Secret Manager, Cloud Storage, BigQuery, Pub/Sub, and Vertex AI.</p>
<h2 id="heading-how-to-connect-on-prem-apps-to-cloud-gpus">How to Connect On-Prem Apps to Cloud GPUs</h2>
<p>Consider a typical flow: an on-prem order processing service needs to call a Vertex AI endpoint for fraud detection. The model runs on GPUs in Google Cloud (you can spin up A100s in minutes, not months). The application logic stays on-prem (you've already paid for that compute).</p>
<p>With the IAM bindings in place, any pod in the allowed namespaces can call Vertex AI:</p>
<pre><code class="language-python"># fraud_detector.py - running on-prem, calling cloud GPUs
from google.cloud import aiplatform

def check_fraud(transaction: dict) -&gt; float:
    """
    Call a Vertex AI endpoint for fraud detection.

    The model runs on A100 GPUs in Google Cloud.
    This code runs on-prem in the datacenter.

    Authentication is automatic:
    1. Kyverno injected GOOGLE_APPLICATION_CREDENTIALS
    2. The aiplatform SDK reads the credential config
    3. K8s SA token is exchanged for GCP token via STS
    4. Request is authenticated to Vertex AI
    """
    endpoint = aiplatform.Endpoint(
        endpoint_name="projects/my-project/locations/us-central1/endpoints/fraud-model"
    )
    prediction = endpoint.predict(instances=[transaction])
    return prediction.predictions[0]["fraud_score"]


def generate_embeddings(texts: list[str]) -&gt; list[list[float]]:
    """
    Generate text embeddings using a cloud-hosted model.

    Embedding models are GPU-intensive. Running them on-prem
    would require dedicated hardware. In the cloud, you pay per request.
    """
    from vertexai.language_models import TextEmbeddingModel

    model = TextEmbeddingModel.from_pretrained("text-embedding-004")
    embeddings = model.get_embeddings(texts)
    return [e.values for e in embeddings]
</code></pre>
<p>The developer doesn't think about authentication at all. They add the label to their deployment, and their on-prem pod can call:</p>
<ul>
<li><p><strong>Vertex AI endpoints</strong> for ML inference on cloud GPUs</p>
</li>
<li><p><strong>Cloud Storage</strong> for model artifacts and training data</p>
</li>
<li><p><strong>BigQuery</strong> for feature stores and analytics</p>
</li>
<li><p><strong>Pub/Sub</strong> for event streaming between environments</p>
</li>
<li><p><strong>Secret Manager</strong> for API keys and configuration</p>
</li>
</ul>
<p>This is the hybrid platform working as intended.</p>
<h2 id="heading-how-to-scale-gpu-access-with-cel-conditions">How to Scale GPU Access with CEL Conditions</h2>
<p>CEL conditions become especially powerful when you want to restrict GPU access to specific namespaces. For example, to allow only ML-related namespaces to access Vertex AI:</p>
<pre><code class="language-plaintext">attribute.namespace in ["ml-inference", "ml-training", "data-science"] &amp;&amp;
  attribute.service_account.startsWith("ml-")
</code></pre>
<p>You can also grant different access levels per namespace:</p>
<pre><code class="language-hcl"># ML inference namespace gets prediction access
resource "google_project_iam_member" "ml_inference" {
  project = "my-project"
  role    = "roles/aiplatform.user"
  member  = "principalSet://iam.googleapis.com/.../attribute.namespace/ml-inference"
}

# Data science namespace gets full Vertex AI access (for experimentation)
resource "google_project_iam_member" "data_science" {
  project = "my-project"
  role    = "roles/aiplatform.admin"
  member  = "principalSet://iam.googleapis.com/.../attribute.namespace/data-science"
}
</code></pre>
<p>The on-prem application teams don't need to know or care about GCP IAM. They deploy to the right namespace, add a label, and the platform handles the rest.</p>
<h2 id="heading-the-security-properties-compared">The Security Properties Compared</h2>
<p>Here's a side-by-side comparison of the two authentication approaches:</p>
<table>
<thead>
<tr>
<th>Property</th>
<th>Service Account Keys</th>
<th>Workload Identity Federation</th>
</tr>
</thead>
<tbody><tr>
<td>Credential lifetime</td>
<td>Until manually rotated (often years)</td>
<td>Short-lived (1 hour for GCP tokens)</td>
</tr>
<tr>
<td>Exfiltration risk</td>
<td>High — static key can be copied anywhere</td>
<td>Low — token expires quickly</td>
</tr>
<tr>
<td>Audit trail</td>
<td>Service account name only</td>
<td>Namespace + service account name</td>
</tr>
<tr>
<td>Key management overhead</td>
<td>600+ keys at scale</td>
<td>Zero keys to manage</td>
</tr>
<tr>
<td>Security policy enforcement</td>
<td>Manual / trust-based</td>
<td>Enforced by GCP infrastructure via CEL</td>
</tr>
<tr>
<td>Developer experience</td>
<td>Copy key, create secret, mount volume</td>
<td>Add one label to the deployment</td>
</tr>
</tbody></table>
<p>The short-lived nature of tokens deserves emphasis. Even in a worst-case scenario where a token is somehow exfiltrated, it expires. Kubernetes ServiceAccount tokens have a configurable lifetime, and the GCP access tokens issued by STS are valid for one hour. A service account key, by contrast, remains valid until someone explicitly rotates it — often years.</p>
<h2 id="heading-the-complete-infrastructure-as-code-layout">The Complete Infrastructure as Code Layout</h2>
<p>The entire solution is codified in Terraform, managing both GCP and Kubernetes resources:</p>
<pre><code class="language-plaintext">workload-identity-federation/
├── providers.tf      # Google + Kubernetes providers
├── locals.tf         # Configuration (namespaces, project ID, etc.)
├── gcp.tf            # Identity pool, provider, IAM bindings
└── kubernetes.tf     # ConfigMap with credential configuration
</code></pre>
<p>A single <code>terraform apply</code>:</p>
<ol>
<li><p>Creates the Workload Identity Pool in GCP</p>
</li>
<li><p>Configures the OIDC provider with your cluster's JWKS</p>
</li>
<li><p>Sets up IAM bindings for allowed namespaces</p>
</li>
<li><p>Creates ConfigMaps in each namespace with the credential configuration</p>
</li>
</ol>
<p>Combined with the Kyverno policy, you get a fully automated pipeline:</p>
<pre><code class="language-plaintext">New namespace added to allowed list
        │
        ▼
Terraform creates ConfigMap in that namespace
        │
        ▼
Developer deploys with label
        │
        ▼
Kyverno injects credentials automatically
        │
        ▼
Pod authenticates to GCP via OIDC
        │
        ▼
Application accesses GCP services
</code></pre>
<p>No tickets. No key requests. No secrets to manage.</p>
<h2 id="heading-how-to-run-a-proof-of-concept-with-vcluster">How to Run a Proof of Concept with vCluster</h2>
<p>To validate this works outside GKE, you can set up a demonstration using <a href="https://www.vcluster.com/">vCluster</a> — a virtual Kubernetes cluster that runs inside another Kubernetes cluster. This proves the solution works for any cluster. You can setup vCluster in Docker using <a href="https://github.com/loft-sh/vind/blob/main/docs/getting-started.md">vind</a></p>
<pre><code class="language-yaml"># vcluster.yaml
experimental:
  docker:
    nodes:
      - name: worker-1
      - name: worker-2
deploy:
  cni:
    flannel:
      enabled: true
controlPlane:
  distro:
    k8s:
      version: "v1.35.0"
</code></pre>
<pre><code class="language-shell">[root@localhost #] vcluster create hybrid --driver docker -f vcluster.yaml
[root@localhost #] kubectl get nodes
hybrid-control-plane   Ready    control-plane   14d   v1.34.0   192.168.107.2   &lt;none&gt;        Debian GNU/Linux 12 (bookworm)   7.0.5-orbstack-00330-ge3df4e19b0a0-dirty   containerd://2.1.3
hybrid-worker          Ready    &lt;none&gt;          14d   v1.34.0   192.168.107.3   &lt;none&gt;        Debian GNU/Linux 12 (bookworm)   7.0.5-orbstack-00330-ge3df4e19b0a0-dirty   containerd://2.1.3
hybrid-worker2         Ready    &lt;none&gt;          14d   v1.34.0   192.168.107.4   &lt;none&gt;        Debian GNU/Linux 12 (bookworm)   7.0.5-orbstack-00330-ge3df4e19b0a0-dirty   containerd://2.1.3
</code></pre>
<p>Inside the vCluster, deploy a simple test deployment:</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: gcp-test
  labels:
    workload-identity-federation: "enabled"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gcp-test
  template:
    metadata:
      labels:
        app: gcp-test
    spec:
      containers:
        - name: test
          image: google/cloud-sdk:slim
          command: ["sleep", "infinity"]
</code></pre>
<p>Exec into the pod and verify:</p>
<pre><code class="language-bash">$ kubectl exec -it gcp-test-xxx -- bash

# Inside the pod:
\( gcloud auth login --cred-file=\)GOOGLE_APPLICATION_CREDENTIALS
Authenticated with external account credentials for: [principal://iam.googleapis.com/...]

$ gcloud secrets list --project=my-project
NAME                 CREATED
database-password    2024-01-15T10:30:00Z
api-key              2024-01-14T09:15:00Z
</code></pre>
<p>No keys. No secrets mounted. Just identity federation working as designed.</p>
<h2 id="heading-common-issues-and-how-to-solve-them">Common Issues and How to Solve Them</h2>
<h3 id="heading-how-to-handle-jwks-retrieval-for-air-gapped-clusters">How to Handle JWKS Retrieval for Air-Gapped Clusters</h3>
<p>If your cluster's OIDC discovery endpoint isn't publicly reachable (most on-prem clusters aren't), you need to manually export the JWKS and upload it to GCP:</p>
<pre><code class="language-bash">kubectl get --raw /openid/v1/jwks &gt; jwks.json
</code></pre>
<p>This file must be updated if the cluster's signing keys rotate. Set up a periodic job that checks for key changes and updates the Terraform configuration.</p>
<h3 id="heading-how-to-fix-issuer-url-mismatches">How to Fix Issuer URL Mismatches</h3>
<p>The <code>iss</code> claim in the Kubernetes token must exactly match the issuer URL configured in the OIDC provider. For clusters using internal DNS:</p>
<pre><code class="language-plaintext">issuer_uri = "https://kubernetes.default.svc.cluster.local"
</code></pre>
<p>This URL doesn't need to be reachable from GCP — the JWKS file provides the validation keys. But it must match what's in the token exactly.</p>
<h3 id="heading-how-to-debug-token-exchange-failures">How to Debug Token Exchange Failures</h3>
<p>When authentication fails, the error messages can be cryptic. Common causes and fixes:</p>
<table>
<thead>
<tr>
<th>Error</th>
<th>Likely Cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td><code>invalid_grant</code></td>
<td>Issuer URL mismatch</td>
<td>Check <code>iss</code> claim in JWT against configured <code>issuer_uri</code></td>
</tr>
<tr>
<td><code>audience mismatch</code></td>
<td>Wrong <code>audience</code> in credential config</td>
<td>Regenerate the credential configuration JSON via Terraform</td>
</tr>
<tr>
<td><code>CEL condition failed</code></td>
<td>Namespace not in allowed list</td>
<td>Add namespace to <code>attribute_condition</code> and re-apply</td>
</tr>
<tr>
<td><code>JWKS validation failed</code></td>
<td>Signing keys have rotated</td>
<td>Re-export JWKS and update Terraform config</td>
</tr>
</tbody></table>
<h2 id="heading-conclusion">Conclusion</h2>
<p>After implementing this setup, on-premises workloads authenticate to Google Cloud exactly like GKE workloads do — without a single long-lived credential. The security team is happy (no keys to audit), developers are happy (just add a label), and the platform team is happy (no more credential management tickets).</p>
<p>Here's what you accomplished in this tutorial:</p>
<ol>
<li><p>/Understood why service account keys fail at scale and the security risks they introduce</p>
</li>
<li><p>Created a Workload Identity Pool and OIDC provider in GCP to trust your cluster's token issuer</p>
</li>
<li><p>Used CEL conditions to enforce fine-grained, namespace-level access policies</p>
</li>
<li><p>Automated credential injection into pods using a Kyverno ClusterPolicy</p>
</li>
<li><p>Bound IAM roles to federated identity attributes — no long-lived keys anywhere</p>
</li>
<li><p>Verified the setup by calling GCP APIs (Secret Manager, Vertex AI) from an on-prem pod</p>
</li>
<li><p>Proved the solution works on any Kubernetes cluster using vCluster</p>
</li>
</ol>
<p>The technologies used here aren't new. OIDC has been in Kubernetes since version 1.20. Workload Identity Federation has been in GCP for years. Kyverno and Terraform are mature tools. What this tutorial puts together is an end-to-end solution that developers can adopt with minimal effort.</p>
<p>If your organization has disabled service account keys (or should), this is the path forward. Your on-prem and cloud clusters can finally be what they were always meant to be: secure extensions of each other.</p>
<p><em>The complete implementation is available as a Terraform module with Kyverno policies:</em> <a href="https://github.com/shkatara/hybrid-platform-gcp-workload-identity-federation"><em>github.com/shkatara/hybrid-platform-gcp-workload-identity-federation</em></a></p>
<p>If this helps, you can follow me on <a href="https://www.linkedin.com/in/shubhamkatara/">https://www.linkedin.com/in/shubhamkatara/</a>, <a href="https://www.youtube.com/@kubesimplify">https://www.youtube.com/@kubesimplify</a>, <a href="https://www.linkedin.com/company/kubesimplify/">https://www.linkedin.com/company/kubesimplify/</a> and</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
