Traefik: HTTP/3 mTLS bypass via exact SNI TLSOptions lookup for wildcard and mixed-case hosts
Description
Traefik HTTP/3 mTLS bypass: exact SNI lookup fails on wildcard/mixed-case hosts, letting attackers skip client certificate authentication.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Traefik HTTP/3 mTLS bypass: exact SNI lookup fails on wildcard/mixed-case hosts, letting attackers skip client certificate authentication.
Vulnerability
A critical vulnerability in Traefik's HTTP/3 (QUIC) TLS configuration selection, present in versions v3.7.0 and v3.7.1, allows unauthenticated clients to bypass router-specific mTLS enforcement [1][2]. When HTTP/3 is enabled on an entrypoint, the QUIC handshake selects the TLS configuration via Router.GetTLSGetClientInfo(), which performs a direct, case-sensitive map lookup on the SNI value (hostHTTPTLSConfig[info.ServerName]). This lookup fails to match wildcard host patterns (e.g., *.example.com) or case variants of the configured hostname, causing the handshake to fall back to the default TLS configuration — which often does not require client certificates. The subsequent HTTP routing layer still dispatches the request to a backend protected by a router-specific mTLS policy, creating the bypass [1][2].
Exploitation
An attacker needs only UDP network access to a Traefik entrypoint where HTTP/3 is enabled and a router uses a wildcard Host rule or case-insensitive hostname matching, combined with a router-specific TLSOptions that enforces client certificate authentication [1][2]. Two exploit paths are confirmed [1][2]: - Wildcard bypass: Sending an HTTP/3 request with any SNI matching a wildcard rule (e.g., *.example.com) allows the handshake to use the default TLS config, while the HTTP layer still routes to the protected backend. - Mixed-case bypass: For an exact host rule (e.g., api.example.com), sending an SNI with mixed case (e.g., API.EXAMPLE.COM) causes the lookup to miss, falling back to default TLS, yet the HTTP layer matches the host case-insensitively and routes the request to the protected backend. The attacker completes the QUIC handshake without presenting any client certificate.
Impact
On successful exploitation, an attacker can reach a backend that expects mutual TLS authentication without providing a client certificate, effectively bypassing mTLS enforcement for the affected routes [1][2]. The compromise of confidentiality, integrity, or availability depends on the backend service's sensitivity; however, the primary impact is the complete circumvention of client authentication, potentially allowing unauthorized access to protected resources.
Mitigation
A fix has been released in Traefik version v3.7.3 [3]. The advisory strongly recommends upgrading to this version immediately [1][2]. No workarounds are documented in the available references; disabling HTTP/3 on affected entrypoints may serve as a temporary mitigation until the patch is applied.
AI Insight generated on Jun 16, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
25ea71f1c3af0Prepare release v3.7.3
3 files changed · +15 −9
CHANGELOG.md+8 −2 modified@@ -1,7 +1,8 @@ -## [v3.7.2](https://github.com/traefik/traefik/tree/v3.7.2) (2026-06-03) -[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.2) +## [v3.7.3](https://github.com/traefik/traefik/tree/v3.7.3) (2026-06-04) +[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.3) **Bug fixes:** +- **[tls]** Compute resolved tlsOptions after applying models ([#13291](https://github.com/traefik/traefik/pull/13291) @rtribotte) - **[webui, tcp]** Fix TCP router service resolution in dashboard flow diagram ([#13155](https://github.com/traefik/traefik/pull/13155) @aliamerj) - **[k8s/ingress-nginx]** Trim quotes from proxy_set_header header name ([#13203](https://github.com/traefik/traefik/pull/13203) @crisbal) - **[accesslogs]** Escape double quotes in quoted log fields ([#13180](https://github.com/traefik/traefik/pull/13180) @KaanSimsek) @@ -70,6 +71,11 @@ - **[middleware]** Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation ([#13215](https://github.com/traefik/traefik/pull/13215) @rtribotte) - **[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.7.2](https://github.com/traefik/traefik/tree/v3.7.2) (2026-06-03) +[All Commits](https://github.com/traefik/traefik/compare/v3.7.1...v3.7.2) + +Release canceled. ## [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)
docs/content/migrate/v3.md+4 −4 modified@@ -9,11 +9,11 @@ This guide provides detailed migration steps for upgrading between different Tra --- -## v3.7.2 +## v3.7.3 ### Kubernetes Gateway API Provider -Starting with `v3.7.2`, 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.7.3`, 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. @@ -23,13 +23,13 @@ and [`kubernetesGateway.burst`](../reference/install-configuration/providers/kub ### BasicAuth Middleware -From version `v3.7.2` onwards, the BasicAuth middleware requires a non-empty users configuration in order to be built successfully. +From version `v3.7.3` 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.7.2` onwards, the StripPrefix middleware and the StripPrefixRegex middleware reject requests (`400 Bad Request`) +From version `v3.7.3` 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).
script/gcg/traefik-bugfix.toml+3 −3 modified@@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.7.2 +# example new bugfix v3.7.3 CurrentRef = "v3.7" -PreviousRef = "v3.7.1" +PreviousRef = "v3.7.2" BaseBranch = "v3.7" -FutureCurrentRefName = "v3.7.2" +FutureCurrentRefName = "v3.7.3" ThresholdPreviousRef = 10000 ThresholdCurrentRef = 10000
a664812e9c30Compute resolved tlsOptions after applying models
5 files changed · +166 −2
integration/fixtures/https/https_entrypoint_tls.toml+53 −0 added@@ -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"
integration/https_test.go+62 −1 modified@@ -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))
pkg/server/aggregator.go+1 −1 modified@@ -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 {
pkg/server/configurationwatcher.go+1 −0 modified@@ -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)
pkg/server/configurationwatcher_test.go+49 −0 modified@@ -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 +}
Vulnerability mechanics
Root cause
"Exact, case-sensitive map lookup on SNI in the HTTP/3 TLS callback fails to match wildcard or mixed-case hostnames, causing fallback to the default TLS configuration and bypassing router-specific mTLS."
Attack vector
An unauthenticated attacker with UDP access to the HTTP/3 entrypoint sends a QUIC ClientHello with an SNI value that either matches a wildcard pattern (e.g., `api.example.com` when the router uses `Host(\`*.example.com\`)`) or uses mixed case (e.g., `API.EXAMPLE.COM` when the router uses `Host(\`api.example.com\`)`). Because `GetTLSGetClientInfo()` performs an exact, case-sensitive map lookup, the SNI does not match any router-specific TLS configuration, so the handshake falls back to the default TLS config—which typically does not require a client certificate. The QUIC handshake succeeds without mTLS, and the subsequent HTTP routing layer still dispatches the request to the backend protected by the router-specific mTLS policy [ref_id=1][ref_id=2].
Affected code
The vulnerability resides in the HTTP/3 TLS configuration selection path. In `pkg/server/server_entrypoint_tcp_http3.go`, the QUIC TLS callback is wired to `rt.GetTLSGetClientInfo()` from `pkg/server/router/tcp/router.go`, which performs an exact, case-sensitive map lookup on `hostHTTPTLSConfig[info.ServerName]`. This fails to match wildcard patterns (e.g., `*.example.com`) or case variants of the configured hostname, causing the handshake to fall back to the default TLS configuration.
What the fix does
The patch moves the `resolveHTTPTLSOptions` call from inside `mergeConfiguration` to after `applyModel` in `configurationwatcher.go` [patch_id=6214999]. Previously, TLS options were resolved before entry-point models were applied, so routers that inherited their TLS configuration from an entry-point model never had their `ResolvedOptions` field populated. By resolving TLS options after models are applied, the fix ensures that the correct TLS configuration (including mTLS options) is associated with the router and available during the HTTP/3 TLS handshake, preventing the fallback to the default config.
Preconditions
- configHTTP/3 is enabled on the affected entrypoint
- configA router-specific TLSOptions configuration enforces client certificate authentication
- configThe default/fallback TLS configuration does not require client certificates
- networkUDP access to the HTTP/3 entrypoint is reachable by the attacker
Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.