From 2a818c9d032f7019c91f48b9c2f5d312ae30a82c Mon Sep 17 00:00:00 2001 From: Saloni Date: Tue, 23 Jul 2024 15:34:51 -0600 Subject: [PATCH] add support for preserving clientIP --- apis/v1alpha1/nginxproxy_types.go | 49 +++++++ apis/v1alpha1/zz_generated.deepcopy.go | 35 +++++ charts/nginx-gateway-fabric/values.yaml | 5 + .../bases/gateway.nginx.org_nginxproxies.yaml | 33 +++++ deploy/crds.yaml | 33 +++++ .../mode/static/nginx/config/http/config.go | 13 +- internal/mode/static/nginx/config/servers.go | 20 ++- .../static/nginx/config/servers_template.go | 48 +++++-- .../mode/static/nginx/config/servers_test.go | 136 +++++++++++------- .../static/state/dataplane/configuration.go | 19 +++ .../state/dataplane/configuration_test.go | 78 ++++++++++ internal/mode/static/state/dataplane/types.go | 26 +++- .../mode/static/state/graph/nginxproxy.go | 49 ++++++- .../static/state/graph/nginxproxy_test.go | 125 +++++++++++++--- .../how-to/monitoring/troubleshooting.md | 14 ++ site/content/reference/api.md | 123 ++++++++++++++++ 16 files changed, 721 insertions(+), 85 deletions(-) diff --git a/apis/v1alpha1/nginxproxy_types.go b/apis/v1alpha1/nginxproxy_types.go index 018911da7e..913f4e9336 100644 --- a/apis/v1alpha1/nginxproxy_types.go +++ b/apis/v1alpha1/nginxproxy_types.go @@ -53,6 +53,12 @@ type NginxProxySpec struct { // // +optional Telemetry *Telemetry `json:"telemetry,omitempty"` + // RewriteClientIP defines configuration for rewriting the client IP to the original client's IP. + // +kubebuilder:validation:XValidation:message="if mode is set, trustedAddresses is a required field",rule="!(has(self.mode) && !has(self.trustedAddresses))" + // + // +optional + //nolint:lll + RewriteClientIP *RewriteClientIP `json:"rewriteClientIP,omitempty"` // DisableHTTP2 defines if http2 should be disabled for all servers. // Default is false, meaning http2 will be enabled for all servers. // @@ -114,3 +120,46 @@ type TelemetryExporter struct { // +kubebuilder:validation:Pattern=`^(?:http?:\/\/)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*(?::\d{1,5})?$` Endpoint string `json:"endpoint"` } + +// RewriteClientIP specifies the configuration for rewriting the client's IP address. +type RewriteClientIP struct { + // Mode defines how NGINX will rewrite the client's IP address. + // Possible modes: ProxyProtocol, XForwardedFor. + // + // +optional + Mode *RewriteClientIPModeType `json:"mode,omitempty"` + + // SetIPRecursively configures whether recursive search is used for selecting client's + // address from the X-Forwarded-For header and used in conjunction with TrustedAddresses. + // If enabled, NGINX will recurse on the values in X-Forwarded-Header from the end of + // array to start of array and select the first untrusted IP. + // + // +optional + SetIPRecursively *bool `json:"setIPRecursively,omitempty"` + + // TrustedAddresses specifies the addresses that are trusted to send correct client IP information. + // If a request comes from a trusted address, NGINX will rewrite the client IP information, + // and forward it to the backend in the X-Forwarded-For* and X-Real-IP headers. + // This field is required if mode is set. + // +kubebuilder:validation:MaxItems=16 + // + // +optional + TrustedAddresses []string `json:"trustedAddresses,omitempty"` +} + +// RewriteClientIPModeType defines how NGINX Gateway Fabric will determine the client's original IP address. +// +kubebuilder:validation:Enum=ProxyProtocol;XForwardedFor +type RewriteClientIPModeType string + +const ( + // RewriteClientIPModeProxyProtocol configures NGINX to accept PROXY protocol and, + // set the client's IP address to the IP address in the PROXY protocol header. + // Sets the proxy_protocol parameter to the listen directive on all servers, and sets real_ip_header + // to proxy_protocol: https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header. + RewriteClientIPModeProxyProtocol RewriteClientIPModeType = "ProxyProtocol" + + // RewriteClientIPModeXForwardedFor configures NGINX to set the client's IP address to the + // IP address in the X-Forwarded-For HTTP header. + // https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header. + RewriteClientIPModeXForwardedFor RewriteClientIPModeType = "XForwardedFor" +) diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 90cdca7a41..2cbfd84d16 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -367,6 +367,11 @@ func (in *NginxProxySpec) DeepCopyInto(out *NginxProxySpec) { *out = new(Telemetry) (*in).DeepCopyInto(*out) } + if in.RewriteClientIP != nil { + in, out := &in.RewriteClientIP, &out.RewriteClientIP + *out = new(RewriteClientIP) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxProxySpec. @@ -463,6 +468,36 @@ func (in *ObservabilityPolicySpec) DeepCopy() *ObservabilityPolicySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RewriteClientIP) DeepCopyInto(out *RewriteClientIP) { + *out = *in + if in.Mode != nil { + in, out := &in.Mode, &out.Mode + *out = new(RewriteClientIPModeType) + **out = **in + } + if in.SetIPRecursively != nil { + in, out := &in.SetIPRecursively, &out.SetIPRecursively + *out = new(bool) + **out = **in + } + if in.TrustedAddresses != nil { + in, out := &in.TrustedAddresses, &out.TrustedAddresses + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RewriteClientIP. +func (in *RewriteClientIP) DeepCopy() *RewriteClientIP { + if in == nil { + return nil + } + out := new(RewriteClientIP) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SpanAttribute) DeepCopyInto(out *SpanAttribute) { *out = *in diff --git a/charts/nginx-gateway-fabric/values.yaml b/charts/nginx-gateway-fabric/values.yaml index 9cfc2064b2..066eb61c05 100644 --- a/charts/nginx-gateway-fabric/values.yaml +++ b/charts/nginx-gateway-fabric/values.yaml @@ -93,6 +93,11 @@ nginx: {} # disableHTTP2: false # ipFamily: dual + # enableProxyProtocol: true + # rewriteClientIP: + # mode: "ProxyProtocol" + # trustedAddresses: ["127.0.0.0/28", "::1/128"] + # setIPRecursively: true # telemetry: # exporter: # endpoint: otel-collector.default.svc:4317 diff --git a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml index 73acae5e84..1c0edfca66 100644 --- a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml +++ b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml @@ -62,6 +62,39 @@ spec: - ipv4 - ipv6 type: string + rewriteClientIP: + description: RewriteClientIP defines configuration for rewriting the + client IP to the original client's IP. + properties: + mode: + description: |- + Mode defines how NGINX will rewrite the client's IP address. + Possible modes: ProxyProtocol, XForwardedFor. + enum: + - ProxyProtocol + - XForwardedFor + type: string + setIPRecursively: + description: |- + SetIPRecursively configures whether recursive search is used for selecting client's + address from the X-Forwarded-For header and used in conjunction with TrustedAddresses. + If enabled, NGINX will recurse on the values in X-Forwarded-Header from the end of + array to start of array and select the first untrusted IP. + type: boolean + trustedAddresses: + description: |- + TrustedAddresses specifies the addresses that are trusted to send correct client IP information. + If a request comes from a trusted address, NGINX will rewrite the client IP information, + and forward it to the backend in the X-Forwarded-For* and X-Real-IP headers. + This field is required if mode is set. + items: + type: string + maxItems: 16 + type: array + type: object + x-kubernetes-validations: + - message: if mode is set, trustedAddresses is a required field + rule: '!(has(self.mode) && !has(self.trustedAddresses))' telemetry: description: Telemetry specifies the OpenTelemetry configuration. properties: diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 547c912748..15cde2f395 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -707,6 +707,39 @@ spec: - ipv4 - ipv6 type: string + rewriteClientIP: + description: RewriteClientIP defines configuration for rewriting the + client IP to the original client's IP. + properties: + mode: + description: |- + Mode defines how NGINX will rewrite the client's IP address. + Possible modes: ProxyProtocol, XForwardedFor. + enum: + - ProxyProtocol + - XForwardedFor + type: string + setIPRecursively: + description: |- + SetIPRecursively configures whether recursive search is used for selecting client's + address from the X-Forwarded-For header and used in conjunction with TrustedAddresses. + If enabled, NGINX will recurse on the values in X-Forwarded-Header from the end of + array to start of array and select the first untrusted IP. + type: boolean + trustedAddresses: + description: |- + TrustedAddresses specifies the addresses that are trusted to send correct client IP information. + If a request comes from a trusted address, NGINX will rewrite the client IP information, + and forward it to the backend in the X-Forwarded-For* and X-Real-IP headers. + This field is required if mode is set. + items: + type: string + maxItems: 16 + type: array + type: object + x-kubernetes-validations: + - message: if mode is set, trustedAddresses is a required field + rule: '!(has(self.mode) && !has(self.trustedAddresses))' telemetry: description: Telemetry specifies the OpenTelemetry configuration. properties: diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index 9326ebb439..2d3867e2cb 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -115,6 +115,15 @@ type ProxySSLVerify struct { // ServerConfig holds configuration for an HTTP server and IP family to be used by NGINX. type ServerConfig struct { - Servers []Server - IPFamily IPFamily + Servers []Server + RewriteClientIP RewriteClientIPSettings + IPFamily IPFamily +} + +// RewriteClientIP holds the configuration for the rewrite client IP settings. +type RewriteClientIPSettings struct { + RealIPHeader string + RealIPFrom []string + Recursive bool + ProxyProtocol bool } diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 3aeefa47c7..3d411bf973 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -61,8 +61,9 @@ func executeServers(conf dataplane.Configuration) []executeResult { servers, httpMatchPairs := createServers(conf.HTTPServers, conf.SSLServers) serverConfig := http.ServerConfig{ - Servers: servers, - IPFamily: getIPFamily(conf.BaseHTTPConfig), + Servers: servers, + IPFamily: getIPFamily(conf.BaseHTTPConfig), + RewriteClientIP: getRewriteClientIPSettings(conf.BaseHTTPConfig.RewriteClientIPSettings), } serverResult := executeResult{ @@ -103,6 +104,21 @@ func getIPFamily(baseHTTPConfig dataplane.BaseHTTPConfig) http.IPFamily { return http.IPFamily{IPv4: true, IPv6: true} } +// getRewriteClientIPSettings returns the configuration for the rewriting client IP settings. +func getRewriteClientIPSettings(rewriteIP dataplane.RewriteClientIPSettings) http.RewriteClientIPSettings { + var proxyProtocol bool + if rewriteIP.Mode == dataplane.RewriteIPModeProxyProtocol { + proxyProtocol = true + } + + return http.RewriteClientIPSettings{ + Recursive: rewriteIP.IPRecursive, + ProxyProtocol: proxyProtocol, + RealIPFrom: rewriteIP.TrustedCIDRs, + RealIPHeader: string(rewriteIP.Mode), + } +} + func createAdditionFileResults(conf dataplane.Configuration) []executeResult { uniqueAdditions := make(map[string][]byte) diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index 4d8196b180..3a56ab3ba5 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -2,27 +2,46 @@ package config const serversTemplateText = ` js_preload_object matches from /etc/nginx/conf.d/matches.json; +{{ $proxyProtocol := "" }} +{{ if $.RewriteClientIP.ProxyProtocol }}{{ $proxyProtocol = " proxy_protocol" }}{{ end }} + {{- range $s := .Servers -}} {{ if $s.IsDefaultSSL -}} server { {{- if $.IPFamily.IPv4 }} - listen {{ $s.Port }} ssl default_server; + listen {{ $s.Port }} ssl default_server{{ $proxyProtocol }}; {{- end }} {{- if $.IPFamily.IPv6 }} - listen [::]:{{ $s.Port }} ssl default_server; + listen [::]:{{ $s.Port }} ssl default_server{{ $proxyProtocol }}; {{- end }} - ssl_reject_handshake on; + {{- range $cidr := $.RewriteClientIP.RealIPFrom }} + set_real_ip_from {{ $cidr }}; + {{- end}} + {{- if $.RewriteClientIP.RealIPHeader}} + real_ip_header {{ $.RewriteClientIP.RealIPHeader }}; + {{- end}} + {{- if $.RewriteClientIP.Recursive }} + real_ip_recursive on; + {{ end }} } {{- else if $s.IsDefaultHTTP }} server { {{- if $.IPFamily.IPv4 }} - listen {{ $s.Port }} default_server; + listen {{ $s.Port }} default_server{{ $proxyProtocol }}; {{- end }} {{- if $.IPFamily.IPv6 }} - listen [::]:{{ $s.Port }} default_server; + listen [::]:{{ $s.Port }} default_server{{ $proxyProtocol }}; {{- end }} - + {{- range $cidr := $.RewriteClientIP.RealIPFrom }} + set_real_ip_from {{ $cidr }}; + {{- end}} + {{- if $.RewriteClientIP.RealIPHeader}} + real_ip_header {{ $.RewriteClientIP.RealIPHeader }}; + {{- end}} + {{- if $.RewriteClientIP.Recursive }} + real_ip_recursive on; + {{ end }} default_type text/html; return 404; } @@ -30,10 +49,10 @@ server { server { {{- if $s.SSL }} {{- if $.IPFamily.IPv4 }} - listen {{ $s.Port }} ssl; + listen {{ $s.Port }} ssl{{ $proxyProtocol }}; {{- end }} {{- if $.IPFamily.IPv6 }} - listen [::]:{{ $s.Port }} ssl; + listen [::]:{{ $s.Port }} ssl{{ $proxyProtocol }}; {{- end }} ssl_certificate {{ $s.SSL.Certificate }}; ssl_certificate_key {{ $s.SSL.CertificateKey }}; @@ -43,10 +62,10 @@ server { } {{- else }} {{- if $.IPFamily.IPv4 }} - listen {{ $s.Port }}; + listen {{ $s.Port }}{{ $proxyProtocol }}; {{- end }} {{- if $.IPFamily.IPv6 }} - listen [::]:{{ $s.Port }}; + listen [::]:{{ $s.Port }}{{ $proxyProtocol }}; {{- end }} {{- end }} @@ -55,6 +74,15 @@ server { {{- range $i := $s.Includes }} include {{ $i }}; {{ end -}} + {{- range $cidr := $.RewriteClientIP.RealIPFrom }} + set_real_ip_from {{ $cidr }}; + {{- end}} + {{- if $.RewriteClientIP.RealIPHeader}} + real_ip_header {{ $.RewriteClientIP.RealIPHeader }}; + {{- end}} + {{- if $.RewriteClientIP.Recursive }} + real_ip_recursive on; + {{ end }} {{ range $l := $s.Locations }} location {{ $l.Path }} { diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index effb0099ab..d8cb74fe56 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -137,7 +137,7 @@ func TestExecuteServers(t *testing.T) { } } -func TestExecuteServersForIPFamily(t *testing.T) { +func TestExecuteServerConfig(t *testing.T) { httpServers := []dataplane.VirtualServer{ { IsDefault: true, @@ -166,68 +166,102 @@ func TestExecuteServersForIPFamily(t *testing.T) { expectedHTTPConfig map[string]int config dataplane.Configuration }{ - { - msg: "http and ssl servers with IPv4 IP family", - config: dataplane.Configuration{ - HTTPServers: httpServers, - SSLServers: sslServers, - BaseHTTPConfig: dataplane.BaseHTTPConfig{ - IPFamily: dataplane.IPv4, - }, - }, - expectedHTTPConfig: map[string]int{ - "listen 8080 default_server;": 1, - "listen 8080;": 1, - "listen 8443 ssl default_server;": 1, - "listen 8443 ssl;": 1, - "server_name example.com;": 2, - "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, - "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, - "ssl_reject_handshake on;": 1, - }, - }, - { - msg: "http and ssl servers with IPv6 IP family", - config: dataplane.Configuration{ - HTTPServers: httpServers, - SSLServers: sslServers, - BaseHTTPConfig: dataplane.BaseHTTPConfig{ - IPFamily: dataplane.IPv6, - }, - }, - expectedHTTPConfig: map[string]int{ - "listen [::]:8080 default_server;": 1, - "listen [::]:8080;": 1, - "listen [::]:8443 ssl default_server;": 1, - "listen [::]:8443 ssl;": 1, - "server_name example.com;": 2, - "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, - "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, - "ssl_reject_handshake on;": 1, - }, - }, - { - msg: "http and ssl servers with Dual IP family", + // { + // msg: "http and ssl servers with IPv4 IP family", + // config: dataplane.Configuration{ + // HTTPServers: httpServers, + // SSLServers: sslServers, + // BaseHTTPConfig: dataplane.BaseHTTPConfig{ + // IPFamily: dataplane.IPv4, + // }, + // }, + // expectedHTTPConfig: map[string]int{ + // "listen 8080 default_server;": 1, + // "listen 8080;": 1, + // "listen 8443 ssl default_server;": 1, + // "listen 8443 ssl;": 1, + // "server_name example.com;": 2, + // "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, + // "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, + // "ssl_reject_handshake on;": 1, + // }, + // }, + // { + // msg: "http and ssl servers with IPv6 IP family", + // config: dataplane.Configuration{ + // HTTPServers: httpServers, + // SSLServers: sslServers, + // BaseHTTPConfig: dataplane.BaseHTTPConfig{ + // IPFamily: dataplane.IPv6, + // }, + // }, + // expectedHTTPConfig: map[string]int{ + // "listen [::]:8080 default_server;": 1, + // "listen [::]:8080;": 1, + // "listen [::]:8443 ssl default_server;": 1, + // "listen [::]:8443 ssl;": 1, + // "server_name example.com;": 2, + // "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, + // "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, + // "ssl_reject_handshake on;": 1, + // }, + // }, + // { + // msg: "http and ssl servers with Dual IP family", + // config: dataplane.Configuration{ + // HTTPServers: httpServers, + // SSLServers: sslServers, + // BaseHTTPConfig: dataplane.BaseHTTPConfig{ + // IPFamily: dataplane.Dual, + // }, + // }, + // expectedHTTPConfig: map[string]int{ + // "listen 8080 default_server;": 1, + // "listen 8080;": 1, + // "listen 8443 ssl default_server;": 1, + // "listen 8443 ssl;": 1, + // "server_name example.com;": 2, + // "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, + // "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, + // "ssl_reject_handshake on;": 1, + // "listen [::]:8080 default_server;": 1, + // "listen [::]:8080;": 1, + // "listen [::]:8443 ssl default_server;": 1, + // "listen [::]:8443 ssl;": 1, + // "real_ip_header proxy-protocol;": 0, + // "real_ip_recursive on;": 0, + // }, + // }, + { + msg: "http and ssl servers with rewrite client IP settings", config: dataplane.Configuration{ HTTPServers: httpServers, SSLServers: sslServers, BaseHTTPConfig: dataplane.BaseHTTPConfig{ IPFamily: dataplane.Dual, + RewriteClientIPSettings: dataplane.RewriteClientIPSettings{ + Mode: dataplane.RewriteIPModeProxyProtocol, + TrustedCIDRs: []string{"0.0.0.0/0"}, + IPRecursive: true, + }, }, }, expectedHTTPConfig: map[string]int{ - "listen 8080 default_server;": 1, - "listen 8080;": 1, - "listen 8443 ssl default_server;": 1, - "listen 8443 ssl;": 1, + "set_real_ip_from 0.0.0.0/0;": 4, + "real_ip_header proxy-protocol;": 4, + "real_ip_recursive on;": 4, + "listen 8080 default_server proxy_protocol;": 1, + "listen 8080 proxy_protocol;": 1, + "listen 8443 ssl default_server proxy_protocol;": 1, + "listen 8443 ssl proxy_protocol;": 1, "server_name example.com;": 2, "ssl_certificate /etc/nginx/secrets/test-keypair.pem;": 1, "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 1, "ssl_reject_handshake on;": 1, - "listen [::]:8080 default_server;": 1, - "listen [::]:8080;": 1, - "listen [::]:8443 ssl default_server;": 1, - "listen [::]:8443 ssl;": 1, + "listen [::]:8080 default_server proxy_protocol;": 1, + "listen [::]:8080 proxy_protocol;": 1, + "listen [::]:8443 ssl default_server proxy_protocol;": 1, + "listen [::]:8443 ssl proxy_protocol;": 1, }, }, } diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index b81dbfe7a0..9bd2587d41 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -696,6 +696,25 @@ func buildBaseHTTPConfig(g *graph.Graph) BaseHTTPConfig { } } + if g.NginxProxy.Source.Spec.RewriteClientIP != nil { + if g.NginxProxy.Source.Spec.RewriteClientIP.Mode != nil { + switch *g.NginxProxy.Source.Spec.RewriteClientIP.Mode { + case ngfAPI.RewriteClientIPModeProxyProtocol: + baseConfig.RewriteClientIPSettings.Mode = RewriteIPModeProxyProtocol + case ngfAPI.RewriteClientIPModeXForwardedFor: + baseConfig.RewriteClientIPSettings.Mode = RewriteIPModeXForwardedFor + } + } + + if len(g.NginxProxy.Source.Spec.RewriteClientIP.TrustedAddresses) > 0 { + baseConfig.RewriteClientIPSettings.TrustedCIDRs = g.NginxProxy.Source.Spec.RewriteClientIP.TrustedAddresses + } + + if g.NginxProxy.Source.Spec.RewriteClientIP.SetIPRecursively != nil { + baseConfig.RewriteClientIPSettings.IPRecursive = *g.NginxProxy.Source.Spec.RewriteClientIP.SetIPRecursively + } + } + return baseConfig } diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index 83d5873ce0..811bc4e245 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -2089,6 +2089,34 @@ func TestBuildConfiguration(t *testing.T) { }), msg: "NginxProxy with IPv6 IPFamily and no routes", }, + // { + // graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + // g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ + // Name: "gw", + // Namespace: "ns", + // } + // g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + // Name: "listener-80-1", + // Source: listener80, + // Valid: true, + // Routes: map[graph.RouteKey]*graph.L7Route{}, + // }) + // g.NginxProxy = &graph.NginxProxy{ + // Valid: true, + // Source: &ngfAPI.NginxProxy{ + // Spec: ngfAPI.NginxProxySpec{}, + // }, + // } + // return g + // }), + // expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + // conf.SSLServers = []VirtualServer{} + // conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + // conf.BaseHTTPConfig = BaseHTTPConfig{HTTP2: true, IPFamily: Dual, ProxyProtocol: true} + // return conf + // }), + // msg: "NginxProxy with proxy protocol enabled", + // }, } for _, test := range tests { @@ -3215,3 +3243,53 @@ func TestGetAllowedAddressType(t *testing.T) { }) } } + +// TODO: add more tests. +func TestBuildRewriteIPSettings(t *testing.T) { + tests := []struct { + msg string + g *graph.Graph + expRewriteIPSettings RewriteClientIPSettings + }{ + { + msg: "no rewrite IP settings configured", + g: &graph.Graph{ + NginxProxy: &graph.NginxProxy{ + Valid: true, + Source: &ngfAPI.NginxProxy{}, + }, + }, + expRewriteIPSettings: RewriteClientIPSettings{}, + }, + { + msg: "rewrite IP settings configured", + g: &graph.Graph{ + NginxProxy: &graph.NginxProxy{ + Valid: true, + Source: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + RewriteClientIP: &ngfAPI.RewriteClientIP{ + Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeProxyProtocol), + TrustedAddresses: []string{"0.0.0.0/0"}, + SetIPRecursively: helpers.GetPointer(true), + }, + }, + }, + }, + }, + expRewriteIPSettings: RewriteClientIPSettings{ + Mode: RewriteIPModeProxyProtocol, + TrustedCIDRs: []string{"0.0.0.0/0"}, + IPRecursive: true, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.msg, func(t *testing.T) { + g := NewWithT(t) + baseConfig := buildBaseHTTPConfig(tc.g) + g.Expect(baseConfig.RewriteClientIPSettings).To(Equal(tc.expRewriteIPSettings)) + }) + } +} diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index d342ff3b0c..846b1a086f 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -33,10 +33,10 @@ type Configuration struct { Upstreams []Upstream // BackendGroups holds all unique BackendGroups. BackendGroups []BackendGroup - // BaseHTTPConfig holds the configuration options at the http context. - BaseHTTPConfig BaseHTTPConfig // Telemetry holds the Otel configuration. Telemetry Telemetry + // BaseHTTPConfig holds the configuration options at the http context. + BaseHTTPConfig BaseHTTPConfig // Version represents the version of the generated configuration. Version int } @@ -298,10 +298,32 @@ type SpanAttribute struct { type BaseHTTPConfig struct { // IPFamily specifies the IP family for all servers. IPFamily IPFamilyType + // RewriteIPSettings defines configuration for rewriting the client IP to the original client's IP. + RewriteClientIPSettings RewriteClientIPSettings // HTTP2 specifies whether http2 should be enabled for all servers. HTTP2 bool } +// RewriteIPSettings defines configuration for rewriting the client IP to the original client's IP. +type RewriteClientIPSettings struct { + // Mode specifies the mode for rewriting the client IP. + Mode RewriteIPModeType + // TrustedCIDRs specifies the CIDRs that are trusted to provide the client IP. + TrustedCIDRs []string + // IPRecursive specifies whether a recursive search is used when selecting the client IP. + IPRecursive bool +} + +// RewriteIPModeType specifies the mode for rewriting the client IP. +type RewriteIPModeType string + +const ( + // RewriteIPModeProxyProtocol specifies that client IP will be rewrritten using the Proxy-Protocol header. + RewriteIPModeProxyProtocol RewriteIPModeType = "proxy-protocol" + // RewriteIPModeXForwardedFor specifies that client IP will be rewrritten using the X-Forwarded-For header. + RewriteIPModeXForwardedFor RewriteIPModeType = "X-Forwarded-For" +) + // IPFamilyType specifies the IP family to be used by NGINX. type IPFamilyType string diff --git a/internal/mode/static/state/graph/nginxproxy.go b/internal/mode/static/state/graph/nginxproxy.go index d36133308b..f53ff34cb5 100644 --- a/internal/mode/static/state/graph/nginxproxy.go +++ b/internal/mode/static/state/graph/nginxproxy.go @@ -2,6 +2,7 @@ package graph import ( "k8s.io/apimachinery/pkg/types" + k8svalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" v1 "sigs.k8s.io/gateway-api/apis/v1" @@ -31,7 +32,6 @@ func buildNginxProxy( npCfg := nps[types.NamespacedName{Name: gc.Spec.ParametersRef.Name}] if npCfg != nil { errs := validateNginxProxy(validator, npCfg) - return &NginxProxy{ Source: npCfg, Valid: len(errs) == 0, @@ -126,5 +126,52 @@ func validateNginxProxy( npCfg.Spec.IPFamily = helpers.GetPointer[ngfAPI.IPFamilyType](ngfAPI.Dual) } + allErrs = append(allErrs, validateRewriteClientIP(npCfg)...) + + return allErrs +} + +func validateRewriteClientIP(npCfg *ngfAPI.NginxProxy) field.ErrorList { + var allErrs field.ErrorList + spec := field.NewPath("spec") + + if npCfg.Spec.RewriteClientIP != nil { + rewriteClientIP := npCfg.Spec.RewriteClientIP + rewriteClientIPPath := spec.Child("rewriteClientIP") + trustedAddressesPath := rewriteClientIPPath.Child("trustedAddresses") + + if rewriteClientIP.Mode != nil { + mode := *rewriteClientIP.Mode + if len(rewriteClientIP.TrustedAddresses) == 0 { + allErrs = append(allErrs, + field.Required(rewriteClientIPPath, "trustedAddresses field required when mode is set")) + } + + switch mode { + case ngfAPI.RewriteClientIPModeProxyProtocol, ngfAPI.RewriteClientIPModeXForwardedFor: + default: + allErrs = append( + allErrs, + field.NotSupported( + rewriteClientIPPath.Child("mode"), + mode, + []string{string(ngfAPI.RewriteClientIPModeProxyProtocol), string(ngfAPI.RewriteClientIPModeXForwardedFor)}), + ) + } + } + + if len(rewriteClientIP.TrustedAddresses) > 16 { + allErrs = append( + allErrs, + field.TooLongMaxLength(trustedAddressesPath, rewriteClientIP.TrustedAddresses, 16)) + } + + for i, addr := range rewriteClientIP.TrustedAddresses { + if err := k8svalidation.IsValidCIDR(trustedAddressesPath, addr); err != nil { + allErrs = append(allErrs, field.Invalid(trustedAddressesPath.Index(i), addr, err.ToAggregate().Error())) + } + } + } + return allErrs } diff --git a/internal/mode/static/state/graph/nginxproxy_test.go b/internal/mode/static/state/graph/nginxproxy_test.go index efec06e865..611145d108 100644 --- a/internal/mode/static/state/graph/nginxproxy_test.go +++ b/internal/mode/static/state/graph/nginxproxy_test.go @@ -222,27 +222,27 @@ func TestGCReferencesAnyNginxProxy(t *testing.T) { } } -func TestValidateNginxProxy(t *testing.T) { - createValidValidator := func() *validationfakes.FakeGenericValidator { - v := &validationfakes.FakeGenericValidator{} - v.ValidateEscapedStringNoVarExpansionReturns(nil) - v.ValidateEndpointReturns(nil) - v.ValidateServiceNameReturns(nil) - v.ValidateNginxDurationReturns(nil) +func createValidValidator() *validationfakes.FakeGenericValidator { + v := &validationfakes.FakeGenericValidator{} + v.ValidateEscapedStringNoVarExpansionReturns(nil) + v.ValidateEndpointReturns(nil) + v.ValidateServiceNameReturns(nil) + v.ValidateNginxDurationReturns(nil) - return v - } + return v +} - createInvalidValidator := func() *validationfakes.FakeGenericValidator { - v := &validationfakes.FakeGenericValidator{} - v.ValidateEscapedStringNoVarExpansionReturns(errors.New("error")) - v.ValidateEndpointReturns(errors.New("error")) - v.ValidateServiceNameReturns(errors.New("error")) - v.ValidateNginxDurationReturns(errors.New("error")) +func createInvalidValidator() *validationfakes.FakeGenericValidator { + v := &validationfakes.FakeGenericValidator{} + v.ValidateEscapedStringNoVarExpansionReturns(errors.New("error")) + v.ValidateEndpointReturns(errors.New("error")) + v.ValidateServiceNameReturns(errors.New("error")) + v.ValidateNginxDurationReturns(errors.New("error")) - return v - } + return v +} +func TestValidateNginxProxy(t *testing.T) { tests := []struct { np *ngfAPI.NginxProxy validator *validationfakes.FakeGenericValidator @@ -266,6 +266,11 @@ func TestValidateNginxProxy(t *testing.T) { }, }, IPFamily: helpers.GetPointer[ngfAPI.IPFamilyType](ngfAPI.Dual), + RewriteClientIP: &ngfAPI.RewriteClientIP{ + SetIPRecursively: helpers.GetPointer(true), + TrustedAddresses: []string{"2001:db8:a0b:12f0::1/32", "0.0.0.0/0"}, + Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeProxyProtocol), + }, }, }, expectErrCount: 0, @@ -356,3 +361,89 @@ func TestValidateNginxProxy(t *testing.T) { }) } } + +func TestValidateRewriteClientIP(t *testing.T) { + tests := []struct { + np *ngfAPI.NginxProxy + validator *validationfakes.FakeGenericValidator + name string + expErrSubstring string + expectErrCount int + }{ + { + name: "valid rewriteClientIP", + validator: createValidValidator(), + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + RewriteClientIP: &ngfAPI.RewriteClientIP{ + SetIPRecursively: helpers.GetPointer(true), + TrustedAddresses: []string{"2001:db8:a0b:12f0::1/32", "0.0.0.0/0"}, + Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeProxyProtocol), + }, + }, + }, + expectErrCount: 0, + }, + { + name: "invalid CIDR in trustedAddresses", + validator: createInvalidValidator(), + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + RewriteClientIP: &ngfAPI.RewriteClientIP{ + SetIPRecursively: helpers.GetPointer(true), + TrustedAddresses: []string{"2001:db8:a0b:12f0::1"}, + Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeProxyProtocol), + }, + }, + }, + expectErrCount: 1, + expErrSubstring: "spec.rewriteClientIP.trustedAddresses[0]", + }, + { + name: "invalid when mode is set and trustedAddresses is empty", + validator: createInvalidValidator(), + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + RewriteClientIP: &ngfAPI.RewriteClientIP{ + Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeProxyProtocol), + }, + }, + }, + expectErrCount: 1, + expErrSubstring: "trustedAddresses field required when mode is set", + }, + { + name: "invalid when trustedAddresses is greater in length than 16", + validator: createInvalidValidator(), + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + RewriteClientIP: &ngfAPI.RewriteClientIP{ + Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeProxyProtocol), + TrustedAddresses: []string{ + "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", + "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", + "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", + "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", + "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", + "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", "2001:db8:a0b:12f0::1/32", + }, + }, + }, + }, + expectErrCount: 1, + expErrSubstring: "Too long: may not be longer than 16", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + allErrs := validateRewriteClientIP(test.np) + g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + if len(allErrs) > 0 { + g.Expect(allErrs.ToAggregate().Error()).To(ContainSubstring(test.expErrSubstring)) + } + }) + } +} diff --git a/site/content/how-to/monitoring/troubleshooting.md b/site/content/how-to/monitoring/troubleshooting.md index e15bee6a1c..98ff2f8b05 100644 --- a/site/content/how-to/monitoring/troubleshooting.md +++ b/site/content/how-to/monitoring/troubleshooting.md @@ -439,6 +439,20 @@ To **resolve** this, you can do one of the following: - Adjust the IPFamily of your Service to match that of the NginxProxy configuration. +##### Broken Header Error + +If you check your _nginx_ container logs and see the following error: + + ```text + 2024/07/25 00:50:45 [error] 211#211: *22 broken header: "GET /coffee HTTP/1.1" while reading PROXY protocol, client: 127.0.0.1, server: 0.0.0.0:80 + ``` + +It indicates that `proxy_protocol` is enabled for the gateway listeners but the request sent to the application endpoint does not contain proxy information.To **resolve** this, you can do one of the following: + +- Update the NginxProxy configuration with [`enableProxyProtocol`](({{< relref "reference/api.md" >}})) disabled. + +- Send valid proxy information with requests being handled by your application. + ### Further reading You can view the [Kubernetes Troubleshooting Guide](https://kubernetes.io/docs/tasks/debug/debug-application/) for more debugging guidance. diff --git a/site/content/reference/api.md b/site/content/reference/api.md index b597e1a39e..1e738de3d4 100644 --- a/site/content/reference/api.md +++ b/site/content/reference/api.md @@ -328,6 +328,20 @@ Telemetry +rewriteClientIP
+ + +RewriteClientIP + + + + +(Optional) +

RewriteClientIP defines configuration for rewriting the client IP to the original client’s IP.

+ + + + disableHTTP2
bool @@ -950,6 +964,20 @@ Telemetry +rewriteClientIP
+ + +RewriteClientIP + + + + +(Optional) +

RewriteClientIP defines configuration for rewriting the client IP to the original client’s IP.

+ + + + disableHTTP2
bool @@ -1012,6 +1040,101 @@ Support: HTTPRoute, GRPCRoute.

+

RewriteClientIP + +

+

+(Appears on: +NginxProxySpec) +

+

+

RewriteClientIP specifies the configuration for rewriting the client’s IP address.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+mode
+ + +RewriteClientIPModeType + + +
+(Optional) +

Mode defines how NGINX will rewrite the client’s IP address. +Possible modes: ProxyProtocol, XForwardedFor.

+
+setIPRecursively
+ +bool + +
+(Optional) +

SetIPRecursively configures whether recursive search is used for selecting client’s +address from the X-Forwarded-For header and used in conjunction with TrustedAddresses. +If enabled, NGINX will recurse on the values in X-Forwarded-Header from the end of +array to start of array and select the first untrusted IP.

+
+trustedAddresses
+ +[]string + +
+(Optional) +

TrustedAddresses specifies the addresses that are trusted to send correct client IP information. +If a request comes from a trusted address, NGINX will rewrite the client IP information, +and forward it to the backend in the X-Forwarded-For* and X-Real-IP headers. +This field is required if mode is set.

+
+

RewriteClientIPModeType +(string alias)

+

+

+(Appears on: +RewriteClientIP) +

+

+

RewriteClientIPModeType defines how NGINX Gateway Fabric will determine the client’s original IP address.

+

+ + + + + + + + + + + + +
ValueDescription

"ProxyProtocol"

RewriteClientIPModeProxyProtocol configures NGINX to accept PROXY protocol and, +set the client’s IP address to the IP address in the PROXY protocol header. +Sets the proxy_protocol parameter to the listen directive on all servers, and sets real_ip_header +to proxy_protocol: https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header.

+

"XForwardedFor"

RewriteClientIPModeXForwardedFor configures NGINX to set the client’s IP address to the +IP address in the X-Forwarded-For HTTP header. +https://nginx.org/en/docs/http/ngx_http_realip_module.html#real_ip_header.

+

Size (string alias)