diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e06d7f0f2..11ab09b6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,10 +31,11 @@ - **[k8s/ingress-nginx]** Add nginx.ingress.kubernetes.io/enable-global-auth to the list of supported annotations ([#13219](https://github.com/traefik/traefik/pull/13219) @filip2mac) - **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems) -## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03) -[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18) +## [v3.6.19](https://github.com/traefik/traefik/tree/v3.6.19) (2026-06-04) +[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.19) **Bug fixes:** +- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte) - **[accesslogs]** Escape double quotes in quoted log fields ([#13180](https://github.com/traefik/traefik/pull/13180) @KaanSimsek) - **[k8s/gatewayapi]** Escape exact gRPC method matches ([#13201](https://github.com/traefik/traefik/pull/13201) @nickmnt) - **[logs, middleware]** Allow query parameters to be dropped from RequestPath in access log ([#13091](https://github.com/traefik/traefik/pull/13091) @calinelson) @@ -51,17 +52,18 @@ **Documentation:** - **[file]** Replace generated File routing reference page ([#13170](https://github.com/traefik/traefik/pull/13170) @sheddy-traefik) -- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87) - **[k8s/crd]** Fix typo in accesslogs field name ([#13177](https://github.com/traefik/traefik/pull/13177) @PlayMTL) - **[k8s/ingress-nginx]** Surface the Ingress status race condition during NGINX coexistence ([#13205](https://github.com/traefik/traefik/pull/13205) @emilevauge) -- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems) - Polish grammar in migration guides ([#13174](https://github.com/traefik/traefik/pull/13174) @quyentonndbs) +- **[middleware]** Remove whitespace in HTML tag ([#13160](https://github.com/traefik/traefik/pull/13160) @marbon87) - Add @LBF38 as a current maintainer ([#13225](https://github.com/traefik/traefik/pull/13225) @emilevauge) +- **[k8s/ingress-nginx]** Capitalize NGINX in kubernetesIngressNGINX ([#13236](https://github.com/traefik/traefik/pull/13236) @smellems) -## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03) -[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47) +## [v2.11.48](https://github.com/traefik/traefik/tree/v2.11.48) (2026-06-04) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.48) **Bug fixes:** +- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte) - **[middleware, authentication]** Add error on basic auth build if users is empty ([#13195](https://github.com/traefik/traefik/pull/13195) @rtribotte) - **[k8s/ingress]** Avoid ingress path matcher injection and backport 11d251415 ([#13227](https://github.com/traefik/traefik/pull/13227) @rtribotte) - **[server]** Move snicheck to ctx instead of simulated routing ([#13214](https://github.com/traefik/traefik/pull/13214) @juliens) @@ -69,6 +71,16 @@ - **[server]** Bump golang.org/x/net to v0.55.0 ([#13251](https://github.com/traefik/traefik/pull/13251) @kevinpollet) - **[server]** Bump golang.org/x/crypto to v0.52.0 ([#13276](https://github.com/traefik/traefik/pull/13276) @rtribotte) +## [v3.6.18](https://github.com/traefik/traefik/tree/v3.6.18) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v3.6.17...v3.6.18) + +Release canceled. + +## [v2.11.47](https://github.com/traefik/traefik/tree/v2.11.47) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.46...v2.11.47) + +Release canceled. + ## [v3.7.1](https://github.com/traefik/traefik/tree/v3.7.1) (2026-05-11) [All Commits](https://github.com/traefik/traefik/compare/v3.7.0...v3.7.1) diff --git a/docs/content/migrate/v3.md b/docs/content/migrate/v3.md index 71c7ab9015..daa4fdfd4c 100644 --- a/docs/content/migrate/v3.md +++ b/docs/content/migrate/v3.md @@ -144,11 +144,11 @@ Note: TLSOptions for `HostRegexp` matchers remains unsupported. Use wildcard `Ho --- -## v3.6.18 +## v3.6.19 ### Kubernetes Gateway API Provider -Starting with `v3.6.18`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). +Starting with `v3.6.19`, the QPS and Burst values of the Kubernetes client used by the Kubernetes Gateway API provider have been increased to `50` and `100` respectively (10x the default values of the Kubernetes client). The Kubernetes Gateway API provider writes status updates intensively to comply with the Kubernetes Gateway API specification. This change helps avoid performance issues related to Kubernetes API rate limiting, which can increase the setup time when a new routing configuration is built. @@ -158,13 +158,13 @@ and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kub ### BasicAuth Middleware -From version `v3.6.18` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. +From version `v3.6.19` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. Previously, the middleware would be built successfully but always return a 401 status code for any request. Now, an error occurs and any routers using it will be unmounted. For the same request, a 404 status code is served instead of a 401 status code. ### StripPrefix and StripPrefixRegex Middleware -From version `v3.6.18` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) +From version `v3.6.19` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) when stripping the configured prefix produces a path that differs from its normalised form (i.e. a path containing `.` or `..` segments that would be collapsed by normalisation). diff --git a/go.mod b/go.mod index f8e2d281d9..de812f1bd1 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/docker/cli v29.4.0+incompatible github.com/docker/go-connections v0.6.0 github.com/fatih/structs v1.1.0 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.10.1 github.com/go-acme/lego/v4 v4.35.2 github.com/go-kit/kit v0.13.0 github.com/go-kit/log v0.2.1 diff --git a/go.sum b/go.sum index b97e7dc353..78e8a419b5 100644 --- a/go.sum +++ b/go.sum @@ -1004,8 +1004,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= diff --git a/integration/fixtures/https/https_entrypoint_tls.toml b/integration/fixtures/https/https_entrypoint_tls.toml new file mode 100644 index 0000000000..26a85c8128 --- /dev/null +++ b/integration/fixtures/https/https_entrypoint_tls.toml @@ -0,0 +1,53 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[log] + level = "DEBUG" + +[entryPoints] + [entryPoints.websecure] + address = ":4443" + [entryPoints.websecure.http.tls] + + [entryPoints.websecure-options] + address = ":4444" + [entryPoints.websecure-options.http.tls] + options = "foo" + +[api] + insecure = true + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router1] + entryPoints = ["websecure"] + service = "service1" + rule = "Host(`snitest.com`)" + + [http.routers.router2] + entryPoints = ["websecure-options"] + service = "service1" + rule = "Host(`snitest.org`)" + +[http.services] + [http.services.service1] + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "http://127.0.0.1:9010" + +[[tls.certificates]] + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" + +[[tls.certificates]] + certFile = "fixtures/https/snitest.org.cert" + keyFile = "fixtures/https/snitest.org.key" + +[tls.options] + [tls.options.foo] + maxVersion = "VersionTLS12" diff --git a/integration/https_test.go b/integration/https_test.go index ab3e190615..e962337e67 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -150,7 +150,69 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute() { require.NoError(s.T(), err) } -// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options. +// TestWithEntryPointTLSConfig verifies that a router relying on the entry point +// TLS configuration (without an explicit router TLS section) is served over HTTPS, +// including when the entry point references user-defined TLS options. +// Regression test for https://github.com/traefik/traefik/issues/13289. +func (s *HTTPSSuite) TestWithEntryPointTLSConfig() { + file := s.adaptFile("fixtures/https/https_entrypoint_tls.toml", struct{}{}) + s.traefikCmd(withConfigFile(file)) + + // wait for Traefik + err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.com`)")) + require.NoError(s.T(), err) + + backend := startTestServer("9010", http.StatusNoContent, "") + defer backend.Close() + + err = try.GetRequest(backend.URL, 1*time.Second, try.StatusCodeIs(http.StatusNoContent)) + require.NoError(s.T(), err) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + require.NoError(s.T(), err) + req.Host = tr.TLSClientConfig.ServerName + req.Header.Set("Host", tr.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + err = try.RequestWithTransport(req, 30*time.Second, tr, try.HasCn(tr.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent)) + require.NoError(s.T(), err) + + // The websecure-options entry point references the user-defined "foo" TLS options (maxVersion VersionTLS12). + // A request with no router-level TLS must still have these options resolved and applied. + trOptions := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4444/", nil) + require.NoError(s.T(), err) + req.Host = trOptions.TLSClientConfig.ServerName + req.Header.Set("Host", trOptions.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + err = try.RequestWithTransport(req, 30*time.Second, trOptions, try.HasCn(trOptions.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent)) + require.NoError(s.T(), err) + + // A TLS 1.3-only client must fail the handshake, proving the "foo" options + // (resolved from the entry point) are effectively enforced. + _, err = tls.Dial("tcp", "127.0.0.1:4444", &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + MinVersion: tls.VersionTLS13, + }) + assert.Error(s.T(), err) +} + +// TestWithTLSOptions verifies that traefik routes the requests with the associated tls options. func (s *HTTPSSuite) TestWithTLSOptions() { file := s.adaptFile("fixtures/https/https_tls_options.toml", struct{}{}) s.traefikCmd(withConfigFile(file)) diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 652db41352..583fa15157 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -170,7 +170,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint delete(conf.TLS.Options, traefiktls.DefaultTLSConfigName) } - return resolveHTTPTLSOptions(conf) + return conf } func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration { diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 747f476f65..919c75d0bf 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -176,6 +176,7 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) { conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints) conf = applyModel(conf) + conf = resolveHTTPTLSOptions(conf) for _, listener := range c.configurationListeners { listener(conf) diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go index 40df7b5aad..a9f5920948 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -992,3 +992,52 @@ func TestConfigurationWatcher_MultipleTransformers(t *testing.T) { assert.Equal(t, 1, callCount1) assert.Equal(t, 1, callCount2) } + +// TestEntryPointTLSResolvedOptions is a regression test for +// https://github.com/traefik/traefik/issues/13289: a router whose TLS +// configuration comes from the entry point (and not from an explicit router TLS +// section) must still have its TLS options resolved in the published configuration. +func TestEntryPointTLSResolvedOptions(t *testing.T) { + routinesPool := safe.NewPool(t.Context()) + t.Cleanup(routinesPool.Stop) + + pvd := &mockProvider{ + messages: []dynamic.Message{{ + ProviderName: "internal", + Configuration: &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "foo": { + EntryPoints: []string{"websecure"}, + Rule: "Host(`foo.example.com`)", + Service: "service", + }, + }, + Models: map[string]*dynamic.Model{ + "websecure": { + TLS: &dynamic.RouterTLSConfig{}, + }, + }, + }, + }, + }}, + } + + watcher := NewConfigurationWatcher(routinesPool, pvd, []string{}, "") + + run := make(chan struct{}) + watcher.AddListener(func(conf dynamic.Configuration) { + router := conf.HTTP.Routers["foo@internal"] + if router == nil || router.TLS == nil { + return + } + + assert.Equal(t, "default", router.TLS.ResolvedOptions) + close(run) + }) + + watcher.Start() + t.Cleanup(watcher.Stop) + + <-run +}