Proxy (X-Forwarded-For) mode
This guide walks through setting up Envoy Gateway in a cluster where you
front the OpenStack load balancer with your own upstream proxy — for
example a CDN, WAF, or edge proxy — that terminates the client connection
and injects the real client IP into the X-Forwarded-For header. The
OpenStack load balancer itself stays in TCP passthrough; the upstream
proxy is what carries the client IP for you.
Note: Your
ClientTrafficPolicymust setclientIPDetection.xForwardedForwithnumTrustedHopsset to the number of trusted proxies in front of Envoy. Without it Envoy will not honour the incomingX-Forwarded-Forheader and your access logs and rate limiting will see the load balancer’s internal IP.
If you are not sure which variant applies to your cluster, see the Envoy Gateway overview.
Prerequisites
- A namespace you can use for your application. The examples below use
team-a. - A DNS record pointing at the load balancer’s public IP. In the examples all references to
team-a.example.commust be replaced by your own domain. - cert-manager installed in the cluster.
Create a namespace
kubectl create namespace team-a
Create a Gateway
Gateway describes the listeners. Put it in your own namespace and
reference the cluster GatewayClass named eg.
Create a file called gateway.yaml with the following content:
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: team-a
namespace: team-a
spec:
gatewayClassName: eg
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: Same
- name: https
port: 443
protocol: HTTPS
hostname: "*.team-a.example.com"
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: team-a-tls
allowedRoutes.from: Same keeps route visibility inside your namespace.
Set it to Selector or All if you route from elsewhere.
Apply it:
kubectl apply -f gateway.yaml
Configure X-Forwarded-For with ClientTrafficPolicy
The ClientTrafficPolicy attaches to the Gateway by name and tells Envoy
how many trusted proxies sit in front of it.
Create a file called client-traffic-policy.yaml:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: team-a
namespace: team-a
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: team-a
clientIPDetection:
xForwardedFor:
numTrustedHops: 1
numTrustedHops tells Envoy how many trusted ingress proxy hops sit in
front of it. Set it to the number of upstream proxies that prepend
entries to X-Forwarded-For. For a single CDN/WAF/edge proxy in front
of the load balancer, 1 is the right value; raise it for chains of
multiple proxies.
The setting affects what Envoy itself treats as the client IP — used in
access logs, rate limiting, and the x-envoy-external-address header.
Backends always see the full X-Forwarded-For chain that arrived
plus the load balancer’s internal IP appended on the right; Envoy does
not trim entries before forwarding the request upstream. Backend code
that needs the real client IP should parse the chain itself, typically
taking the leftmost public IP.
Note: The policy must live in the same namespace as the
Gateway. Envoy Gateway rejects cross-namespace policy targets.
Apply it:
kubectl apply -f client-traffic-policy.yaml
Alternative: using a custom header
Some load balancer setups forward the client IP in a different header.
Use customHeader instead — it is mutually exclusive with xForwardedFor:
clientIPDetection:
customHeader:
name: X-Real-IP
Issue a TLS certificate
We use cert-manager with HTTP-01 validation through the same Gateway.
Create a file called issuer.yaml:
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt
namespace: team-a
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: platform@team-a.example.com
privateKeySecretRef:
name: letsencrypt-account
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: team-a
namespace: team-a
kind: Gateway
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: team-a-tls
namespace: team-a
spec:
secretName: team-a-tls
issuerRef:
name: letsencrypt
kind: Issuer
dnsNames:
- echo.team-a.example.com
- web.team-a.example.com
Replace the email and DNS names with your own. Then apply it:
kubectl apply -f issuer.yaml
cert-manager creates a short-lived HTTPRoute on your Gateway to solve
the HTTP-01 challenge, then removes it. Prefer DNS-01 for wildcard certs.
Route traffic to your app
Create a file called route.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: echo
namespace: team-a
spec:
parentRefs:
- name: team-a
sectionName: https
hostnames:
- echo.team-a.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: echo
port: 80
Apply it:
kubectl apply -f route.yaml
Verify
Check that the Gateway got an external address and that traffic flows:
kubectl -n team-a get gateway team-a -o jsonpath='{.status.addresses[0].value}'
curl -v https://echo.team-a.example.com/
The backend should see the real client IP at the left of
X-Forwarded-For. Envoy forwards the full chain to the backend
(including the load balancer IP it appends on the right) — it does not
remove entries — so the backend application is responsible for parsing
the chain and picking the leftmost public IP.
Common mistakes
- No upstream proxy in front — this variant assumes a CDN, WAF, or other proxy injects
X-Forwarded-Forbefore traffic reaches the load balancer. Without one, no real client IP arrives, and your backend will only see the LB’s internal IP. If you have no upstream proxy, use direct (PROXY-protocol) mode instead. - Forgetting
ClientTrafficPolicy— Envoy ignores the incomingX-Forwarded-Forand treats the load balancer’s internal IP as the client. Rate limiting and access logs see the LB, not your real client. - Putting
ClientTrafficPolicyin another namespace — silently ignored. Must be colocated with theGateway. - Wrong
numTrustedHops— too low and a caller can spoof the client IP by adding their ownX-Forwarded-Forentry. Too high and Envoy walks too far back into spoofable territory. Count one per trusted upstream proxy. - Mixing
xForwardedForandcustomHeader— they are mutually exclusive. Pick one.
Advanced usage
For more advanced use cases please refer to the documentation provided by each project or contact our support: