From 5d3dc3348ef0aa533454078c84f708272aecc15d Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Fri, 9 Jul 2021 14:22:13 +0200 Subject: [PATCH 1/7] accesslog: multiple times the same header name. --- pkg/middlewares/accesslog/logger.go | 3 ++- pkg/middlewares/accesslog/logger_test.go | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index b0e273a1f..db45aa3cf 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "sync" "sync/atomic" "time" @@ -344,7 +345,7 @@ func (h *Handler) redactHeaders(headers http.Header, fields logrus.Fields, prefi for k := range headers { v := h.config.Fields.KeepHeader(k) if v == types.AccessLogKeep { - fields[prefix+k] = headers.Get(k) + fields[prefix+k] = strings.Join(headers.Values(k), ",") } else if v == types.AccessLogRedact { fields[prefix+k] = "REDACTED" } diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 3f37b8312..5d75e8aaa 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -114,7 +114,7 @@ func lineCount(t *testing.T, fileName string) int { } func TestLoggerHeaderFields(t *testing.T) { - expectedValue := "expectedValue" + expectedValues := []string{"AAA", "BBB"} testCases := []struct { desc string @@ -191,7 +191,10 @@ func TestLoggerHeaderFields(t *testing.T) { Path: testPath, }, } - req.Header.Set(test.header, expectedValue) + + for _, s := range expectedValues { + req.Header.Add(test.header, s) + } logger.ServeHTTP(httptest.NewRecorder(), req, http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) { writer.WriteHeader(http.StatusOK) @@ -201,9 +204,9 @@ func TestLoggerHeaderFields(t *testing.T) { require.NoError(t, err) if test.expected == types.AccessLogDrop { - assert.NotContains(t, string(logData), expectedValue) + assert.NotContains(t, string(logData), strings.Join(expectedValues, ",")) } else { - assert.Contains(t, string(logData), expectedValue) + assert.Contains(t, string(logData), strings.Join(expectedValues, ",")) } }) } From 14499cd6e5fcc4dff83658dc2aea152b6aa1ac53 Mon Sep 17 00:00:00 2001 From: Romain Date: Mon, 12 Jul 2021 18:32:10 +0200 Subject: [PATCH 2/7] Fix: Add dedicated integration tests targets for CI --- Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Makefile b/Makefile index fb0d20c8f..ec5ee86df 100644 --- a/Makefile +++ b/Makefile @@ -96,6 +96,15 @@ test-integration: $(PRE_TARGET) $(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary test-integration TEST_HOST=1 ./script/make.sh test-integration +## Run the container integration tests +test-integration-container: $(PRE_TARGET) + $(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary test-integration + +## Run the host integration tests +test-integration-host: $(PRE_TARGET) + $(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK),TEST_CONTAINER=1) ./script/make.sh generate binary + TEST_HOST=1 ./script/make.sh test-integration + ## Validate code and docs validate-files: $(PRE_TARGET) $(if $(PRE_TARGET),$(DOCKER_RUN_TRAEFIK)) ./script/make.sh generate validate-lint validate-misspell From 3072354ca5d61e85d946f0e3839e9d09c5ee24cb Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 13 Jul 2021 02:48:05 -0600 Subject: [PATCH 3/7] Disable Cross-Namespace by default for IngressRoute provider --- Makefile | 2 +- docs/content/migration/v2.md | 6 ++++++ docs/content/providers/kubernetes-crd.md | 14 +++++--------- .../reference/static-configuration/cli-ref.md | 2 +- .../reference/static-configuration/env-ref.md | 2 +- pkg/provider/kubernetes/crd/kubernetes.go | 13 ++++--------- pkg/provider/kubernetes/crd/kubernetes_http.go | 2 +- pkg/provider/kubernetes/crd/kubernetes_test.go | 12 ++++-------- 8 files changed, 23 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index ec5ee86df..fea41bcfd 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ shell: build-dev-image docs: make -C ./docs docs -## Serve the documentation site localy +## Serve the documentation site locally docs-serve: make -C ./docs docs-serve diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index a34fed323..4d27d856e 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -364,3 +364,9 @@ For more information, please read the [HTTP routers rule](../routing/routers/ind ### Tracing Span In `v2.4.9`, we changed span error to log only server errors (>= 500). + +## v2.4.9 to v2.4.10 + +### K8S CrossNamespace + +In `v2.4.10`, the default value for `allowCrossNamespace` has been changed to `false`. diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index 814c4fe43..b9e89a4d9 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -260,29 +260,25 @@ providers: ### `allowCrossNamespace` -_Optional, Default: true_ +_Optional, Default: false_ -If the parameter is set to `false`, IngressRoutes are not able to reference any resources in other namespaces than theirs. - -!!! warning "Deprecation" - - Please note that the default value for this option will be set to `false` in a future version. +If the parameter is set to `true`, IngressRoutes are able to reference resources in other namespaces than theirs. ```yaml tab="File (YAML)" providers: kubernetesCRD: - allowCrossNamespace: false + allowCrossNamespace: true # ... ``` ```toml tab="File (TOML)" [providers.kubernetesCRD] - allowCrossNamespace = false + allowCrossNamespace = true # ... ``` ```bash tab="CLI" ---providers.kubernetescrd.allowCrossNamespace=false +--providers.kubernetescrd.allowCrossNamespace=true ``` ## Full Example diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 4208eef6e..5afe6e696 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -556,7 +556,7 @@ TLS key Enable Kubernetes backend with default settings. (Default: ```false```) `--providers.kubernetescrd.allowcrossnamespace`: -Allow cross namespace resource reference. (Default: ```true```) +Allow cross namespace resource reference. (Default: ```false```) `--providers.kubernetescrd.certauthfilepath`: Kubernetes certificate authority file path (not needed for in-cluster client). diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 9ea6fa2a9..55c3d3f50 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -556,7 +556,7 @@ TLS key Enable Kubernetes backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`: -Allow cross namespace resource reference. (Default: ```true```) +Allow cross namespace resource reference. (Default: ```false```) `TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`: Kubernetes certificate authority file path (not needed for in-cluster client). diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index ccda4a173..7e107fed4 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -42,18 +42,13 @@ type Provider struct { Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"` CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` - AllowCrossNamespace *bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"` + AllowCrossNamespace bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"` LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` lastConfiguration safe.Safe } -// SetDefaults sets the default values. -func (p *Provider) SetDefaults() { - p.AllowCrossNamespace = func(b bool) *bool { return &b }(true) -} - func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { _, err := labels.Parse(p.LabelSelector) if err != nil { @@ -103,7 +98,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return err } - if p.AllowCrossNamespace == nil || *p.AllowCrossNamespace { + if p.AllowCrossNamespace { logger.Warn("Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)") } @@ -826,7 +821,7 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s return eventsChanBuffered } -func isNamespaceAllowed(allowCrossNamespace *bool, parentNamespace, namespace string) bool { +func isNamespaceAllowed(allowCrossNamespace bool, parentNamespace, namespace string) bool { // If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references. - return allowCrossNamespace == nil || *allowCrossNamespace || parentNamespace == namespace + return allowCrossNamespace || parentNamespace == namespace } diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 57a16cab5..b110e9467 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -173,7 +173,7 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str type configBuilder struct { client Client - allowCrossNamespace *bool + allowCrossNamespace bool } // buildTraefikService creates the configuration for the traefik service defined in tService, diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 9b2fa2e60..0c1c15637 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -1153,8 +1153,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass} - p.SetDefaults() + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -3338,8 +3337,7 @@ func TestLoadIngressRoutes(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass} - p.SetDefaults() + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -3655,8 +3653,7 @@ func TestLoadIngressRouteUDPs(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass} - p.SetDefaults() + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -4439,9 +4436,8 @@ func TestCrossNamespace(t *testing.T) { } p := Provider{} - p.SetDefaults() - p.AllowCrossNamespace = func(b bool) *bool { return &b }(test.allowCrossNamespace) + p.AllowCrossNamespace = test.allowCrossNamespace conf := p.loadConfigurationFromCRD(context.Background(), client) assert.Equal(t, test.expected, conf) }) From 10ab39c33b328cc8ade365c0fd9742e6172f848a Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 13 Jul 2021 04:28:07 -0600 Subject: [PATCH 4/7] Add *headers.responseModifier CloseNotify() --- pkg/middlewares/headers/responsewriter.go | 63 ++++++++++++----------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/pkg/middlewares/headers/responsewriter.go b/pkg/middlewares/headers/responsewriter.go index a50643eae..39a171dc2 100644 --- a/pkg/middlewares/headers/responsewriter.go +++ b/pkg/middlewares/headers/responsewriter.go @@ -10,8 +10,8 @@ import ( ) type responseModifier struct { - r *http.Request - w http.ResponseWriter + req *http.Request + rw http.ResponseWriter headersSent bool // whether headers have already been sent code int // status code, must default to 200 @@ -24,71 +24,76 @@ type responseModifier struct { // modifier can be nil. func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) *responseModifier { return &responseModifier{ - r: r, - w: w, + req: r, + rw: w, modifier: modifier, code: http.StatusOK, } } -func (w *responseModifier) WriteHeader(code int) { - if w.headersSent { +func (r *responseModifier) WriteHeader(code int) { + if r.headersSent { return } defer func() { - w.code = code - w.headersSent = true + r.code = code + r.headersSent = true }() - if w.modifier == nil || w.modified { - w.w.WriteHeader(code) + if r.modifier == nil || r.modified { + r.rw.WriteHeader(code) return } resp := http.Response{ - Header: w.w.Header(), - Request: w.r, + Header: r.rw.Header(), + Request: r.req, } - if err := w.modifier(&resp); err != nil { - w.modifierErr = err + if err := r.modifier(&resp); err != nil { + r.modifierErr = err // we are propagating when we are called in Write, but we're logging anyway, // because we could be called from another place which does not take care of // checking w.modifierErr. log.WithoutContext().Errorf("Error when applying response modifier: %v", err) - w.w.WriteHeader(http.StatusInternalServerError) + r.rw.WriteHeader(http.StatusInternalServerError) return } - w.modified = true - w.w.WriteHeader(code) + r.modified = true + r.rw.WriteHeader(code) } -func (w *responseModifier) Header() http.Header { - return w.w.Header() +func (r *responseModifier) Header() http.Header { + return r.rw.Header() } -func (w *responseModifier) Write(b []byte) (int, error) { - w.WriteHeader(w.code) - if w.modifierErr != nil { - return 0, w.modifierErr +func (r *responseModifier) Write(b []byte) (int, error) { + r.WriteHeader(r.code) + if r.modifierErr != nil { + return 0, r.modifierErr } - return w.w.Write(b) + return r.rw.Write(b) } // Hijack hijacks the connection. -func (w *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) { - if h, ok := w.w.(http.Hijacker); ok { +func (r *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if h, ok := r.rw.(http.Hijacker); ok { return h.Hijack() } - return nil, nil, fmt.Errorf("not a hijacker: %T", w.w) + return nil, nil, fmt.Errorf("not a hijacker: %T", r.rw) } // Flush sends any buffered data to the client. -func (w *responseModifier) Flush() { - if flusher, ok := w.w.(http.Flusher); ok { +func (r *responseModifier) Flush() { + if flusher, ok := r.rw.(http.Flusher); ok { flusher.Flush() } } + +// CloseNotify implements http.CloseNotifier. +func (r *responseModifier) CloseNotify() <-chan bool { + return r.rw.(http.CloseNotifier).CloseNotify() +} From 3c1ed0d9b275c19a9469a56966f01599284cbe7a Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 13 Jul 2021 04:54:09 -0600 Subject: [PATCH 5/7] Disable ExternalName Services by default on Kubernetes providers --- docs/content/migration/v2.md | 5 + docs/content/providers/kubernetes-crd.md | 23 ++ docs/content/providers/kubernetes-ingress.md | 23 ++ .../reference/static-configuration/cli-ref.md | 6 + .../reference/static-configuration/env-ref.md | 6 + integration/fixtures/k8s_crd.toml | 1 + .../kubernetes/crd/fixtures/udp/services.yml | 13 + .../udp/with_externalname_service.yml | 14 + pkg/provider/kubernetes/crd/kubernetes.go | 27 +- .../kubernetes/crd/kubernetes_http.go | 11 +- pkg/provider/kubernetes/crd/kubernetes_tcp.go | 8 +- .../kubernetes/crd/kubernetes_test.go | 291 +++++++++++++++++- pkg/provider/kubernetes/crd/kubernetes_udp.go | 8 +- ...endpoints-externalname-enabled_ingress.yml | 14 + ...endpoints-externalname-enabled_service.yml | 13 + ...vice-with-externalName-enabled_ingress.yml | 15 + ...vice-with-externalName-enabled_service.yml | 13 + pkg/provider/kubernetes/ingress/kubernetes.go | 33 +- .../kubernetes/ingress/kubernetes_test.go | 215 +++++++++---- 19 files changed, 637 insertions(+), 102 deletions(-) create mode 100644 pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_service.yml diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 4d27d856e..2e7892296 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -370,3 +370,8 @@ In `v2.4.9`, we changed span error to log only server errors (>= 500). ### K8S CrossNamespace In `v2.4.10`, the default value for `allowCrossNamespace` has been changed to `false`. + +### K8S ExternalName Service + +In `v2.4.10`, by default, it is no longer authorized to reference Kubernetes ExternalName services. +To allow it, the `allowExternalNameServices` option should be set to `true`. diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index b9e89a4d9..860c82ec5 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -281,6 +281,29 @@ providers: --providers.kubernetescrd.allowCrossNamespace=true ``` +### `allowExternalNameServices` + +_Optional, Default: false_ + +If the parameter is set to `true`, IngressRoutes are able to reference ExternalName services. + +```yaml tab="File (YAML)" +providers: + kubernetesCRD: + allowExternalNameServices: true + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesCRD] + allowExternalNameServices = true + # ... +``` + +```bash tab="CLI" +--providers.kubernetescrd.allowexternalnameservices=true +``` + ## Full Example For additional information, refer to the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt. diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index c2be26d1f..a4c0f1822 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -375,6 +375,29 @@ providers: --providers.kubernetesingress.throttleDuration=10s ``` +### `allowExternalNameServices` + +_Optional, Default: false_ + +If the parameter is set to `true`, Ingresses are able to reference ExternalName services. + +```yaml tab="File (YAML)" +providers: + kubernetesIngress: + allowExternalNameServices: true + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesIngress] + allowExternalNameServices = true + # ... +``` + +```bash tab="CLI" +--providers.kubernetesingress.allowexternalnameservices=true +``` + ### Further To learn more about the various aspects of the Ingress specification that Traefik supports, diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 5afe6e696..966d49ae7 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -558,6 +558,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```) `--providers.kubernetescrd.allowcrossnamespace`: Allow cross namespace resource reference. (Default: ```false```) +`--providers.kubernetescrd.allowexternalnameservices`: +Allow ExternalName services. (Default: ```false```) + `--providers.kubernetescrd.certauthfilepath`: Kubernetes certificate authority file path (not needed for in-cluster client). @@ -603,6 +606,9 @@ Kubernetes bearer token (not needed for in-cluster client). `--providers.kubernetesingress`: Enable Kubernetes backend with default settings. (Default: ```false```) +`--providers.kubernetesingress.allowexternalnameservices`: +Allow ExternalName services. (Default: ```false```) + `--providers.kubernetesingress.certauthfilepath`: Kubernetes certificate authority file path (not needed for in-cluster client). diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 55c3d3f50..3f2f30f00 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -558,6 +558,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`: Allow cross namespace resource reference. (Default: ```false```) +`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWEXTERNALNAMESERVICES`: +Allow ExternalName services. (Default: ```false```) + `TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`: Kubernetes certificate authority file path (not needed for in-cluster client). @@ -603,6 +606,9 @@ Kubernetes bearer token (not needed for in-cluster client). `TRAEFIK_PROVIDERS_KUBERNETESINGRESS`: Enable Kubernetes backend with default settings. (Default: ```false```) +`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEXTERNALNAMESERVICES`: +Allow ExternalName services. (Default: ```false```) + `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`: Kubernetes certificate authority file path (not needed for in-cluster client). diff --git a/integration/fixtures/k8s_crd.toml b/integration/fixtures/k8s_crd.toml index 3015b6093..86e85ee91 100644 --- a/integration/fixtures/k8s_crd.toml +++ b/integration/fixtures/k8s_crd.toml @@ -17,3 +17,4 @@ [providers.kubernetesCRD] allowCrossNamespace = false + allowExternalNameServices = true diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml index 887ddc385..50450c0bf 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml @@ -160,3 +160,16 @@ subsets: ports: - name: myapp port: 8000 +--- +kind: Service +apiVersion: v1 +metadata: + name: external.service.with.port + namespace: default +spec: + externalName: external.domain + type: ExternalName + ports: + - name: http + port: 80 + diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml new file mode 100644 index 000000000..962178ed5 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml @@ -0,0 +1,14 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteUDP +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - services: + - name: external.service.with.port + port: 80 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 7e107fed4..03f0733e4 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -38,15 +38,16 @@ const ( // Provider holds configurations of the provider. type Provider struct { - Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` - Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"` - CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` - Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` - AllowCrossNamespace bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"` - LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` - IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` - ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` - lastConfiguration safe.Safe + Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` + Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"` + CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` + Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` + AllowCrossNamespace bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"` + AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"` + LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` + IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` + ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` + lastConfiguration safe.Safe } func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) { @@ -102,6 +103,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. logger.Warn("Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)") } + if p.AllowExternalNameServices { + logger.Warn("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)") + } + pool.GoCtx(func(ctxPool context.Context) { operation := func() error { eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done()) @@ -240,7 +245,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) } } - cb := configBuilder{client, p.AllowCrossNamespace} + cb := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices} for _, service := range client.GetTraefikServices() { err := cb.buildTraefikService(ctx, service, conf.HTTP.Services) @@ -360,7 +365,7 @@ func (p *Provider) createErrorPageMiddleware(client Client, namespace string, er Query: errorPage.Query, } - balancerServerHTTP, err := configBuilder{client, p.AllowCrossNamespace}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) + balancerServerHTTP, err := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec) if err != nil { return nil, nil, err } diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index b110e9467..b179082e1 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -49,7 +49,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli ingressName = ingressRoute.GenerateName } - cb := configBuilder{client, p.AllowCrossNamespace} + cb := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices} for _, route := range ingressRoute.Spec.Routes { if route.Kind != "Rule" { @@ -172,8 +172,9 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str } type configBuilder struct { - client Client - allowCrossNamespace bool + client Client + allowCrossNamespace bool + allowExternalNameServices bool } // buildTraefikService creates the configuration for the traefik service defined in tService, @@ -322,6 +323,10 @@ func (c configBuilder) loadServers(parentNamespace string, svc v1alpha1.LoadBala var servers []dynamic.Server if service.Spec.Type == corev1.ServiceTypeExternalName { + if !c.allowExternalNameServices { + return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, sanitizedName) + } + protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port) if err != nil { return nil, err diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index fe84dffbe..f8d9deb0d 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -135,7 +135,7 @@ func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace st ns = service.Namespace } - servers, err := loadTCPServers(client, ns, service) + servers, err := p.loadTCPServers(client, ns, service) if err != nil { return nil, err } @@ -162,7 +162,7 @@ func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace st return tcpService, nil } -func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) { +func (p *Provider) loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) { service, exists, err := client.GetService(namespace, svc.Name) if err != nil { return nil, err @@ -172,6 +172,10 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([ return nil, errors.New("service not found") } + if service.Spec.Type == corev1.ServiceTypeExternalName && !p.AllowExternalNameServices { + return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, svc.Name) + } + svcPort, err := getServicePort(service, svc.Port) if err != nil { return nil, err diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 0c1c15637..21b486feb 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -1153,7 +1153,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true} + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -3337,7 +3337,7 @@ func TestLoadIngressRoutes(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true} + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -4435,9 +4435,292 @@ func TestCrossNamespace(t *testing.T) { <-eventCh } - p := Provider{} + p := Provider{AllowCrossNamespace: test.allowCrossNamespace} + + conf := p.loadConfigurationFromCRD(context.Background(), client) + assert.Equal(t, test.expected, conf) + }) + } +} + +func TestExternalNameService(t *testing.T) { + testCases := []struct { + desc string + allowExternalNameService bool + ingressClass string + paths []string + expected *dynamic.Configuration + }{ + { + desc: "Empty", + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "HTTP ExternalName services allowed", + paths: []string{"services.yml", "with_externalname_with_http.yml"}, + allowExternalNameService: true, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{ + "default-test-route-6f97418635c7e18853da": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-6f97418635c7e18853da", + Rule: "Host(`foo.com`)", + Priority: 0, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-6f97418635c7e18853da": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://external.domain:80", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "HTTP Externalname services disallowed", + paths: []string{"services.yml", "with_externalname_with_http.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "TCP ExternalName services allowed", + paths: []string{"tcp/services.yml", "tcp/with_externalname_with_port.yml"}, + allowExternalNameService: true, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{ + "default-test.route-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + }, + Services: map[string]*dynamic.TCPService{ + "default-test.route-fdd3e9338e47a45efefc": { + LoadBalancer: &dynamic.TCPServersLoadBalancer{ + Servers: []dynamic.TCPServer{ + { + Address: "external.domain:80", + Port: "", + }, + }, + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "TCP ExternalName services disallowed", + paths: []string{"tcp/services.yml", "tcp/with_externalname_with_port.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + // The router that references the invalid service will be discarded. + Routers: map[string]*dynamic.TCPRouter{ + "default-test.route-fdd3e9338e47a45efefc": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-fdd3e9338e47a45efefc", + Rule: "HostSNI(`foo.com`)", + }, + }, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "UDP ExternalName services allowed", + paths: []string{"udp/services.yml", "udp/with_externalname_service.yml"}, + allowExternalNameService: true, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "default-test.route-0": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-0", + }, + }, + Services: map[string]*dynamic.UDPService{ + "default-test.route-0": { + LoadBalancer: &dynamic.UDPServersLoadBalancer{ + Servers: []dynamic.UDPServer{ + { + Address: "external.domain:80", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "UDP ExternalName service disallowed", + paths: []string{"udp/services.yml", "udp/with_externalname_service.yml"}, + expected: &dynamic.Configuration{ + // The router that references the invalid service will be discarded. + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{ + "default-test.route-0": { + EntryPoints: []string{"foo"}, + Service: "default-test.route-0", + }, + }, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + ServersTransports: map[string]*dynamic.ServersTransport{}, + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var k8sObjects []runtime.Object + var crdObjects []runtime.Object + for _, path := range test.paths { + yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path)) + if err != nil { + panic(err) + } + + objects := k8s.MustParseYaml(yamlContent) + for _, obj := range objects { + switch o := obj.(type) { + case *corev1.Service, *corev1.Endpoints, *corev1.Secret: + k8sObjects = append(k8sObjects, o) + case *v1alpha1.IngressRoute: + crdObjects = append(crdObjects, o) + case *v1alpha1.IngressRouteTCP: + crdObjects = append(crdObjects, o) + case *v1alpha1.IngressRouteUDP: + crdObjects = append(crdObjects, o) + case *v1alpha1.Middleware: + crdObjects = append(crdObjects, o) + case *v1alpha1.TraefikService: + crdObjects = append(crdObjects, o) + case *v1alpha1.TLSOption: + crdObjects = append(crdObjects, o) + case *v1alpha1.TLSStore: + crdObjects = append(crdObjects, o) + default: + } + } + } + + kubeClient := kubefake.NewSimpleClientset(k8sObjects...) + crdClient := crdfake.NewSimpleClientset(crdObjects...) + + client := newClientImpl(kubeClient, crdClient) + + stopCh := make(chan struct{}) + + eventCh, err := client.WatchAll([]string{"default", "cross-ns"}, stopCh) + require.NoError(t, err) + + if k8sObjects != nil || crdObjects != nil { + // just wait for the first event + <-eventCh + } + + p := Provider{AllowExternalNameServices: test.allowExternalNameService} - p.AllowCrossNamespace = test.allowCrossNamespace conf := p.loadConfigurationFromCRD(context.Background(), client) assert.Equal(t, test.expected, conf) }) diff --git a/pkg/provider/kubernetes/crd/kubernetes_udp.go b/pkg/provider/kubernetes/crd/kubernetes_udp.go index 0346084a2..e28e56e02 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_udp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_udp.go @@ -87,7 +87,7 @@ func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace st ns = service.Namespace } - servers, err := loadUDPServers(client, ns, service) + servers, err := p.loadUDPServers(client, ns, service) if err != nil { return nil, err } @@ -101,7 +101,7 @@ func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace st return udpService, nil } -func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([]dynamic.UDPServer, error) { +func (p *Provider) loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([]dynamic.UDPServer, error) { service, exists, err := client.GetService(namespace, svc.Name) if err != nil { return nil, err @@ -111,6 +111,10 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([ return nil, errors.New("service not found") } + if service.Spec.Type == corev1.ServiceTypeExternalName && !p.AllowExternalNameServices { + return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, svc.Name) + } + var portSpec *corev1.ServicePort for _, p := range service.Spec.Ports { p := p diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_ingress.yml new file mode 100644 index 000000000..e41dde42f --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_ingress.yml @@ -0,0 +1,14 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: example.com + namespace: testing + +spec: + rules: + - http: + paths: + - path: /foo + backend: + serviceName: service-foo + servicePort: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_service.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_service.yml new file mode 100644 index 000000000..67c193cfa --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_service.yml @@ -0,0 +1,13 @@ +--- +kind: Service +apiVersion: v1 +metadata: + name: service-foo + namespace: testing + +spec: + ports: + - name: http + port: 8080 + type: ExternalName + externalName: "2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b" diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_ingress.yml new file mode 100644 index 000000000..f9645ad09 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_ingress.yml @@ -0,0 +1,15 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1beta1 +metadata: + name: "" + namespace: testing + +spec: + rules: + - host: traefik.tchouk + http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_service.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_service.yml new file mode 100644 index 000000000..972e4cdbc --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_service.yml @@ -0,0 +1,13 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 8080 + clusterIP: 10.0.0.1 + type: ExternalName + externalName: traefik.wtf + diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index c828d48d1..3a4e03d0b 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -37,15 +37,16 @@ const ( // Provider holds configurations of the provider. type Provider struct { - Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` - Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"` - CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` - Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` - LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` - IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` - IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"` - ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` - lastConfiguration safe.Safe + Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` + Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"` + CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` + Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` + LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` + IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"` + IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"` + ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` + AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"` + lastConfiguration safe.Safe } // EndpointIngress holds the endpoint information for the Kubernetes provider. @@ -107,6 +108,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe. return err } + if p.AllowExternalNameServices { + logger.Warn("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)") + } + pool.GoCtx(func(ctxPool context.Context) { operation := func() error { eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done()) @@ -228,7 +233,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl continue } - service, err := loadService(client, ingress.Namespace, *ingress.Spec.Backend) + service, err := p.loadService(client, ingress.Namespace, *ingress.Spec.Backend) if err != nil { log.FromContext(ctx). WithField("serviceName", ingress.Spec.Backend.ServiceName). @@ -265,7 +270,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl } for _, pa := range rule.HTTP.Paths { - service, err := loadService(client, ingress.Namespace, pa.Backend) + service, err := p.loadService(client, ingress.Namespace, pa.Backend) if err != nil { log.FromContext(ctx). WithField("serviceName", pa.Backend.ServiceName). @@ -460,7 +465,7 @@ func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores return configs } -func loadService(client Client, namespace string, backend networkingv1beta1.IngressBackend) (*dynamic.Service, error) { +func (p *Provider) loadService(client Client, namespace string, backend networkingv1beta1.IngressBackend) (*dynamic.Service, error) { service, exists, err := client.GetService(namespace, backend.ServiceName) if err != nil { return nil, err @@ -470,6 +475,10 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr return nil, errors.New("service not found") } + if !p.AllowExternalNameServices && service.Spec.Type == corev1.ServiceTypeExternalName { + return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, backend.ServiceName) + } + var portName string var portSpec corev1.ServicePort var match bool diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 3b3509037..f005064d9 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -732,33 +732,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, - { - desc: "Ingress with service with externalName", - expected: &dynamic.Configuration{ - TCP: &dynamic.TCPConfiguration{}, - HTTP: &dynamic.HTTPConfiguration{ - Middlewares: map[string]*dynamic.Middleware{}, - Routers: map[string]*dynamic.Router{ - "testing-traefik-tchouk-bar": { - Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", - Service: "testing-service1-8080", - }, - }, - Services: map[string]*dynamic.Service{ - "testing-service1-8080": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - PassHostHeader: Bool(true), - Servers: []dynamic.Server{ - { - URL: "http://traefik.wtf:8080", - }, - }, - }, - }, - }, - }, - }, - }, { desc: "Ingress with port invalid for one service", expected: &dynamic.Configuration{ @@ -786,47 +759,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, - { - desc: "Ingress with IPv6 endpoints", - expected: &dynamic.Configuration{ - TCP: &dynamic.TCPConfiguration{}, - HTTP: &dynamic.HTTPConfiguration{ - Middlewares: map[string]*dynamic.Middleware{}, - Routers: map[string]*dynamic.Router{ - "example-com-testing-bar": { - Rule: "PathPrefix(`/bar`)", - Service: "testing-service-bar-8080", - }, - "example-com-testing-foo": { - Rule: "PathPrefix(`/foo`)", - Service: "testing-service-foo-8080", - }, - }, - Services: map[string]*dynamic.Service{ - "testing-service-bar-8080": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b]:8080", - }, - }, - PassHostHeader: Bool(true), - }, - }, - "testing-service-foo-8080": { - LoadBalancer: &dynamic.ServersLoadBalancer{ - Servers: []dynamic.Server{ - { - URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b]:8080", - }, - }, - PassHostHeader: Bool(true), - }, - }, - }, - }, - }, - }, { desc: "TLS support", expected: &dynamic.Configuration{ @@ -1332,6 +1264,152 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, } + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var paths []string + _, err := os.Stat(generateTestFilename("_ingress", test.desc)) + if err == nil { + paths = append(paths, generateTestFilename("_ingress", test.desc)) + } + _, err = os.Stat(generateTestFilename("_endpoint", test.desc)) + if err == nil { + paths = append(paths, generateTestFilename("_endpoint", test.desc)) + } + _, err = os.Stat(generateTestFilename("_service", test.desc)) + if err == nil { + paths = append(paths, generateTestFilename("_service", test.desc)) + } + _, err = os.Stat(generateTestFilename("_secret", test.desc)) + if err == nil { + paths = append(paths, generateTestFilename("_secret", test.desc)) + } + _, err = os.Stat(generateTestFilename("_ingressclass", test.desc)) + if err == nil { + paths = append(paths, generateTestFilename("_ingressclass", test.desc)) + } + + serverVersion := test.serverVersion + if serverVersion == "" { + serverVersion = "v1.17" + } + + clientMock := newClientMock(serverVersion, paths...) + p := Provider{IngressClass: test.ingressClass} + conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) + + assert.Equal(t, test.expected, conf) + }) + } +} + +func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) { + testCases := []struct { + desc string + ingressClass string + serverVersion string + allowExternalNameServices bool + expected *dynamic.Configuration + }{ + { + desc: "Ingress with service with externalName", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{}, + Services: map[string]*dynamic.Service{}, + }, + }, + }, + { + desc: "Ingress with service with externalName enabled", + allowExternalNameServices: true, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-traefik-tchouk-bar": { + Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)", + Service: "testing-service1-8080", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://traefik.wtf:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "Ingress with IPv6 endpoints", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "example-com-testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service-bar-8080", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service-bar-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b]:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + { + desc: "Ingress with IPv6 endpoints externalname enabled", + allowExternalNameServices: true, + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "example-com-testing-foo": { + Rule: "PathPrefix(`/foo`)", + Service: "testing-service-foo-8080", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service-foo-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b]:8080", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + }, + }, + } + for _, test := range testCases { test := test @@ -1368,6 +1446,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { clientMock := newClientMock(serverVersion, paths...) p := Provider{IngressClass: test.ingressClass} + p.AllowExternalNameServices = test.allowExternalNameServices conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) assert.Equal(t, test.expected, conf) From e1f5866989514821535c8422a9d20bcb304ba826 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou <925513+jbdoumenjou@users.noreply.github.com> Date: Tue, 13 Jul 2021 14:14:35 +0200 Subject: [PATCH 6/7] Detect certificates content modifications Co-authored-by: Romain Co-authored-by: Mathieu Lonjaret --- docs/content/getting-started/faq.md | 13 +++ pkg/provider/file/file.go | 80 +++++++++++++++++++ pkg/provider/file/file_test.go | 31 ++++++- .../file/fixtures/toml/tls_file_key.cert | 1 + pkg/server/configurationwatcher.go | 18 +++++ 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 pkg/provider/file/fixtures/toml/tls_file_key.cert diff --git a/docs/content/getting-started/faq.md b/docs/content/getting-started/faq.md index 66ae963af..74ad989bf 100644 --- a/docs/content/getting-started/faq.md +++ b/docs/content/getting-started/faq.md @@ -124,3 +124,16 @@ http: If there is a need for a response code other than a `503` and/or a custom message, the principle of the above example above (a catchall router) still stands, but the `unavailable` service should be adapted to fit such a need. + +## Why Is My TLS Certificate Not Reloaded When Its Contents Change ? + +With the file provider, +a configuration update is only triggered when one of the [watched](../providers/file.md#provider-configuration) configuration files is modified. + +Which is why, when a certificate is defined by path, +and the actual contents of this certificate change, +a configuration update is _not_ triggered. + +To take into account the new certificate contents, the update of the dynamic configuration must be forced. +One way to achieve that, is to trigger a file notification, +for example, by using the `touch` command on the configuration file. diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index 7578cc62c..f6a48b613 100644 --- a/pkg/provider/file/file.go +++ b/pkg/provider/file/file.go @@ -167,6 +167,86 @@ func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTem if configuration.TLS != nil { configuration.TLS.Certificates = flattenCertificates(ctx, configuration.TLS) + + // TLS Options + if configuration.TLS.Options != nil { + for name, options := range configuration.TLS.Options { + var caCerts []tls.FileOrContent + + for _, caFile := range options.ClientAuth.CAFiles { + content, err := caFile.Read() + if err != nil { + log.FromContext(ctx).Error(err) + continue + } + + caCerts = append(caCerts, tls.FileOrContent(content)) + } + options.ClientAuth.CAFiles = caCerts + + configuration.TLS.Options[name] = options + } + } + + // TLS stores + if len(configuration.TLS.Stores) > 0 { + for name, store := range configuration.TLS.Stores { + content, err := store.DefaultCertificate.CertFile.Read() + if err != nil { + log.FromContext(ctx).Error(err) + continue + } + store.DefaultCertificate.CertFile = tls.FileOrContent(content) + + content, err = store.DefaultCertificate.KeyFile.Read() + if err != nil { + log.FromContext(ctx).Error(err) + continue + } + store.DefaultCertificate.KeyFile = tls.FileOrContent(content) + + configuration.TLS.Stores[name] = store + } + } + } + + // ServersTransport + if configuration.HTTP != nil && len(configuration.HTTP.ServersTransports) > 0 { + for name, st := range configuration.HTTP.ServersTransports { + var certificates []tls.Certificate + for _, cert := range st.Certificates { + content, err := cert.CertFile.Read() + if err != nil { + log.FromContext(ctx).Error(err) + continue + } + cert.CertFile = tls.FileOrContent(content) + + content, err = cert.KeyFile.Read() + if err != nil { + log.FromContext(ctx).Error(err) + continue + } + cert.KeyFile = tls.FileOrContent(content) + + certificates = append(certificates, cert) + } + + configuration.HTTP.ServersTransports[name].Certificates = certificates + + var rootCAs []tls.FileOrContent + for _, rootCA := range st.RootCAs { + content, err := rootCA.Read() + if err != nil { + log.FromContext(ctx).Error(err) + continue + } + + rootCAs = append(rootCAs, tls.FileOrContent(content)) + } + + st.RootCAs = rootCAs + } } return configuration, nil diff --git a/pkg/provider/file/file_test.go b/pkg/provider/file/file_test.go index 6dde44321..23d820891 100644 --- a/pkg/provider/file/file_test.go +++ b/pkg/provider/file/file_test.go @@ -25,19 +25,35 @@ type ProvideTestCase struct { expectedNumTLSOptions int } -func TestTLSContent(t *testing.T) { +func TestTLSCertificateContent(t *testing.T) { tempDir := t.TempDir() fileTLS, err := createTempFile("./fixtures/toml/tls_file.cert", tempDir) require.NoError(t, err) + fileTLSKey, err := createTempFile("./fixtures/toml/tls_file_key.cert", tempDir) + require.NoError(t, err) + fileConfig, err := os.CreateTemp(tempDir, "temp*.toml") require.NoError(t, err) content := ` [[tls.certificates]] certFile = "` + fileTLS.Name() + `" - keyFile = "` + fileTLS.Name() + `" + keyFile = "` + fileTLSKey.Name() + `" + +[tls.options.default.clientAuth] + caFiles = ["` + fileTLS.Name() + `"] + +[tls.stores.default.defaultCertificate] + certFile = "` + fileTLS.Name() + `" + keyFile = "` + fileTLSKey.Name() + `" + +[http.serversTransports.default] + rootCAs = ["` + fileTLS.Name() + `"] + [[http.serversTransports.default.certificates]] + certFile = "` + fileTLS.Name() + `" + keyFile = "` + fileTLSKey.Name() + `" ` _, err = fileConfig.Write([]byte(content)) @@ -48,7 +64,16 @@ func TestTLSContent(t *testing.T) { require.NoError(t, err) require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.CertFile.String()) - require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.KeyFile.String()) + require.Equal(t, "CONTENTKEY", configuration.TLS.Certificates[0].Certificate.KeyFile.String()) + + require.Equal(t, "CONTENT", configuration.TLS.Options["default"].ClientAuth.CAFiles[0].String()) + + require.Equal(t, "CONTENT", configuration.TLS.Stores["default"].DefaultCertificate.CertFile.String()) + require.Equal(t, "CONTENTKEY", configuration.TLS.Stores["default"].DefaultCertificate.KeyFile.String()) + + require.Equal(t, "CONTENT", configuration.HTTP.ServersTransports["default"].Certificates[0].CertFile.String()) + require.Equal(t, "CONTENTKEY", configuration.HTTP.ServersTransports["default"].Certificates[0].KeyFile.String()) + require.Equal(t, "CONTENT", configuration.HTTP.ServersTransports["default"].RootCAs[0].String()) } func TestErrorWhenEmptyConfig(t *testing.T) { diff --git a/pkg/provider/file/fixtures/toml/tls_file_key.cert b/pkg/provider/file/fixtures/toml/tls_file_key.cert new file mode 100644 index 000000000..f196b32ee --- /dev/null +++ b/pkg/provider/file/fixtures/toml/tls_file_key.cert @@ -0,0 +1 @@ +CONTENTKEY \ No newline at end of file diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 1d71fe472..a4b78e6ad 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -12,6 +12,7 @@ import ( "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/safe" + "github.com/traefik/traefik/v2/pkg/tls" ) // ConfigurationWatcher watches configuration changes. @@ -164,6 +165,16 @@ func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) { if copyConf.TLS != nil { copyConf.TLS.Certificates = nil + if copyConf.TLS.Options != nil { + cleanedOptions := make(map[string]tls.Options, len(copyConf.TLS.Options)) + for name, option := range copyConf.TLS.Options { + option.ClientAuth.CAFiles = []tls.FileOrContent{} + cleanedOptions[name] = option + } + + copyConf.TLS.Options = cleanedOptions + } + for k := range copyConf.TLS.Stores { st := copyConf.TLS.Stores[k] st.DefaultCertificate = nil @@ -171,6 +182,13 @@ func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) { } } + if copyConf.HTTP != nil { + for _, transport := range copyConf.HTTP.ServersTransports { + transport.Certificates = tls.Certificates{} + transport.RootCAs = []tls.FileOrContent{} + } + } + jsonConf, err := json.Marshal(copyConf) if err != nil { logger.Errorf("Could not marshal dynamic configuration: %v", err) From 7e0f0d9d117ece8ebd7acbbe0481decc8c50820d Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 13 Jul 2021 07:30:20 -0600 Subject: [PATCH 7/7] Ignore http 1.0 request host missing errors --- pkg/rules/rules.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/rules/rules.go b/pkg/rules/rules.go index 08b0ebaae..1634b6bcb 100644 --- a/pkg/rules/rules.go +++ b/pkg/rules/rules.go @@ -113,7 +113,11 @@ func host(route *mux.Route, hosts ...string) error { route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool { reqHost := requestdecorator.GetCanonizedHost(req.Context()) if len(reqHost) == 0 { - log.FromContext(req.Context()).Warnf("Could not retrieve CanonizedHost, rejecting %s", req.Host) + // If the request is an HTTP/1.0 request, then a Host may not be defined. + if req.ProtoAtLeast(1, 1) { + log.FromContext(req.Context()).Warnf("Could not retrieve CanonizedHost, rejecting %s", req.Host) + } + return false }