Skip to content

Commit 83b2394

Browse files
committed
NGINX: Correctly determine client IP when using PROXY protocol and multiple proxies are involved
1 parent 01d0a20 commit 83b2394

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
lines changed

docs/user-guide/miscellaneous.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ If the ingress controller is running in AWS we need to use the VPC IPv4 CIDR.
1010

1111
Another option is to enable the **PROXY protocol** using [`use-proxy-protocol: "true"`](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#use-proxy-protocol).
1212

13-
In this mode NGINX does not use the content of the header to get the source IP address of the connection.
13+
In this mode, NGINX uses the PROXY protocol TCP header to retrieve the source IP address of the connection.
14+
15+
This works in most cases, but if you have a Layer 7 proxy (e.g., Cloudflare) in front of a TCP load balancer, it may not work correctly. The HTTP proxy IP address might appear as the client IP address. In this case, you should also enable the `use-forwarded-headers` setting in addition to enabling `use-proxy-protocol`, and properly configure `proxy-real-ip-cidr` to trust all intermediate proxies (both within the private network and any external proxies).
16+
17+
Example configmap for setups with multiple proxies:
18+
19+
```yaml
20+
use-proxy-protocol: "true"
21+
use-forwarded-headers: "true"
22+
proxy-real-ip-cidr: "10.0.0.0/8,131.0.72.0/22,172.64.0.0/13,104.24.0.0/14,104.16.0.0/13,162.158.0.0/15,198.41.128.0/17"
23+
```
24+
25+
**Note:** Be sure to use real CIDRs that match your exact environment.
1426
1527
## Path types
1628

docs/user-guide/nginx-configuration/configmap.md

+5
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ The following table shows a configuration option's name, type, and the default v
131131
| [use-forwarded-headers](#use-forwarded-headers) | bool | "false" | |
132132
| [enable-real-ip](#enable-real-ip) | bool | "false" | |
133133
| [forwarded-for-header](#forwarded-for-header) | string | "X-Forwarded-For" | |
134+
| [forwarded-for-proxy-protocol-header](#forwarded-for-proxy-protocol-header) | string | "X-Forwarded-For-Proxy-Protocol" | |
134135
| [compute-full-forwarded-for](#compute-full-forwarded-for) | bool | "false" | |
135136
| [proxy-add-original-uri-header](#proxy-add-original-uri-header) | bool | "false" | |
136137
| [generate-request-id](#generate-request-id) | bool | "true" | |
@@ -913,6 +914,10 @@ If false, NGINX ignores incoming `X-Forwarded-*` headers, filling them with the
913914

914915
Sets the header field for identifying the originating IP address of a client. _**default:**_ X-Forwarded-For
915916

917+
## forwarded-for-proxy-protocol-header
918+
919+
Sets the name of the intermediate header used to determine the client's originating IP when both `use-proxy-protocol` and `use-forwarded-headers` are enabled. This doesn't impact functionality and should not typically be modified. _**default:**_ X-Forwarded-For-Proxy-Protocol
920+
916921
## compute-full-forwarded-for
917922

918923
Append the remote address to the X-Forwarded-For header instead of replacing it. When this option is enabled, the upstream application is responsible for extracting the client IP based on its own list of trusted proxies.

internal/ingress/controller/config/config.go

+7
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,12 @@ type Configuration struct {
569569
// Default is X-Forwarded-For
570570
ForwardedForHeader string `json:"forwarded-for-header,omitempty"`
571571

572+
// Sets the name of the intermediate header used to determine the client's originating IP
573+
// when both use-proxy-protocol and use-forwarded-headers are enabled. This doesn't impact
574+
// functionality and should not typically be modified.
575+
// Default is X-Forwarded-For-Proxy-Protocol
576+
ForwardedForProxyProtocolHeader string `json:"forwarded-for-proxy-protocol-header,omitempty"`
577+
572578
// Append the remote address to the X-Forwarded-For header instead of replacing it
573579
// Default: false
574580
ComputeFullForwardedFor bool `json:"compute-full-forwarded-for,omitempty"`
@@ -780,6 +786,7 @@ func NewDefault() Configuration {
780786
UseForwardedHeaders: false,
781787
EnableRealIP: false,
782788
ForwardedForHeader: "X-Forwarded-For",
789+
ForwardedForProxyProtocolHeader: "X-Forwarded-For-Proxy-Protocol",
783790
ComputeFullForwardedFor: false,
784791
ProxyAddOriginalURIHeader: false,
785792
GenerateRequestID: true,

rootfs/etc/nginx/template/nginx.tmpl

+36-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ http {
7777
{{/* Enable the real_ip module only if we use either X-Forwarded headers or Proxy Protocol. */}}
7878
{{/* we use the value of the real IP for the geo_ip module */}}
7979
{{ if or (or $cfg.UseForwardedHeaders $cfg.UseProxyProtocol) $cfg.EnableRealIP }}
80-
{{ if $cfg.UseProxyProtocol }}
80+
{{ if and $cfg.UseForwardedHeaders $cfg.UseProxyProtocol }}
81+
real_ip_header {{ $cfg.ForwardedForProxyProtocolHeader }};
82+
{{ else if $cfg.UseProxyProtocol }}
8183
real_ip_header proxy_protocol;
8284
{{ else }}
8385
real_ip_header {{ $cfg.ForwardedForHeader }};
@@ -409,6 +411,39 @@ http {
409411

410412
{{ end }}
411413

414+
{{ if and $cfg.UseForwardedHeaders $cfg.UseProxyProtocol }}
415+
# When the proxy protocol is enabled, we cannot rely solely on the proxy protocol address
416+
# due to potential proxy chain issues. Multiple proxies may modify the client IP before
417+
# it reaches the ingress controller. We use the proxy-real-ip-cidr list to trust specific proxy
418+
# addresses and determine the correct client IP from the forwarded headers.
419+
#
420+
# -------- -------------- --------------------- -----------------
421+
# | User | --> | HTTP Proxy | -- http --> | TCP Load Balancer | -- proxy protocol --> | Ingress Nginx |
422+
# -------- -------------- --------------------- -----------------
423+
#
424+
# The algorithm for determining the header to be used with the real_ip_header:
425+
# 1. Check if the $proxy_protocol_addr is trusted (i.e., whether it's in the proxy-real-ip-cidr list).
426+
# 2. If trusted, use the configured forwarded-for header (X-Forwarded-For by default).
427+
# 3. If not trusted, fall back to using the $proxy_protocol_addr.
428+
#
429+
geo $proxy_protocol_addr $proxy_protocol_addr_trusted {
430+
default 0;
431+
{{ range $trusted_ip := $cfg.ProxyRealIPCIDR }}
432+
{{ $trusted_ip }} 1;
433+
{{ end }}
434+
}
435+
436+
map $proxy_protocol_addr_trusted $forwarded_for_proxy_protocol {
437+
default $proxy_protocol_addr;
438+
1 {{ buildForwardedFor $cfg.ForwardedForHeader }};
439+
}
440+
441+
# The realip module does not support variables for the real_ip_header directive
442+
# so we need to define a custom header.
443+
more_set_input_headers "{{ $cfg.ForwardedForProxyProtocolHeader }}: $forwarded_for_proxy_protocol";
444+
445+
{{ end }}
446+
412447
# Create a variable that contains the literal $ character.
413448
# This works because the geo module will not resolve variables.
414449
geo $literal_dollar {

test/e2e/settings/proxy_protocol.go

+83
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ var _ = framework.DescribeSetting("use-proxy-protocol", func() {
4444
f.NewEchoDeployment()
4545
f.UpdateNginxConfigMapData(setting, "false")
4646
})
47+
4748
//nolint:dupl // Ignore dupl errors for similar test case
4849
ginkgo.It("should respect port passed by the PROXY Protocol", func() {
4950
host := proxyProtocol
@@ -227,4 +228,86 @@ var _ = framework.DescribeSetting("use-proxy-protocol", func() {
227228
assert.Nil(ginkgo.GinkgoT(), err, "obtaining nginx logs")
228229
assert.Contains(ginkgo.GinkgoT(), logs, `192.168.0.1`)
229230
})
231+
232+
ginkgo.Context("when use-forwarded-headers setting is true", func() {
233+
cmapData := map[string]string{}
234+
235+
cmapData[setting] = "true"
236+
cmapData["use-forwarded-headers"] = "true"
237+
238+
ginkgo.It("should not trust X-Forwarded headers when the client IP address is not trusted", func() {
239+
host := proxyProtocol
240+
241+
f.SetNginxConfigMapData(cmapData)
242+
243+
f.EnsureIngress(framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, nil))
244+
245+
f.WaitForNginxServer(host,
246+
func(server string) bool {
247+
return strings.Contains(server, "server_name proxy-protocol") &&
248+
strings.Contains(server, "listen 80 proxy_protocol")
249+
})
250+
251+
ip := f.GetNginxIP()
252+
253+
conn, err := net.Dial("tcp", net.JoinHostPort(ip, "80"))
254+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error creating connection to %s:80", ip)
255+
defer conn.Close()
256+
257+
header := "PROXY TCP4 192.168.0.1 192.168.0.11 56324 1234\r\n"
258+
_, err = conn.Write([]byte(header))
259+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error writing header")
260+
261+
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: proxy-protocol\r\nX-Forwarded-For: 192.168.0.111\r\n\r\n"))
262+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error writing request")
263+
264+
data, err := io.ReadAll(conn)
265+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error reading connection data")
266+
267+
body := string(data)
268+
assert.Contains(ginkgo.GinkgoT(), body, fmt.Sprintf("host=%v", proxyProtocol))
269+
assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-port=1234")
270+
assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-proto=http")
271+
assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-for=192.168.0.1")
272+
})
273+
274+
ginkgo.It("should trust X-Forwarded headers when the client IP address is trusted", func() {
275+
host := proxyProtocol
276+
277+
// Trust IPs from the private network CIDR block and the client IP address in the proxy protocol header
278+
cmapData["proxy-real-ip-cidr"] = "10.0.0.0/8,192.168.0.1/32"
279+
280+
f.SetNginxConfigMapData(cmapData)
281+
282+
f.EnsureIngress(framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, nil))
283+
284+
f.WaitForNginxServer(host,
285+
func(server string) bool {
286+
return strings.Contains(server, "server_name proxy-protocol") &&
287+
strings.Contains(server, "listen 80 proxy_protocol")
288+
})
289+
290+
ip := f.GetNginxIP()
291+
292+
conn, err := net.Dial("tcp", net.JoinHostPort(ip, "80"))
293+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error creating connection to %s:80", ip)
294+
defer conn.Close()
295+
296+
header := "PROXY TCP4 192.168.0.1 192.168.0.11 56324 1234\r\n"
297+
_, err = conn.Write([]byte(header))
298+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error writing header")
299+
300+
_, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: proxy-protocol\r\nX-Forwarded-For: 192.168.0.111\r\n\r\n"))
301+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error writing request")
302+
303+
data, err := io.ReadAll(conn)
304+
assert.Nil(ginkgo.GinkgoT(), err, "unexpected error reading connection data")
305+
306+
body := string(data)
307+
assert.Contains(ginkgo.GinkgoT(), body, fmt.Sprintf("host=%v", proxyProtocol))
308+
assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-port=1234")
309+
assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-proto=http")
310+
assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-for=192.168.0.111")
311+
})
312+
})
230313
})

0 commit comments

Comments
 (0)