diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
index 07ba27e38..f06d3c772 100644
--- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
+++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
@@ -320,6 +320,7 @@ The following annotations are organized by category for easier navigation.
| Annotation | Limitations / Notes |
|-------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `nginx.ingress.kubernetes.io/app-root` | |
+| `nginx.ingress.kubernetes.io/from-to-www-redirect` | Doesn't support wildcard hosts. |
| `nginx.ingress.kubernetes.io/use-regex` | |
| `nginx.ingress.kubernetes.io/rewrite-target` | |
| `nginx.ingress.kubernetes.io/permanent-redirect` | Defaults to a 301 Moved Permanently status code. |
diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go
index 4425db6c4..4abb00e22 100644
--- a/pkg/provider/kubernetes/ingress-nginx/annotations.go
+++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go
@@ -36,6 +36,8 @@ type ingressConfig struct {
TemporalRedirect *string `annotation:"nginx.ingress.kubernetes.io/temporal-redirect"`
TemporalRedirectCode *int `annotation:"nginx.ingress.kubernetes.io/temporal-redirect-code"`
+ FromToWwwRedirect *bool `annotation:"nginx.ingress.kubernetes.io/from-to-www-redirect"`
+
Affinity *string `annotation:"nginx.ingress.kubernetes.io/affinity"`
SessionCookieName *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-name"`
SessionCookieSecure *bool `annotation:"nginx.ingress.kubernetes.io/session-cookie-secure"`
diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-host.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-host.yml
new file mode 100644
index 000000000..7f7803946
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-host.yml
@@ -0,0 +1,22 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-with-host
+ namespace: default
+ annotations:
+ nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
+
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: host.localhost
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: whoami
+ port:
+ number: 80
diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-www-host.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-www-host.yml
new file mode 100644
index 000000000..c02df3650
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-www-host.yml
@@ -0,0 +1,22 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-with-www-host
+ namespace: default
+ annotations:
+ nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
+
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: www.host.localhost
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: whoami
+ port:
+ number: 80
diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingresses-with-www-redirect.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingresses-with-www-redirect.yml
new file mode 100644
index 000000000..03fcb9758
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingresses-with-www-redirect.yml
@@ -0,0 +1,42 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-with-www-host
+ namespace: default
+ annotations:
+ nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
+
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: www.host.localhost
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: whoami
+ port:
+ number: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-with-host
+ namespace: default
+
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: host.localhost
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ 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 e2e7cebe2..a59707a19 100644
--- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go
@@ -266,6 +266,19 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
ingresses := p.k8sClient.ListIngresses()
+ hosts := make(map[string]bool)
+ for _, ing := range ingresses {
+ if !p.shouldProcessIngress(ing, ingressClasses) {
+ continue
+ }
+
+ for _, rule := range ing.Spec.Rules {
+ if !hosts[rule.Host] {
+ hosts[rule.Host] = true
+ }
+ }
+ }
+
uniqCerts := make(map[string]*tls.CertAndStores)
tlsOptions := make(map[string]tls.Options)
for _, ingress := range ingresses {
@@ -340,7 +353,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
Service: defaultBackendName,
}
- if err := p.applyMiddlewares(ingress.Namespace, defaultBackendName, "", ingressConfig, hasTLS, rt, conf); err != nil {
+ if err := p.applyMiddlewares(ingress.Namespace, defaultBackendName, "", "", hosts, ingressConfig, hasTLS, rt, conf); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@@ -358,7 +371,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
rtTLS.TLS.Options = clientAuthTLSOptionName
}
- if err := p.applyMiddlewares(ingress.Namespace, defaultBackendTLSName, "", ingressConfig, false, rtTLS, conf); err != nil {
+ if err := p.applyMiddlewares(ingress.Namespace, defaultBackendTLSName, "", "", hosts, ingressConfig, false, rtTLS, conf); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@@ -435,7 +448,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
Service: key,
}
- if err := p.applyMiddlewares(ingress.Namespace, key, "", ingressConfig, hasTLS, rt, conf); err != nil {
+ if err := p.applyMiddlewares(ingress.Namespace, key, "", "", hosts, ingressConfig, hasTLS, rt, conf); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@@ -452,7 +465,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
rtTLS.TLS.Options = clientAuthTLSOptionName
}
- if err := p.applyMiddlewares(ingress.Namespace, key+"-tls", "", ingressConfig, false, rtTLS, conf); err != nil {
+ if err := p.applyMiddlewares(ingress.Namespace, key+"-tls", "", "", hosts, ingressConfig, false, rtTLS, conf); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@@ -521,7 +534,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
conf.HTTP.ServersTransports[namedServersTransport.Name] = namedServersTransport.ServersTransport
}
- if err := p.applyMiddlewares(ingress.Namespace, routerKey, pa.Path, ingressConfig, hasTLS, rt, conf); err != nil {
+ if err := p.applyMiddlewares(ingress.Namespace, routerKey, pa.Path, rule.Host, hosts, ingressConfig, hasTLS, rt, conf); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
}
@@ -830,8 +843,10 @@ func (p *Provider) loadCertificates(ctx context.Context, ingress *netv1.Ingress,
return nil
}
-func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error {
+func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath, ruleHost string, hosts map[string]bool, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error {
applyAppRootConfiguration(routerKey, ingressConfig, rt, conf)
+ applyFromToWwwRedirect(hosts, ruleHost, routerKey, ingressConfig, rt, conf)
+ applyRedirect(routerKey, ingressConfig, rt, conf)
// Apply SSL redirect is mandatory to be applied after all other middlewares.
// TODO: check how to remove this, and create the HTTP router elsewhere.
@@ -851,8 +866,6 @@ func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath string, ingre
applyRewriteTargetConfiguration(rulePath, routerKey, ingressConfig, rt, conf)
- applyRedirect(routerKey, ingressConfig, rt, conf)
-
applyUpstreamVhost(routerKey, ingressConfig, rt, conf)
if err := p.applyCustomHeaders(routerKey, ingressConfig, rt, conf); err != nil {
@@ -966,6 +979,49 @@ func applyAppRootConfiguration(routerName string, ingressConfig ingressConfig, r
rt.Middlewares = append(rt.Middlewares, appRootMiddlewareName)
}
+func applyFromToWwwRedirect(hosts map[string]bool, ruleHost, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) {
+ if ingressConfig.FromToWwwRedirect == nil || !*ingressConfig.FromToWwwRedirect {
+ return
+ }
+
+ wwwType := strings.HasPrefix(ruleHost, "www.")
+ wildcardType := strings.HasPrefix(ruleHost, "*.")
+ bypass := wwwType && hosts[strings.TrimPrefix(ruleHost, "www.")] || !wwwType && hosts["www."+ruleHost] || wildcardType
+
+ if bypass {
+ // Wildcard host not compatible with this annotation. (limitation)
+ // hosts already configured for www. and normal hosts.
+ return
+ }
+
+ newRule := fmt.Sprintf("Host(`www.%s`)", ruleHost)
+ if wwwType {
+ // if current ingress host is www.example.com, redirect from example.com => www.example.com
+ host := strings.TrimPrefix(ruleHost, "www.")
+ newRule = fmt.Sprintf("Host(`%s`)", host)
+ }
+
+ fromToWwwRedirectMiddlewareName := routerName + "-from-to-www-redirect"
+ conf.HTTP.Middlewares[fromToWwwRedirectMiddlewareName] = &dynamic.Middleware{
+ RedirectRegex: &dynamic.RedirectRegex{
+ Regex: `(https?)://[^/]+:([0-9]+)/(.*)`,
+ Replacement: fmt.Sprintf("$1://%s:$2/$3", ruleHost),
+ Permanent: true,
+ },
+ }
+
+ wwwRedirectRouter := &dynamic.Router{
+ Rule: newRule,
+ EntryPoints: rt.EntryPoints,
+ Priority: rt.Priority,
+ // "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
+ RuleSyntax: "default",
+ Middlewares: []string{fromToWwwRedirectMiddlewareName},
+ Service: rt.Service,
+ }
+ conf.HTTP.Routers[routerName+"-from-to-www-redirect"] = wwwRedirectRouter
+}
+
func (p *Provider) applyBasicAuthConfiguration(namespace, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
if ingressConfig.AuthType == nil {
return nil
diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
index 68b497cc6..24c799a7a 100644
--- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
@@ -1047,6 +1047,224 @@ func TestLoadIngresses(t *testing.T) {
},
},
},
+ {
+ desc: "From To WWW Redirect - www host",
+ paths: []string{
+ "services.yml",
+ "ingressclasses.yml",
+ "ingresses/ingress-with-www-host.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-www-host-rule-0-path-0": {
+ Rule: "Host(`www.host.localhost`) && PathPrefix(`/`)",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-www-host-whoami-80",
+ },
+ "default-ingress-with-www-host-rule-0-path-0-from-to-www-redirect": {
+ Rule: "Host(`host.localhost`)",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-www-host-whoami-80",
+ Middlewares: []string{"default-ingress-with-www-host-rule-0-path-0-from-to-www-redirect"},
+ },
+ },
+ Middlewares: map[string]*dynamic.Middleware{
+ "default-ingress-with-www-host-rule-0-path-0-from-to-www-redirect": {
+ RedirectRegex: &dynamic.RedirectRegex{
+ Regex: `(https?)://[^/]+:([0-9]+)/(.*)`,
+ Replacement: "$1://www.host.localhost:$2/$3",
+ Permanent: true,
+ },
+ },
+ },
+ Services: map[string]*dynamic.Service{
+ "default-ingress-with-www-host-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),
+ ServersTransport: "default-ingress-with-www-host",
+ ResponseForwarding: &dynamic.ResponseForwarding{
+ FlushInterval: dynamic.DefaultFlushInterval,
+ },
+ },
+ },
+ },
+ ServersTransports: map[string]*dynamic.ServersTransport{
+ "default-ingress-with-www-host": {
+ ForwardingTimeouts: &dynamic.ForwardingTimeouts{
+ DialTimeout: ptypes.Duration(60 * time.Second),
+ },
+ },
+ },
+ },
+ TLS: &dynamic.TLSConfiguration{
+ Options: map[string]tls.Options{},
+ },
+ },
+ },
+ {
+ desc: "From To WWW Redirect - host",
+ paths: []string{
+ "services.yml",
+ "ingressclasses.yml",
+ "ingresses/ingress-with-host.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-host-rule-0-path-0": {
+ Rule: "Host(`host.localhost`) && PathPrefix(`/`)",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-host-whoami-80",
+ },
+ "default-ingress-with-host-rule-0-path-0-from-to-www-redirect": {
+ Rule: "Host(`www.host.localhost`)",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-host-whoami-80",
+ Middlewares: []string{"default-ingress-with-host-rule-0-path-0-from-to-www-redirect"},
+ },
+ },
+ Middlewares: map[string]*dynamic.Middleware{
+ "default-ingress-with-host-rule-0-path-0-from-to-www-redirect": {
+ RedirectRegex: &dynamic.RedirectRegex{
+ Regex: `(https?)://[^/]+:([0-9]+)/(.*)`,
+ Replacement: "$1://host.localhost:$2/$3",
+ Permanent: true,
+ },
+ },
+ },
+ Services: map[string]*dynamic.Service{
+ "default-ingress-with-host-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),
+ ServersTransport: "default-ingress-with-host",
+ ResponseForwarding: &dynamic.ResponseForwarding{
+ FlushInterval: dynamic.DefaultFlushInterval,
+ },
+ },
+ },
+ },
+ ServersTransports: map[string]*dynamic.ServersTransport{
+ "default-ingress-with-host": {
+ ForwardingTimeouts: &dynamic.ForwardingTimeouts{
+ DialTimeout: ptypes.Duration(60 * time.Second),
+ },
+ },
+ },
+ },
+ TLS: &dynamic.TLSConfiguration{
+ Options: map[string]tls.Options{},
+ },
+ },
+ },
+ {
+ desc: "From To WWW Redirect - multiple ingresses",
+ paths: []string{
+ "services.yml",
+ "ingressclasses.yml",
+ "ingresses/ingresses-with-www-redirect.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-host-rule-0-path-0": {
+ Rule: "Host(`host.localhost`) && PathPrefix(`/`)",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-host-whoami-80",
+ },
+ "default-ingress-with-www-host-rule-0-path-0": {
+ Rule: "Host(`www.host.localhost`) && PathPrefix(`/`)",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-www-host-whoami-80",
+ },
+ },
+ Middlewares: map[string]*dynamic.Middleware{},
+ Services: map[string]*dynamic.Service{
+ "default-ingress-with-host-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),
+ ServersTransport: "default-ingress-with-host",
+ ResponseForwarding: &dynamic.ResponseForwarding{
+ FlushInterval: dynamic.DefaultFlushInterval,
+ },
+ },
+ },
+ "default-ingress-with-www-host-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),
+ ServersTransport: "default-ingress-with-www-host",
+ ResponseForwarding: &dynamic.ResponseForwarding{
+ FlushInterval: dynamic.DefaultFlushInterval,
+ },
+ },
+ },
+ },
+ ServersTransports: map[string]*dynamic.ServersTransport{
+ "default-ingress-with-www-host": {
+ ForwardingTimeouts: &dynamic.ForwardingTimeouts{
+ DialTimeout: ptypes.Duration(60 * time.Second),
+ },
+ },
+ "default-ingress-with-host": {
+ ForwardingTimeouts: &dynamic.ForwardingTimeouts{
+ DialTimeout: ptypes.Duration(60 * time.Second),
+ },
+ },
+ },
+ },
+ TLS: &dynamic.TLSConfiguration{
+ Options: map[string]tls.Options{},
+ },
+ },
+ },
{
desc: "Default Backend",
defaultBackendServiceName: "whoami",