diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md index d8b4f6693..457214766 100644 --- a/docs/user-guide/nginx-configuration/configmap.md +++ b/docs/user-guide/nginx-configuration/configmap.md @@ -909,6 +909,10 @@ If false, NGINX ignores incoming `X-Forwarded-*` headers, filling them with the `enable-real-ip` enables the configuration of [https://nginx.org/en/docs/http/ngx_http_realip_module.html](https://nginx.org/en/docs/http/ngx_http_realip_module.html). Specific attributes of the module can be configured further by using `forwarded-for-header` and `proxy-real-ip-cidr` settings. +## enable-real-ip-recursive + +`enable-real-ip-recursive` enables the configuration of [https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_recursive](https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_recursive). + ## forwarded-for-header Sets the header field for identifying the originating IP address of a client. _**default:**_ X-Forwarded-For diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go index beac1405d..4e8b9e11f 100644 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -565,6 +565,11 @@ type Configuration struct { // Sets whether to enable the real ip module EnableRealIP bool `json:"enable-real-ip"` + // Sets whether to use recursive search in the real ip module + // https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_recursive + // Default: true + EnableRealIPRecursive bool `json:"enable-real-ip-recursive"` + // Sets the header field for identifying the originating IP address of a client // Default is X-Forwarded-For ForwardedForHeader string `json:"forwarded-for-header,omitempty"` @@ -779,6 +784,7 @@ func NewDefault() Configuration { ErrorLogLevel: errorLevel, UseForwardedHeaders: false, EnableRealIP: false, + EnableRealIPRecursive: false, ForwardedForHeader: "X-Forwarded-For", ComputeFullForwardedFor: false, ProxyAddOriginalURIHeader: false, diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 6b8e750b0..9e106a092 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -83,7 +83,12 @@ http { real_ip_header {{ $cfg.ForwardedForHeader }}; {{ end }} + {{ if $cfg.EnableRealIPRecursive }} real_ip_recursive on; + {{ else }} + real_ip_recursive off; + {{ end }} + {{ range $trusted_ip := $cfg.ProxyRealIPCIDR }} set_real_ip_from {{ $trusted_ip }}; {{ end }} diff --git a/test/e2e/settings/enable_real_ip_recursive.go b/test/e2e/settings/enable_real_ip_recursive.go new file mode 100644 index 000000000..feff475e0 --- /dev/null +++ b/test/e2e/settings/enable_real_ip_recursive.go @@ -0,0 +1,102 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package settings + +import ( + "net/http" + "strings" + + "github.com/onsi/ginkgo" + "github.com/stretchr/testify/assert" + + "k8s.io/ingress-nginx/test/e2e/framework" +) + +const forwardedForHost = "forwarded-for-header" + +var _ = framework.DescribeSetting("enable-real-ip-recursive", func() { + f := framework.NewDefaultFramework("enable-real-ip-recursive") + + setting := "enable-real-ip-recursive" + + ginkgo.BeforeEach(func() { + f.NewEchoDeployment() + + f.SetNginxConfigMapData(map[string]string{ + "log-format-escape-json": "true", + "log-format-upstream": "clientip=\"$remote_addr\"", + "use-forwarded-headers": "true", + setting: "false", + }) + }) + + ginkgo.It("should use the first IP in X-Forwarded-For header when setting is true", func() { + host := forwardedForHost + + f.UpdateNginxConfigMapData(setting, "true") + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, nil) + f.EnsureIngress(ing) + + f.WaitForNginxConfiguration(func(conf string) bool { + return strings.Contains(conf, "real_ip_recursive on;") + }) + + ginkgo.By("ensuring single values are parsed correctly") + body := f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("X-Forwarded-For", "127.0.0.1, 1.2.3.4"). + Expect(). + Status(http.StatusOK). + Body(). + Raw() + + assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-for=127.0.0.1") + + logs, err := f.NginxLogs() + assert.Nil(ginkgo.GinkgoT(), err, "obtaining nginx logs") + assert.Contains(ginkgo.GinkgoT(), logs, "clientip=\"127.0.0.1\"") + }) + + ginkgo.It("should use the last IP in X-Forwarded-For header when setting is false", func() { + host := forwardedForHost + + f.UpdateNginxConfigMapData(setting, "false") + + f.EnsureIngress(framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, nil)) + + f.WaitForNginxConfiguration(func(conf string) bool { + return strings.Contains(conf, "real_ip_recursive off;") + }) + + body := f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("X-Forwarded-For", "127.0.0.1, 1.2.3.4"). + Expect(). + Status(http.StatusOK). + Body(). + Raw() + + assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-for=1.2.3.4") + + logs, err := f.NginxLogs() + assert.Nil(ginkgo.GinkgoT(), err, "obtaining nginx logs") + assert.Contains(ginkgo.GinkgoT(), logs, "clientip=\"1.2.3.4\"") + }) +})