Skip to main content

Egress gateway for a stable source IP

Inspect 1.36

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.

cluster.yamlyaml
		spec:
  topology:
    workers:
      machineDeployments:
        - class: workeramd64baremetal
          name: md-egress
          replicas: 1
          metadata:
            labels:
              node-role.kubernetes.io/egress-gateway: "true"
	

See 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}'
	
egress-gateway-policy.yamlyaml
		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 .

The egress gateway is one piece of how the platform handles traffic without a private network. See for the wider picture.

Where to go next#