From a664812e9c30f8c9705ce16f0d2f2d96e017bccd Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Jun 2026 10:16:05 +0200 Subject: [PATCH 1/2] Compute resolved tlsOptions after applying models Co-authored-by: Gina A. <70909035+gndz07@users.noreply.github.com> --- .../fixtures/https/https_entrypoint_tls.toml | 53 ++++++++++++++++ integration/https_test.go | 63 ++++++++++++++++++- pkg/server/aggregator.go | 2 +- pkg/server/configurationwatcher.go | 1 + pkg/server/configurationwatcher_test.go | 49 +++++++++++++++ 5 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 integration/fixtures/https/https_entrypoint_tls.toml 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 8bdf5c1c45..2a79da18b3 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -114,8 +114,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 2dcabdae2b..1d8f5ea337 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -138,7 +138,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 4d6798694e..5c1e8241ec 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -167,6 +167,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 3e6c32fd76..08fd497e0d 100644 --- a/pkg/server/configurationwatcher_test.go +++ b/pkg/server/configurationwatcher_test.go @@ -893,3 +893,52 @@ func TestPublishConfigUpdatedByConfigWatcherListener(t *testing.T) { assert.Equal(t, 1, publishedConfigCount) } + +// 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 +} From 2c436f3c239932f9eb8afab57fc2668d86880e35 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 4 Jun 2026 10:36:22 +0200 Subject: [PATCH 2/2] Prepare release v2.11.48 --- CHANGELOG.md | 10 ++++++++-- docs/content/migration/v2.md | 6 +++--- script/gcg/traefik-bugfix.toml | 6 +++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60182a3ed3..335030a399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## [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) @@ -9,6 +10,11 @@ - **[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) +## [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. + ## [v2.11.46](https://github.com/traefik/traefik/tree/v2.11.46) (2026-05-11) [All Commits](https://github.com/traefik/traefik/compare/v2.11.45...v2.11.46) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 8c09fd7d5f..3c74454bce 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -849,17 +849,17 @@ The behavior is as follows: Please check out the [Kubernetes CRD](../providers/kubernetes-crd.md#crossprovidernamespaces), [Kubernetes Ingress](../providers/kubernetes-ingress.md#crossprovidernamespaces), and [Kubernetes Gateway](../providers/kubernetes-gateway.md#crossprovidernamespaces) provider documentation for more details. -## v2.11.47 +## v2.11.48 ### BasicAuth Middleware -From version `v2.11.47` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. +From version `v2.11.48` 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 `v2.11.47` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) +From version `v2.11.48` 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/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 29e2645689..6f161f1556 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.47 +# example new bugfix v2.11.48 CurrentRef = "v2.11" -PreviousRef = "v2.11.46" +PreviousRef = "v2.11.47" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.47" +FutureCurrentRefName = "v2.11.48" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000