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.
```mermaid
flowchart LR
P1["Pod (app: my-app)"]:::app --> GW["Gateway node"]:::platform
P2["Pod (app: my-app)"]:::app --> GW
GW -->|source IP 203.0.113.10| API["Allowlisted API"]:::external
```
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.
```yaml vars title="cluster.yaml"
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](/docs/hetzner/apalla/servers-and-nodes/pools/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`:
```console
$ kubectl get nodes -l node-role.kubernetes.io/egress-gateway=true \
-o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}'
```
```yaml title="egress-gateway-policy.yaml"
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](https://docs.cilium.io/en/stable/network/egress-gateway/egress-gateway/).
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:
```console
$ 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.
```mermaid
flowchart LR
B["Backend pods"]:::app --> S["Service
(egress proxies)"]:::app
S --> P1["Egress proxy
node A"]:::app
S --> P2["Egress proxy
node B"]:::app
S --> P3["Egress proxy
node C"]:::app
P1 -->|source IP .10| API["Allowlisted API"]:::external
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](/docs/hetzner/apalla/security/segment-with-network-policies).
The egress gateway is one piece of how the platform handles traffic without a private network. See [Networking](/docs/hetzner/apalla/concepts/internals/networking) for the wider picture.
## Where to go next
- [Label nodes and assign roles](/docs/hetzner/apalla/servers-and-nodes/pools/label-nodes-and-assign-roles)
- [Segment with network policies](/docs/hetzner/apalla/security/segment-with-network-policies)
- [Connect to on-prem or another VPC](/docs/hetzner/apalla/network/egress/connect-to-on-prem-or-vpc)