diff --git a/Makefile b/Makefile index 7bb8e73de..7cb590cc4 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 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/docs/content/migration/v2.md b/docs/content/migration/v2.md index 2293976ff..eac80bed9 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -365,6 +365,17 @@ For more information, please read the [HTTP routers rule](../routing/routers/ind 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`. + +### 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`. + ## v2.4 to v2.5 ### Kubernetes CRD diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index 6f5d64fd7..7276e391d 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -266,29 +266,48 @@ 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 +``` + +### `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 diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index 68d819418..c3a3a86c9 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -464,6 +464,29 @@ providers: Allow the creation of services if there are no endpoints available. This results in `503` http responses instead of `404`. +### `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 f8a2aa8d3..32f572187 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -577,7 +577,10 @@ 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.allowexternalnameservices`: +Allow ExternalName services. (Default: ```false```) `--providers.kubernetescrd.certauthfilepath`: Kubernetes certificate authority file path (not needed for in-cluster client). @@ -627,6 +630,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```) `--providers.kubernetesingress.allowemptyservices`: Allow creation of services without endpoints. (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 a1f82ca13..9a62e1159 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -577,7 +577,10 @@ 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_ALLOWEXTERNALNAMESERVICES`: +Allow ExternalName services. (Default: ```false```) `TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`: Kubernetes certificate authority file path (not needed for in-cluster client). @@ -627,6 +630,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```) `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEMPTYSERVICES`: Allow creation of services without endpoints. (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/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index b3657b67a..a6cfea67d 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" @@ -347,7 +348,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 02f4e6989..751380018 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, ",")) } }) } 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() +} diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index c8f915d1d..4be512da2 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/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 cd55a18a7..dcb37ae8a 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -41,20 +41,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 -} - -// SetDefaults sets the default values. -func (p *Provider) SetDefaults() { - p.AllowCrossNamespace = func(b bool) *bool { return &b }(true) + 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) { @@ -106,10 +102,14 @@ 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)") } + 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()) @@ -274,7 +274,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) @@ -450,7 +450,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 } @@ -919,7 +919,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 79f848035..3f969d7e8 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -50,7 +50,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" { @@ -173,8 +173,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, @@ -323,6 +324,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 e283ecbdf..181baa7d5 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -172,7 +172,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 } @@ -199,7 +199,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 @@ -209,6 +209,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 aea40d19c..f10c37b4f 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -1294,8 +1294,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass} - p.SetDefaults() + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -3476,8 +3475,7 @@ func TestLoadIngressRoutes(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass} - p.SetDefaults() + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -3894,8 +3892,7 @@ func TestLoadIngressRouteUDPs(t *testing.T) { return } - p := Provider{IngressClass: test.ingressClass} - p.SetDefaults() + p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true} clientMock := newClientMock(test.paths...) conf := p.loadConfigurationFromCRD(context.Background(), clientMock) @@ -4895,10 +4892,299 @@ func TestCrossNamespace(t *testing.T) { <-eventCh } - p := Provider{} - p.SetDefaults() + 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{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + 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{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + 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{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + 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`)", + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + 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`)", + }, + }, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + 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{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + 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{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + 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 = Bool(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 808b6bff0..ad0c22dc6 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) + } + svcPort, err := getServicePort(service, svc.Port) if err != nil { return nil, err 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 0a2240640..a3c7eac46 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -36,16 +36,17 @@ 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 or IngressClass name 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"` - AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,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 or IngressClass name 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"` + AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,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()) @@ -232,7 +237,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl continue } - service, err := loadService(client, ingress.Namespace, *ingress.Spec.DefaultBackend) + service, err := p.loadService(client, ingress.Namespace, *ingress.Spec.DefaultBackend) if err != nil { log.FromContext(ctx). WithField("serviceName", ingress.Spec.DefaultBackend.Service.Name). @@ -277,7 +282,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.Service.Name). @@ -486,7 +491,7 @@ func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores return configs } -func loadService(client Client, namespace string, backend networkingv1.IngressBackend) (*dynamic.Service, error) { +func (p *Provider) loadService(client Client, namespace string, backend networkingv1.IngressBackend) (*dynamic.Service, error) { service, exists, err := client.GetService(namespace, backend.Service.Name) if err != nil { return nil, err @@ -496,6 +501,10 @@ func loadService(client Client, namespace string, backend networkingv1.IngressBa 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.Service.Name) + } + 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 67ba7e063..58d8226a9 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -746,33 +746,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{ @@ -800,47 +773,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{ @@ -1702,7 +1634,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { } clientMock := newClientMock(serverVersion, paths...) - p := Provider{IngressClass: test.ingressClass, AllowEmptyServices: test.allowEmptyServices} conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) @@ -1711,6 +1642,154 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { } } +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 + + 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} + p.AllowExternalNameServices = test.allowExternalNameServices + conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) + + assert.Equal(t, test.expected, conf) + }) + } +} + func generateTestFilename(suffix, desc string) string { return "./fixtures/" + strings.ReplaceAll(desc, " ", "-") + suffix + ".yml" } diff --git a/pkg/rules/rules.go b/pkg/rules/rules.go index 2eeb3ee3b..3f709eae7 100644 --- a/pkg/rules/rules.go +++ b/pkg/rules/rules.go @@ -116,7 +116,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 } diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index c7541de80..c46ac5785 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)