← blogEShome

Kubernetes · Networking · 2026-09-03 · 5 min read

HA egress on Cilium OSS without paying for Enterprise

An on-prem Kubernetes cluster (kubeadm on VMware) needed a fixed egress IP: third parties whitelist one address, and all outbound traffic to them must originate from it. Cilium's egress gateway does exactly that — it steers matching traffic through one gateway node, SNATed to the designated IP.

The problem is the word one. In the Cilium this cluster runs — 1.17 OSS — the egress gateway does no failover. We verified it in testing: with several candidate nodes labeled, the datapath picks one deterministically, and when that node dies, matching traffic is dropped until someone intervenes. Gateway HA with leader election ships in Isovalent's enterprise egress gateway; the open-source version leaves the seat empty. So your carefully whitelisted egress IP is now a single point of failure with a pager attached.

The design: float the IP, follow the leader

Two moving parts, each doing the thing it's good at:

kube-vip floats the egress IP itself. It runs on the candidate worker nodes, elects a leader through a Kubernetes lease, and the winner announces the VIP with gratuitous ARP. Node dies → lease expires → another node takes the IP and GARPs the network. This is battle-tested plumbing; the same mechanism many clusters already use for their control-plane VIP.

A small label-sync controller follows the lease. Cilium doesn't care where the VIP lives — it routes egress traffic to whichever node carries the gateway label. So the missing piece is embarrassingly specific: keep exactly one node labeled, and make it the node that currently holds the kube-vip lease. The controller watches the lease and patches node labels accordingly. That's the entire job description.

kube-vip alone doesn't solve it (the label would still point at the dead node) and relabeling alone doesn't solve it (the IP wouldn't move). Together they give OSS Cilium what Enterprise sells: egress that survives a node failure.

From PoC to something you can page people about

The proof of concept was a bash script running in a tmux session — it validated the approach and would have died with my SSH connection. The production version is a Go operator on controller-runtime:

The interesting part is the invariants, because label-flipping has sharp edges:

The cold-start scare

One observation from the PoC nearly killed the whole design: the first failover onto a node that had never held the egress IP took over 30 minutes to converge — the Cilium agent was slow to recognize the egress IP as local. If that reproduced, a real failover landing on a "virgin" node would mean a half-hour egress outage, and the design would be worthless for production.

This became the gating spike, ahead of all the packaging work: reproduce it with a genuinely clean node, or bury it. Result: it didn't reproduce — a virgin node converged in ~5 seconds, consistently. Whatever produced the 30-minute convergence was an artifact of the PoC environment, not a property of the design.

Two lessons folded into one: chase your single worst observation before building anything on top of the design it questions — and remember that a proof-of-concept environment is itself a prime suspect. We could have shipped pre-warming machinery (rotating the VIP across all candidates at setup) to mitigate a problem that didn't exist.

Failure modes, stated out loud

Writing these down explicitly was worth more than any of the code:

What diesWhat happens
One controller replicaThe other takes over in ≤15 s; egress never notices (state is already applied on the nodes)
Both controller replicasLabel freezes where it is; egress keeps working. Only a kube-vip failover during that window would go unfollowed
kube-vip loses all leadersThe VIP lives nowhere and egress degrades to the pre-HA state. The controller can't help — but the alert fires

Four Prometheus alerts cover the system: lease holder ≠ labeled node, zero-or-two labeled nodes, controller down, lease without holder. Each one maps to a row in that table.

Results

Failover measured end-to-end — from killing the gateway node to egress traffic leaving with the whitelisted source IP again, verified with packet capture on the new gateway:

node C → node B:  5 s
node B → node C:  4 s

For short-lived HTTP traffic, in-flight connections just retry and nobody notices. The VMware side needed its own validation (the port group must accept MAC changes / forged transmits for GARP-based IP migration to work), which an existing keepalived control-plane VIP had conveniently already proven. And the two planes stay isolated: the worst possible kube-vip failure returns egress to its old static-IP behavior — the API server VIP, owned by keepalived on the control-plane nodes, is never in the blast radius.

Takeaways

Ciliumkube-vipcontroller-runtimeegressKubernetes