diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
index 185d843a5..ee055c64c 100644
--- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
+++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
@@ -287,12 +287,13 @@ The following annotations are organized by category for easier navigation.
### Load Balancing & Backend
-| Annotation | Limitations / Notes |
-|-------------------------------------------------------|--------------------------------------------------------------------------------------------|
-| `nginx.ingress.kubernetes.io/load-balance` | Only round_robin supported; ewma and IP hash not supported. |
-| `nginx.ingress.kubernetes.io/backend-protocol` | FCGI and AUTO_HTTP not supported. |
-| `nginx.ingress.kubernetes.io/service-upstream` | |
-| `nginx.ingress.kubernetes.io/upstream-vhost` | |
+| Annotation | Limitations / Notes |
+|-------------------------------------------------------|--------------------------------------------------------------------------------------------------|
+| `nginx.ingress.kubernetes.io/load-balance` | Only round_robin supported; ewma and IP hash not supported. |
+| `nginx.ingress.kubernetes.io/backend-protocol` | FCGI and AUTO_HTTP not supported. |
+| `nginx.ingress.kubernetes.io/service-upstream` | |
+| `nginx.ingress.kubernetes.io/upstream-vhost` | |
+| | Header whitelisting, similar to `global-allowed-response-headers` NGINX config is not supported. |
### CORS
diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go
index df183f014..06edd65d9 100644
--- a/pkg/provider/kubernetes/ingress-nginx/annotations.go
+++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go
@@ -52,6 +52,7 @@ type ingressConfig struct {
WhitelistSourceRange *string `annotation:"nginx.ingress.kubernetes.io/whitelist-source-range"`
+ CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
}
diff --git a/pkg/provider/kubernetes/ingress-nginx/client.go b/pkg/provider/kubernetes/ingress-nginx/client.go
index 93eae4a09..f1819ad11 100644
--- a/pkg/provider/kubernetes/ingress-nginx/client.go
+++ b/pkg/provider/kubernetes/ingress-nginx/client.go
@@ -37,6 +37,7 @@ type clientWrapper struct {
clusterScopeFactory kinformers.SharedInformerFactory
factoriesKube map[string]kinformers.SharedInformerFactory
factoriesSecret map[string]kinformers.SharedInformerFactory
+ factoriesConfigMap map[string]kinformers.SharedInformerFactory
factoriesIngress map[string]kinformers.SharedInformerFactory
isNamespaceAll bool
watchedNamespaces []string
@@ -115,10 +116,11 @@ func createClientFromConfig(c *rest.Config) (*clientWrapper, error) {
func newClient(clientSet kclientset.Interface) *clientWrapper {
return &clientWrapper{
- clientset: clientSet,
- factoriesSecret: make(map[string]kinformers.SharedInformerFactory),
- factoriesIngress: make(map[string]kinformers.SharedInformerFactory),
- factoriesKube: make(map[string]kinformers.SharedInformerFactory),
+ clientset: clientSet,
+ factoriesSecret: make(map[string]kinformers.SharedInformerFactory),
+ factoriesConfigMap: make(map[string]kinformers.SharedInformerFactory),
+ factoriesIngress: make(map[string]kinformers.SharedInformerFactory),
+ factoriesKube: make(map[string]kinformers.SharedInformerFactory),
}
}
@@ -182,12 +184,20 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
return nil, err
}
c.factoriesSecret[ns] = factorySecret
+
+ factoryConfigMap := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
+ _, err = factoryConfigMap.Core().V1().ConfigMaps().Informer().AddEventHandler(eventHandler)
+ if err != nil {
+ return nil, err
+ }
+ c.factoriesConfigMap[ns] = factoryConfigMap
}
for _, ns := range c.watchedNamespaces {
c.factoriesIngress[ns].Start(stopCh)
c.factoriesKube[ns].Start(stopCh)
c.factoriesSecret[ns].Start(stopCh)
+ c.factoriesConfigMap[ns].Start(stopCh)
}
for _, ns := range c.watchedNamespaces {
@@ -208,6 +218,12 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
}
}
+
+ for t, ok := range c.factoriesConfigMap[ns].WaitForCacheSync(stopCh) {
+ if !ok {
+ return nil, fmt.Errorf("timed out waiting for controller caches to sync %s in namespace %q", t.String(), ns)
+ }
+ }
}
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
@@ -314,6 +330,15 @@ func (c *clientWrapper) GetEndpointSlicesForService(namespace, serviceName strin
return c.factoriesKube[c.lookupNamespace(namespace)].Discovery().V1().EndpointSlices().Lister().EndpointSlices(namespace).List(serviceSelector)
}
+// GetConfigMap returns the named configMap from the given namespace.
+func (c *clientWrapper) GetConfigMap(namespace, name string) (*corev1.ConfigMap, error) {
+ if !c.isWatchedNamespace(namespace) {
+ return nil, fmt.Errorf("failed to get configmap %s/%s: namespace is not within watched namespaces", namespace, name)
+ }
+
+ return c.factoriesConfigMap[c.lookupNamespace(namespace)].Core().V1().ConfigMaps().Lister().ConfigMaps(namespace).Get(name)
+}
+
// GetSecret returns the named secret from the given namespace.
func (c *clientWrapper) GetSecret(namespace, name string) (*corev1.Secret, error) {
if !c.isWatchedNamespace(namespace) {
diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-custom-headers.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-custom-headers.yml
new file mode 100644
index 000000000..19803a1e8
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-custom-headers.yml
@@ -0,0 +1,31 @@
+---
+kind: Ingress
+apiVersion: networking.k8s.io/v1
+metadata:
+ name: ingress-with-custom-headers
+ namespace: default
+ annotations:
+ nginx.ingress.kubernetes.io/custom-headers: default/custom-headers-configmap
+
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: whoami.localhost
+ http:
+ paths:
+ - path: /
+ pathType: Exact
+ backend:
+ service:
+ name: whoami
+ port:
+ number: 80
+
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+ name: custom-headers-configmap
+ namespace: default
+data:
+ X-Custom-Header: "some-random-string"
diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go
index 7495400fb..307daac2d 100644
--- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go
@@ -804,6 +804,40 @@ func (p *Provider) applyMiddlewares(namespace, routerKey string, ingressConfig i
applyUpstreamVhost(routerKey, ingressConfig, rt, conf)
+ if err := p.applyCustomHeaders(routerKey, ingressConfig, rt, conf); err != nil {
+ return fmt.Errorf("applying custom headers: %w", err)
+ }
+
+ return nil
+}
+
+func (p *Provider) applyCustomHeaders(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
+ customHeaders := ptr.Deref(ingressConfig.CustomHeaders, "")
+ if customHeaders == "" {
+ return nil
+ }
+
+ customHeadersParts := strings.Split(customHeaders, "/")
+ if len(customHeadersParts) != 2 {
+ return fmt.Errorf("invalid custom headers config map %q", customHeaders)
+ }
+
+ configMapNamespace := customHeadersParts[0]
+ configMapName := customHeadersParts[1]
+
+ configMap, err := p.k8sClient.GetConfigMap(configMapNamespace, configMapName)
+ if err != nil {
+ return fmt.Errorf("getting configMap %s: %w", customHeaders, err)
+ }
+
+ customHeadersMiddlewareName := routerName + "-custom-headers"
+ conf.HTTP.Middlewares[customHeadersMiddlewareName] = &dynamic.Middleware{
+ Headers: &dynamic.Headers{
+ CustomResponseHeaders: configMap.Data,
+ },
+ }
+ rt.Middlewares = append(rt.Middlewares, customHeadersMiddlewareName)
+
return nil
}
diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
index 0492cdf85..b2f1d3de4 100644
--- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
@@ -46,6 +46,58 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
+ {
+ desc: "Custom Headers",
+ paths: []string{
+ "services.yml",
+ "ingressclasses.yml",
+ "ingresses/11-ingress-with-custom-headers.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-custom-headers-rule-0-path-0": {
+ Rule: "Host(`whoami.localhost`) && Path(`/`)",
+ RuleSyntax: "default",
+ Middlewares: []string{"default-ingress-with-custom-headers-rule-0-path-0-custom-headers"},
+ Service: "default-whoami-80",
+ },
+ },
+ Middlewares: map[string]*dynamic.Middleware{
+ "default-ingress-with-custom-headers-rule-0-path-0-custom-headers": {
+ Headers: &dynamic.Headers{
+ CustomResponseHeaders: map[string]string{"X-Custom-Header": "some-random-string"},
+ },
+ },
+ },
+ 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: "Basic Auth",
paths: []string{