Egress gateway for a stable source IP
Route outbound traffic from chosen pods through one node so an IP-allowlisted third party always sees the same source address. Some APIs only accept requests from an address you registered with them ahead of time, and by default your pods have no stable outbound address: a pod that reschedules leaves through its new node's IP, and a cloud node changes its IP when it is replaced. A Cilium egress gateway pins the path, so the far end always sees one node's address.
flowchart LR P1["Pod (app: my-app)"] --> GW["Gateway node"] P2["Pod (app: my-app)"] --> GW GW -->|source IP 203.0.113.10| API["Allowlisted API"]
Cilium's egress gateway ships enabled on every cluster, so you configure it rather than install it.
Note
This is for traffic your cluster initiates outbound. If the far end connects to you instead, skip the egress gateway: expose the service with a type: LoadBalancer Service and allowlist its IP, which is a stable public address on cloud as well as bare metal. Use the egress gateway only when your pods open the outbound connection and the remote allowlists your source address.
Set up the gateway#
Label a node through its worker pool #
Pick a node to carry the egress traffic and label it. A CiliumEgressGatewayPolicy selects the gateway by that label.
Set the label through a worker pool in the Cluster object, not with kubectl label. A label set manually is lost the next time the node is replaced. A label on the pool's metadata is reapplied to every node the pool creates.
spec:
topology:
workers:
machineDeployments:
- class: workeramd64baremetal
name: md-egress
replicas: 1
metadata:
labels:
node-role.kubernetes.io/egress-gateway: "true"
See Label nodes and assign roles for how pool labels flow to nodes.
Warning
Use a bare-metal node as the egress gateway when the allowlist must hold for a long time. A cloud server receives a different public IP after a node replacement or upgrade, which silently breaks an allowlist entry. A Hetzner bare-metal server retains its IP across reinstalls. It also carries far more traffic: 1 Gbit/s with unlimited traffic, upgradeable to 10 Gbit/s, against a cloud server's roughly 300 to 500 Mbit/s.
Write the egress policy #
The policy names the pods whose traffic to redirect, the destinations it covers, and the gateway node. Read the node's external IP and put it in egressIP:
$ kubectl get nodes -l node-role.kubernetes.io/egress-gateway=true \
-o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}'
apiVersion: cilium.io/v2
kind: CiliumEgressGatewayPolicy
metadata:
name: force-egress-via-node
spec:
selectors:
- podSelector:
matchLabels:
app: my-app
destinationCIDRs:
- 0.0.0.0/0
egressGateway:
nodeSelector:
matchLabels:
node-role.kubernetes.io/egress-gateway: "true"
egressIP: 203.0.113.10
This routes all outbound traffic from pods labeled app: my-app through the labeled node. Narrow destinationCIDRs to the allowlisted third party's ranges if you do not want every outbound connection to be redirected. For more options, see the Cilium egress gateway documentation.
Verify the source address #
Run a test pod that carries the selector label but is kept off the gateway node, and check the address the internet sees:
$ kubectl run egress-test --image=curlimages/curl:8.5.0 \
--labels app=my-app --restart=Never -- sleep 3600
$ kubectl exec egress-test -- curl -s ifconfig.me
203.0.113.10
If the address is the gateway node's IP, the policy works.
Failover and network policy#
Warning
One gateway node is a single point of failure. All traffic the policy redirects leaves through it, so if the node goes down that traffic has no path out. There is no built-in round-robin across egress nodes.
For high availability, run your own egress proxies alongside or instead of the egress gateway. Choose several egress nodes, run a proxy on each, and place a Service in front of those proxies. Your backend sends its outbound traffic to that Service, and the Service round-robins across the egress-proxy nodes. Each node retains its own public IP, so the far end allowlists all of them. This is the alternative to a CiliumEgressGatewayPolicy, and you can mix the two.
flowchart LR B["Backend pods"] --> S["Service<br/>(egress proxies)"] S --> P1["Egress proxy<br/>node A"] S --> P2["Egress proxy<br/>node B"] S --> P3["Egress proxy<br/>node C"] P1 -->|source IP .10| API["Allowlisted API"] P2 -->|source IP .11| API P3 -->|source IP .12| API
A network policy deny applies first. If a NetworkPolicy blocks the pod's egress, the traffic never reaches the gateway, so allow the destination in policy before you expect the egress gateway to carry it. See Segment with network policies .
The egress gateway is one piece of how the platform handles traffic without a private network. See Networking for the wider picture.
Where to go next#
Pass TLS through at Layer 4
Terminate TLS inside your pod instead of at the ingress by passing raw TCP or TLS straight through the load balancer.
Configure pod and service subnets
Set the pod and service CIDRs before you create a cluster, because they are fixed at creation and cannot change afterward.