|
1 | 1 | --- |
2 | 2 | title: Egress IP |
3 | | -linktitle: Egress IP |
4 | | -description: Egress IP demo |
5 | | -tags: ['Egress','v4.19'] |
| 3 | +linktitle: Egress IP |
| 4 | +description: Configure egress IP addresses so traffic from selected namespaces leaves the cluster with a fixed, predictable source IP. |
| 5 | +tags: ['Egress','v4.20'] |
6 | 6 | --- |
7 | | -# Some information |
| 7 | +# Egress IP |
8 | 8 |
|
9 | | -Official documentation: <https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/ovn-kubernetes_network_plugin/configuring-egress-ips-ovn> |
| 9 | +This example shows how to configure **egress IP addresses** on OpenShift so that traffic from selected namespaces uses a fixed source IP. That makes it easy to whitelist cluster workloads in firewalls or identify traffic in logs (e.g., from a specific app or team). |
10 | 10 |
|
11 | | -Tested with: |
| 11 | +**Official documentation:** [Chapter 9. Configuring an egress IP address](https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html/ovn-kubernetes_network_plugin/configuring-egress-ips-ovn) |
12 | 12 |
|
13 | | -|Component|Version| |
14 | | -|---|---| |
15 | | -|OpenShift|v4.19.14| |
| 13 | +**Tested with:** |
16 | 14 |
|
17 | | -## Prepare cluster |
| 15 | +| Component | Version | |
| 16 | +|-----------|---------| |
| 17 | +| OpenShift | v4.20.1 | |
| 18 | + |
| 19 | +## Overview |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | +Two OpenShift clusters are used: |
| 24 | + |
| 25 | +### Cluster: ocp6 |
| 26 | + |
| 27 | +**Source cluster** — here we configure egress IPs and run a pod that sends HTTP requests (via `curl`) to the web server on **ocp7**. |
| 28 | + |
| 29 | +=== "Worker Nodes" |
| 30 | + |
| 31 | + | NAME | IP | |
| 32 | + |-----------------|--------------| |
| 33 | + | ocp6-worker-0 | 10.32.105.95 | |
| 34 | + | ocp6-worker-1 | 10.32.105.96 | |
| 35 | + | ocp6-worker-2 | 10.32.105.97 | |
| 36 | + |
| 37 | +=== "oc output" |
| 38 | + |
| 39 | + ```shell |
| 40 | + % oc get nodes -l node-role.kubernetes.io/worker -o custom-columns='NAME:.metadata.name,IP:.status.addresses[0].address' |
| 41 | + NAME IP |
| 42 | + ocp6-worker-0 10.32.105.95 |
| 43 | + ocp6-worker-1 10.32.105.96 |
| 44 | + ocp6-worker-2 10.32.105.97 |
| 45 | + ``` |
| 46 | + |
| 47 | +### Cluster: ocp7 |
| 48 | + |
| 49 | +**Target cluster** — hosts a simple web server so we can see the **sender IP** in the server logs and verify egress IP behavior. |
| 50 | + |
| 51 | +=== "Worker Nodes" |
| 52 | + |
| 53 | + | NAME | IP | |
| 54 | + |-----------------|---------------| |
| 55 | + | ocp7-worker-0 | 10.32.105.103 | |
| 56 | + | ocp7-worker-1 | 10.32.105.104 | |
| 57 | + | ocp7-worker-2 | 10.32.105.105 | |
| 58 | + |
| 59 | +=== "oc output" |
| 60 | + |
| 61 | + ```shell |
| 62 | + % oc get nodes -l node-role.kubernetes.io/worker -o custom-columns='NAME:.metadata.name,IP:.status.addresses[0].address' |
| 63 | + NAME IP |
| 64 | + ocp7-worker-0 10.32.105.103 |
| 65 | + ocp7-worker-1 10.32.105.104 |
| 66 | + ocp7-worker-2 10.32.105.105 |
| 67 | + ``` |
| 68 | + |
| 69 | +## Deploy simple-nginx on ocp7 |
| 70 | + |
| 71 | +Deploy the target web server on **ocp7** (target cluster): |
18 | 72 |
|
19 | 73 | ```shell |
20 | | -% oc get nodes -l node-role.kubernetes.io/worker |
21 | | -NAME STATUS ROLES AGE VERSION |
22 | | -ocp1-worker-0 Ready worker 79d v1.32.5 |
23 | | -ocp1-worker-1 Ready worker 79d v1.32.5 |
24 | | -ocp1-worker-2 Ready worker 79d v1.32.5 |
25 | | - |
26 | | -% oc label node/ocp1-worker-0 k8s.ovn.org/egress-assignable="" |
27 | | -node/ocp1-worker-0 labeled |
28 | | -% oc label node/ocp1-worker-1 k8s.ovn.org/egress-assignable="" |
29 | | -node/ocp1-worker-1 labeled |
30 | | -% oc label node/ocp1-worker-2 k8s.ovn.org/egress-assignable="" |
31 | | -node/ocp1-worker-2 labeled |
32 | | - |
33 | | -oc apply -f - <<EOF |
34 | | -heredoc> apiVersion: k8s.ovn.org/v1 |
35 | | -kind: EgressIP |
36 | | -metadata: |
37 | | - name: egress-coe |
38 | | -spec: |
39 | | - egressIPs: |
40 | | - - 10.32.105.72 |
41 | | - - 10.32.105.73 |
42 | | - namespaceSelector: |
43 | | - matchLabels: |
44 | | - egress: coe |
45 | | -heredoc> EOF |
46 | | -egressip.k8s.ovn.org/egress-coe created |
| 74 | +oc new-project egress-target |
| 75 | +oc apply -k 'git@github.com:openshift-examples/kustomize/components/simple-nginx?ref=2026-02-12' |
47 | 76 | ``` |
48 | 77 |
|
49 | | -## Deployment |
| 78 | +Get the route hostname: |
50 | 79 |
|
51 | 80 | ```shell |
52 | | -oc new-project rbohne-egress |
53 | | -oc deploy -k simple-nginx... |
54 | | -oc rsh deployment/simple-nginx |
55 | | -curl $WEBSERVER |
56 | | -
|
57 | | -oc label namespace/rbohne-egress egress=coe |
| 81 | +% oc get route simple-nginx -o jsonpath='{.spec.host}' |
| 82 | +simple-nginx-egress-target.apps.ocp7.stormshift.coe.muc.redhat.com |
58 | 83 | ``` |
59 | 84 |
|
60 | | -```log |
61 | | -10.32.96.44 - - [31/Aug/2025:11:54:00 +0200] "GET / HTTP/1.1" 301 247 "-" "curl/7.76.1" |
62 | | -10.32.105.72 - - [31/Aug/2025:11:56:31 +0200] "GET / HTTP/1.1" 301 247 "-" "curl/7.76.1" |
| 85 | +Call the web server from your machine (or any client): |
| 86 | + |
| 87 | +```shell |
| 88 | +% curl -I https://simple-nginx-egress-target.apps.ocp7.stormshift.coe.muc.redhat.com |
| 89 | + |
| 90 | +HTTP/1.1 200 OK |
| 91 | +server: nginx/1.26.3 |
| 92 | +date: Thu, 12 Feb 2026 15:19:17 GMT |
| 93 | +content-type: text/html |
| 94 | +content-length: 45 |
| 95 | +last-modified: Thu, 12 Feb 2026 15:17:31 GMT |
| 96 | +etag: "698def0b-2d" |
| 97 | +accept-ranges: bytes |
| 98 | +set-cookie: 5386f97f58871214bc079b41bd88c1cd=43357b3d3312239cb10b42b1c3d17138; path=/; HttpOnly; Secure; SameSite=None |
| 99 | +cache-control: private |
63 | 100 | ``` |
64 | 101 |
|
65 | | -## Check IP on the node |
| 102 | +Check the pod logs to see the client IP that nginx recorded: |
66 | 103 |
|
67 | 104 | ```shell |
68 | | -sh-5.1# ip -br a show dev br-ex |
69 | | -br-ex UNKNOWN 10.32.105.69/20 169.254.0.2/17 10.32.105.72/32 |
70 | | -sh-5.1# |
| 105 | +% oc logs -f deployment/simple-nginx | grep curl |
| 106 | +10.130.0.2 - - [12/Feb/2026:15:26:45 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.12.1" "10.32.96.62" |
71 | 107 | ``` |
| 108 | + |
| 109 | +- `10.130.0.2` — cluster-internal IP of the ingress/router pod |
| 110 | +- `10.32.96.62` — **real client IP** (your machine or the pod making the request) |
| 111 | + |
| 112 | +## Configure egress IP on ocp6 |
| 113 | + |
| 114 | +1. **Label worker nodes** so they can be assigned egress IPs: |
| 115 | + |
| 116 | + ```shell |
| 117 | + % oc label node -l node-role.kubernetes.io/worker k8s.ovn.org/egress-assignable="" |
| 118 | + node/ocp6-worker-0 labeled |
| 119 | + node/ocp6-worker-1 labeled |
| 120 | + node/ocp6-worker-2 labeled |
| 121 | + ``` |
| 122 | + |
| 123 | +2. **Create an `EgressIP` object** with the IPs to use and which namespaces (via label) will use them. Use IPs that are valid on your cluster’s egress network and not in use by nodes or other services: |
| 124 | + |
| 125 | + ```shell |
| 126 | + oc apply -f - <<EOF |
| 127 | + apiVersion: k8s.ovn.org/v1 |
| 128 | + kind: EgressIP |
| 129 | + metadata: |
| 130 | + name: egress-coe |
| 131 | + spec: |
| 132 | + egressIPs: |
| 133 | + - 10.32.105.72 |
| 134 | + - 10.32.105.73 |
| 135 | + namespaceSelector: |
| 136 | + matchLabels: |
| 137 | + egress: coe |
| 138 | + EOF |
| 139 | + ``` |
| 140 | +
|
| 141 | +3. **Deploy a curl pod** in a new project — first **without** the egress label so traffic uses the node IP: |
| 142 | +
|
| 143 | + ```shell |
| 144 | + oc new-project curl |
| 145 | + oc apply -k 'git@github.com:openshift-examples/kustomize/components/rhel-support-tools?rev=2026-02-12' |
| 146 | + ``` |
| 147 | +
|
| 148 | + From inside the pod, run `curl` against the ocp7 route. The pod is on `ocp6-worker-2` (node IP `10.32.105.97`): |
| 149 | +
|
| 150 | + ```shell |
| 151 | + % oc rsh deployment.apps/rhel-support-tools |
| 152 | + sh-5.1$ echo $NODE_NAME $NODE_IP |
| 153 | + ocp6-worker-2 10.32.105.97 |
| 154 | + sh-5.1$ curl -Ik https://simple-nginx-egress-target.apps.ocp7.stormshift.coe.muc.redhat.com |
| 155 | + HTTP/1.1 200 OK |
| 156 | + server: nginx/1.26.3 |
| 157 | + date: Thu, 12 Feb 2026 15:42:30 GMT |
| 158 | + content-type: text/html |
| 159 | + content-length: 45 |
| 160 | + last-modified: Thu, 12 Feb 2026 15:31:00 GMT |
| 161 | + etag: "698df234-2d" |
| 162 | + accept-ranges: bytes |
| 163 | + set-cookie: 5386f97f58871214bc079b41bd88c1cd=7fbe3d3d6235cc5431656b0f57361872; path=/; HttpOnly; Secure; SameSite=None |
| 164 | + cache-control: private |
| 165 | +
|
| 166 | + sh-5.1$ |
| 167 | + ``` |
| 168 | +
|
| 169 | + On **ocp7**, check the `simple-nginx` logs. The client IP should be the **node IP** (`10.32.105.97`), because the namespace does not use egress IP yet: |
| 170 | +
|
| 171 | + ```shell |
| 172 | + 10.130.0.2 - - [12/Feb/2026:15:42:30 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.76.1" "10.32.105.97" |
| 173 | + ``` |
| 174 | +
|
| 175 | +  |
| 176 | +
|
| 177 | +4. **Enable egress IP** for the `curl` namespace by adding the label that matches the `EgressIP` `namespaceSelector`: |
| 178 | +
|
| 179 | + ```shell |
| 180 | + oc label namespace/curl egress=coe |
| 181 | + ``` |
| 182 | +
|
| 183 | + Run `curl` again from the same pod (still on the same node): |
| 184 | +
|
| 185 | + ```shell |
| 186 | + % oc rsh deployment.apps/rhel-support-tools |
| 187 | + sh-5.1$ echo $NODE_NAME $NODE_IP |
| 188 | + ocp6-worker-2 10.32.105.97 |
| 189 | + sh-5.1$ curl -Ik https://simple-nginx-egress-target.apps.ocp7.stormshift.coe.muc.redhat.com |
| 190 | + HTTP/1.1 200 OK |
| 191 | + server: nginx/1.26.3 |
| 192 | + date: Thu, 12 Feb 2026 15:48:38 GMT |
| 193 | + content-type: text/html |
| 194 | + content-length: 45 |
| 195 | + last-modified: Thu, 12 Feb 2026 15:31:00 GMT |
| 196 | + etag: "698df234-2d" |
| 197 | + accept-ranges: bytes |
| 198 | + set-cookie: 5386f97f58871214bc079b41bd88c1cd=7fbe3d3d6235cc5431656b0f57361872; path=/; HttpOnly; Secure; SameSite=None |
| 199 | + cache-control: private |
| 200 | +
|
| 201 | + sh-5.1$ |
| 202 | + ``` |
| 203 | +
|
| 204 | + On **ocp7**, check the `simple-nginx` logs again. The client IP should now be **10.32.105.72** — one of the egress IPs — instead of the node IP: |
| 205 | +
|
| 206 | + ```shell |
| 207 | + 10.130.0.2 - - [12/Feb/2026:15:48:38 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.76.1" "10.32.105.72" |
| 208 | + ``` |
| 209 | +
|
| 210 | + **Source IP is now the egress IP.** |
| 211 | +
|
| 212 | +  |
0 commit comments