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 {