Skip to content

Commit d1dc3c6

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

File tree

3 files changed

+126
-1
lines changed

3 files changed

+126
-1
lines changed

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)