From f71b9419953bf810b53c46d073079b4766b5c06c Mon Sep 17 00:00:00 2001 From: blasko03 <60181060+blasko03@users.noreply.github.com> Date: Mon, 22 Dec 2025 09:52:04 +0100 Subject: [PATCH] Support NGINX whitelist-source-range annotation --- .../kubernetes/ingress-nginx.md | 8 +- .../kubernetes/ingress-nginx/annotations.go | 2 + .../10-ingress-with-whitelist-single-ip.yml | 22 ++ .../11-ingress-with-whitelist-single-cidr.yml | 22 ++ ...ss-with-whitelist-multiple-ip-and-cidr.yml | 22 ++ .../13-ingress-with-whitelist-empty.yml | 22 ++ .../kubernetes/ingress-nginx/kubernetes.go | 22 ++ .../ingress-nginx/kubernetes_test.go | 202 ++++++++++++++++++ 8 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-whitelist-single-ip.yml create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-whitelist-single-cidr.yml create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/13-ingress-with-whitelist-empty.yml diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index d307d9edf..185d843a5 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -311,6 +311,13 @@ The following annotations are organized by category for easier navigation. |-------------------------------------------------------|--------------------------------------------------------------------------------------------| | `nginx.ingress.kubernetes.io/use-regex` | | +### IP Whitelist + +| Annotation | Limitations / Notes | +|-------------------------------------------------------|--------------------------------------------------------------------------------------------| +| `nginx.ingress.kubernetes.io/whitelist-source-range` | | + + ## Limitations ### Caveats and Key Behavioral Differences @@ -422,7 +429,6 @@ The following annotations are organized by category for easier navigation. | `nginx.ingress.kubernetes.io/x-forwarded-prefix` | | | `nginx.ingress.kubernetes.io/upstream-hash-by` | | | `nginx.ingress.kubernetes.io/denylist-source-range` | | -| `nginx.ingress.kubernetes.io/whitelist-source-range` | | | `nginx.ingress.kubernetes.io/proxy-buffering` | | | `nginx.ingress.kubernetes.io/proxy-buffers-number` | | | `nginx.ingress.kubernetes.io/proxy-buffer-size` | | diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go index ed8827240..df183f014 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go @@ -50,6 +50,8 @@ type ingressConfig struct { CORSAllowOrigin *[]string `annotation:"nginx.ingress.kubernetes.io/cors-allow-origin"` CORSMaxAge *int `annotation:"nginx.ingress.kubernetes.io/cors-max-age"` + WhitelistSourceRange *string `annotation:"nginx.ingress.kubernetes.io/whitelist-source-range"` + UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"` } diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-whitelist-single-ip.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-whitelist-single-ip.yml new file mode 100644 index 000000000..100961e94 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/10-ingress-with-whitelist-single-ip.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-whitelist-single-ip + namespace: default + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "192.168.20.1" + +spec: + ingressClassName: nginx + rules: + - host: whitelist-source-range.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-whitelist-single-cidr.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-whitelist-single-cidr.yml new file mode 100644 index 000000000..daf30cd67 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-whitelist-single-cidr.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-whitelist-single-cidr + namespace: default + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "192.168.1.0/24" + +spec: + ingressClassName: nginx + rules: + - host: whitelist-source-range.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml new file mode 100644 index 000000000..482c97d52 --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-whitelist-multiple-ip-and-cidr + namespace: default + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "192.168.1.0/24, 10.0.0.0/8, 192.168.20.1" + +spec: + ingressClassName: nginx + rules: + - host: whitelist-source-range.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/13-ingress-with-whitelist-empty.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/13-ingress-with-whitelist-empty.yml new file mode 100644 index 000000000..5b9bf806e --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/13-ingress-with-whitelist-empty.yml @@ -0,0 +1,22 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-whitelist-empty + namespace: default + annotations: + nginx.ingress.kubernetes.io/whitelist-source-range: "" + +spec: + ingressClassName: nginx + rules: + - host: whitelist-source-range.localhost + http: + paths: + - path: / + pathType: Exact + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go index 1f5a57306..7495400fb 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go @@ -794,6 +794,8 @@ func (p *Provider) applyMiddlewares(namespace, routerKey string, ingressConfig i return fmt.Errorf("applying forward auth configuration: %w", err) } + applyWhitelistSourceRangeConfiguration(routerKey, ingressConfig, rt, conf) + applyCORSConfiguration(routerKey, ingressConfig, rt, conf) // Apply SSL redirect is mandatory to be applied after all other middlewares. @@ -951,6 +953,26 @@ func applyUpstreamVhost(routerName string, ingressConfig ingressConfig, rt *dyna rt.Middlewares = append(rt.Middlewares, vHostMiddlewareName) } +func applyWhitelistSourceRangeConfiguration(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) { + whitelistSourceRange := ptr.Deref(ingressConfig.WhitelistSourceRange, "") + if whitelistSourceRange == "" { + return + } + + sourceRanges := strings.Split(whitelistSourceRange, ",") + for i := range sourceRanges { + sourceRanges[i] = strings.TrimSpace(sourceRanges[i]) + } + + whitelistSourceRangeMiddlewareName := routerName + "-whitelist-source-range" + conf.HTTP.Middlewares[whitelistSourceRangeMiddlewareName] = &dynamic.Middleware{ + IPAllowList: &dynamic.IPAllowList{ + SourceRange: sourceRanges, + }, + } + rt.Middlewares = append(rt.Middlewares, whitelistSourceRangeMiddlewareName) +} + func applySSLRedirectConfiguration(routerName string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) { var forceSSLRedirect bool if ingressConfig.ForceSSLRedirect != nil { diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go index 437b4f277..0492cdf85 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go @@ -609,6 +609,208 @@ func TestLoadIngresses(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "WhitelistSourceRange with single IP", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/10-ingress-with-whitelist-single-ip.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-whitelist-single-ip-rule-0-path-0": { + Rule: "Host(`whitelist-source-range.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-whitelist-single-ip-rule-0-path-0-whitelist-source-range"}, + Service: "default-whoami-80", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-whitelist-single-ip-rule-0-path-0-whitelist-source-range": { + IPAllowList: &dynamic.IPAllowList{ + SourceRange: []string{"192.168.20.1"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "WhitelistSourceRange with single CIDR", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/11-ingress-with-whitelist-single-cidr.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-whitelist-single-cidr-rule-0-path-0": { + Rule: "Host(`whitelist-source-range.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-whitelist-single-cidr-rule-0-path-0-whitelist-source-range"}, + Service: "default-whoami-80", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-whitelist-single-cidr-rule-0-path-0-whitelist-source-range": { + IPAllowList: &dynamic.IPAllowList{ + SourceRange: []string{"192.168.1.0/24"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "WhitelistSourceRange when specified multiple IP/CIDR", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/12-ingress-with-whitelist-multiple-ip-and-cidr.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-whitelist-multiple-ip-and-cidr-rule-0-path-0": { + Rule: "Host(`whitelist-source-range.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: []string{"default-ingress-with-whitelist-multiple-ip-and-cidr-rule-0-path-0-whitelist-source-range"}, + Service: "default-whoami-80", + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-whitelist-multiple-ip-and-cidr-rule-0-path-0-whitelist-source-range": { + IPAllowList: &dynamic.IPAllowList{ + SourceRange: []string{"192.168.1.0/24", "10.0.0.0/8", "192.168.20.1"}, + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "WhitelistSourceRange when empty ignored", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/13-ingress-with-whitelist-empty.yml", + }, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-ingress-with-whitelist-empty-rule-0-path-0": { + Rule: "Host(`whitelist-source-range.localhost`) && Path(`/`)", + RuleSyntax: "default", + Middlewares: nil, + Service: "default-whoami-80", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + Strategy: "wrr", + PassHostHeader: ptr.To(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: dynamic.DefaultFlushInterval, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, } for _, test := range testCases {