VYPR
High severity7.8GHSA Advisory· Published Jun 16, 2026· Updated Jun 16, 2026

Traefik: SNICheck ignores wildcard TLSOptions mappings, allowing domain-fronted mTLS bypass

CVE-2026-48491

Description

CVE-2026-48491: Traefik's SNICheck domain-fronting protection ignores wildcard TLSOptions, allowing an attacker to bypass mutual TLS by domain fronting on the same entrypoint.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2026-48491: Traefik's SNICheck domain-fronting protection ignores wildcard TLSOptions, allowing an attacker to bypass mutual TLS by domain fronting on the same entrypoint.

Vulnerability

Traefik versions prior to v3.7.3 contain a flaw in the SNICheck middleware that performs domain-fronting validation. When a wildcard host rule such as Host(*.example.com) is configured with stricter TLS options (e.g., RequireAndVerifyClientCert), SNICheck resolves the TLS options for the HTTP Host header using exact map lookups only and does not apply wildcard matching. If another permissive SNI is served on the same entrypoint, an attacker can exploit this inconsistency [1][2].

Exploitation

An unauthenticated attacker with network access to a Traefik entrypoint that serves both a permissive SNI and a wildcard-protected backend can complete the TLS handshake under the permissive SNI options (no client certificate required) and then send an HTTP request with a Host header that matches the wildcard pattern (e.g., api.example.com). The SNICheck middleware fails to enforce the stricter TLS options for that Host because it does not expand wildcard entries in its lookup map. This affects the regular HTTPS and HTTP/2 paths and does not require HTTP/3 [1][2].

Impact

Successful exploitation allows the attacker to reach the wildcard-protected backend without presenting a valid client certificate, effectively bypassing mutual TLS authentication. This can lead to unauthorized access to services that require mTLS, potentially resulting in information disclosure or unauthorized operations depending on the backend configuration [1][2].

Mitigation

The vulnerability is fixed in Traefik v3.7.3, released concurrently with this advisory [3]. Organizations should update to this version or later. No workaround is provided for unpatched versions.

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

1

Patches

2
a664812e9c30

Compute resolved tlsOptions after applying models

https://github.com/traefik/traefikRomainJun 4, 2026Fixed in 3.7.3via llm-release-walk
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
    +}
    
5026ca97d026

Move snicheck to ctx instead of simulated routing

https://github.com/traefik/traefikJulien SalleyronMay 28, 2026Fixed in 3.7.2via llm-release-walk
19 files changed · +362 597
  • integration/fixtures/https/https_domain_fronting.toml+46 1 modified
    @@ -7,6 +7,10 @@
     
     [entryPoints.websecure]
       address = ":4443"
    +  [entryPoints.websecure.http3]
    +
    +[experimental]
    +  http3 = true
     
     [api]
       insecure = true
    @@ -32,6 +36,35 @@
       [http.routers.router3.tls]
         options = "mytls"
     
    +[http.routers.router4]
    +  rule = "Host(`site4.www.snitest.com`)"
    +  service = "service4"
    +  [http.routers.router4.tls]
    +
    +[http.routers.router4path]
    +  rule = "Host(`site4.www.snitest.com`) && PathPrefix(`/foo`)"
    +  service = "service4"
    +  [http.routers.router4path.tls]
    +    options = "mytls"
    +
    +[http.routers.router5]
    +  rule = "Host(`site5.www.snitest.com`)"
    +  service = "service5"
    +  [http.routers.router5.tls]
    +    options = "mytls"
    +
    +[http.routers.router5path]
    +  rule = "Host(`site5.www.snitest.com`) && PathPrefix(`/bar`)"
    +  service = "service5"
    +  [http.routers.router5path.tls]
    +    options = "mytls"
    +
    +[http.routers.router6]
    +  rule = "Host(`site6.www.snitest.com.`)"
    +  service = "service6"
    +  [http.routers.router6.tls]
    +    options = "mytls"
    +
     [http.services.service1]
       [[http.services.service1.loadBalancer.servers]]
         url = "http://127.0.0.1:9010"
    @@ -44,10 +77,22 @@
       [[http.services.service3.loadBalancer.servers]]
         url = "http://127.0.0.1:9030"
     
    +[http.services.service4]
    +  [[http.services.service4.loadBalancer.servers]]
    +    url = "http://127.0.0.1:9040"
    +
    +[http.services.service5]
    +  [[http.services.service5.loadBalancer.servers]]
    +    url = "http://127.0.0.1:9050"
    +
    +[http.services.service6]
    +  [[http.services.service6.loadBalancer.servers]]
    +    url = "http://127.0.0.1:9060"
    +
     [[tls.certificates]]
       certFile = "fixtures/https/wildcard.www.snitest.com.cert"
       keyFile = "fixtures/https/wildcard.www.snitest.com.key"
     
     [tls.options]
       [tls.options.mytls]
    -    maxVersion = "VersionTLS12"
    +    maxVersion = "VersionTLS13"
    
  • integration/https_test.go+37 24 modified
    @@ -13,6 +13,7 @@ import (
     	"time"
     
     	"github.com/BurntSushi/toml"
    +	"github.com/quic-go/quic-go/http3"
     	"github.com/stretchr/testify/assert"
     	"github.com/stretchr/testify/require"
     	"github.com/stretchr/testify/suite"
    @@ -254,7 +255,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
     	assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion")
     
     	// with unknown tls option
    -	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains(fmt.Sprintf("found different TLS options for routers on the same host %v, so using the default TLS options instead", tr4.TLSClientConfig.ServerName)))
    +	err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead"))
     	require.NoError(s.T(), err)
     }
     
    @@ -995,19 +996,20 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
     	defer backend2.Close()
     	backend3 := startTestServer("9030", http.StatusOK, "server3")
     	defer backend3.Close()
    +	backend5 := startTestServer("9050", http.StatusOK, "server5")
    +	defer backend5.Close()
     
     	file := s.adaptFile("fixtures/https/https_domain_fronting.toml", struct{}{})
     	s.traefikCmd(withConfigFile(file))
     
     	// wait for Traefik
    -	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)"))
    +	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1000*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)"))
     	require.NoError(s.T(), err)
     
     	testCases := []struct {
     		desc               string
     		hostHeader         string
     		serverName         string
    -		expectedError      bool
     		expectedContent    string
     		expectedStatusCode int
     	}{
    @@ -1025,29 +1027,13 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
     			expectedContent:    "server3",
     			expectedStatusCode: http.StatusOK,
     		},
    -		{
    -			desc:               "Spaces after the host header",
    -			hostHeader:         "site3.www.snitest.com ",
    -			serverName:         "site3.www.snitest.com",
    -			expectedError:      true,
    -			expectedContent:    "server3",
    -			expectedStatusCode: http.StatusOK,
    -		},
     		{
     			desc:               "Spaces after the servername",
     			hostHeader:         "site3.www.snitest.com",
     			serverName:         "site3.www.snitest.com ",
     			expectedContent:    "server3",
     			expectedStatusCode: http.StatusOK,
     		},
    -		{
    -			desc:               "Spaces after the servername and host header",
    -			hostHeader:         "site3.www.snitest.com ",
    -			serverName:         "site3.www.snitest.com ",
    -			expectedError:      true,
    -			expectedContent:    "server3",
    -			expectedStatusCode: http.StatusOK,
    -		},
     		{
     			desc:               "Domain Fronting with same tlsOptions should follow header",
     			hostHeader:         "site1.www.snitest.com",
    @@ -1083,6 +1069,34 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
     			expectedContent:    "server1",
     			expectedStatusCode: http.StatusOK,
     		},
    +		{
    +			desc:               "Domain Fronting with ambiguous TLS options should produce a 421",
    +			hostHeader:         "site4.www.snitest.com",
    +			serverName:         "site3.www.snitest.com",
    +			expectedContent:    "",
    +			expectedStatusCode: http.StatusMisdirectedRequest,
    +		},
    +		{
    +			desc:               "Domain Fronting with same non-default TLS options should not produce a 421",
    +			hostHeader:         "site5.www.snitest.com",
    +			serverName:         "site3.www.snitest.com",
    +			expectedContent:    "server5",
    +			expectedStatusCode: http.StatusOK,
    +		},
    +		{
    +			desc:               "FQDN host header with empty SNI to non-default TLS options route should produce a 421",
    +			hostHeader:         "site3.www.snitest.com.",
    +			serverName:         "",
    +			expectedContent:    "",
    +			expectedStatusCode: http.StatusMisdirectedRequest,
    +		},
    +		{
    +			desc:               "Non-FQDN host header with empty SNI matching FQDN route rule should produce a 421",
    +			hostHeader:         "site6.www.snitest.com",
    +			serverName:         "",
    +			expectedContent:    "",
    +			expectedStatusCode: http.StatusMisdirectedRequest,
    +		},
     	}
     
     	for _, test := range testCases {
    @@ -1091,11 +1105,10 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
     		req.Host = test.hostHeader
     
     		err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent))
    -		if test.expectedError {
    -			assert.Error(s.T(), err)
    -		} else {
    -			require.NoError(s.T(), err)
    -		}
    +		assert.NoError(s.T(), err, "test %s failed with: %v", test.desc, err)
    +
    +		err = try.RequestWithTransport(req, 500*time.Millisecond, &http3.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent))
    +		assert.NoError(s.T(), err, "test %s failed with: %v", test.desc, err)
     	}
     }
     
    
  • integration/simple_test.go+1 1 modified
    @@ -667,7 +667,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
     	s.traefikCmd(withConfigFile(file))
     
     	// All errors
    -	err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host snitest.net, so using the default TLS options instead"]`))
    +	err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`))
     	require.NoError(s.T(), err)
     
     	// router3 has an error because it uses an unknown entrypoint
    
  • integration/try/try.go+3 3 modified
    @@ -76,7 +76,7 @@ func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCon
     // the condition on the response.
     // ResponseCondition may be nil, in which case only the request against the URL must
     // succeed.
    -func RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error {
    +func RequestWithTransport(req *http.Request, timeout time.Duration, transport http.RoundTripper, conditions ...ResponseCondition) error {
     	resp, err := doTryRequest(req, timeout, transport, conditions...)
     
     	if resp != nil && resp.Body != nil {
    @@ -140,12 +140,12 @@ func doTryRequest(request *http.Request, timeout time.Duration, transport http.R
     func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport http.RoundTripper, conditions ...ResponseCondition) (*http.Response, error) {
     	var resp *http.Response
     	return resp, action(timeout, func() error {
    -		var err error
    -		client := http.DefaultClient
    +		var client http.Client
     		if transport != nil {
     			client.Transport = transport
     		}
     
    +		var err error
     		resp, err = client.Do(request)
     		if err != nil {
     			return err
    
  • pkg/config/dynamic/http_config.go+4 3 modified
    @@ -103,9 +103,10 @@ func (r *RouterDeniedEncodedPathCharacters) Map() map[string]struct{} {
     
     // RouterTLSConfig holds the TLS configuration for a router.
     type RouterTLSConfig struct {
    -	Options      string         `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"`
    -	CertResolver string         `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"`
    -	Domains      []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"`
    +	Options         string         `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"`
    +	ResolvedOptions string         `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"false"`
    +	CertResolver    string         `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"`
    +	Domains         []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"`
     }
     
     // +k8s:deepcopy-gen=true
    
  • pkg/middlewares/snicheck/snicheck.go+19 82 modified
    @@ -1,24 +1,26 @@
     package snicheck
     
     import (
    -	"net"
     	"net/http"
    -	"strings"
     
     	"github.com/traefik/traefik/v2/pkg/log"
    -	"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
    -	traefiktls "github.com/traefik/traefik/v2/pkg/tls"
    +	"github.com/traefik/traefik/v2/pkg/tcp"
     )
     
     // SNICheck is an HTTP handler that checks whether the TLS configuration for the server name is the same as for the host header.
     type SNICheck struct {
    -	next              http.Handler
    -	tlsOptionsForHost map[string]string
    +	next           http.Handler
    +	routerName     string
    +	tlsOptionsName string
     }
     
     // New creates a new SNICheck.
    -func New(tlsOptionsForHost map[string]string, next http.Handler) *SNICheck {
    -	return &SNICheck{next: next, tlsOptionsForHost: tlsOptionsForHost}
    +func New(routerName, tlsOptionsName string, next http.Handler) *SNICheck {
    +	return &SNICheck{
    +		next:           next,
    +		routerName:     routerName,
    +		tlsOptionsName: tlsOptionsName,
    +	}
     }
     
     func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
    @@ -27,81 +29,16 @@ func (s SNICheck) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
     		return
     	}
     
    -	host := getHost(req)
    -	serverName := strings.TrimSpace(req.TLS.ServerName)
    -
    -	// Domain Fronting
    -	if !strings.EqualFold(host, serverName) {
    -		tlsOptionHeader := findTLSOptionName(s.tlsOptionsForHost, host, true)
    -		tlsOptionSNI := findTLSOptionName(s.tlsOptionsForHost, serverName, false)
    -
    -		if tlsOptionHeader != tlsOptionSNI {
    -			log.WithoutContext().
    -				WithField("host", host).
    -				WithField("req.Host", req.Host).
    -				WithField("req.TLS.ServerName", req.TLS.ServerName).
    -				Debugf("TLS options difference: SNI:%s, Header:%s", tlsOptionSNI, tlsOptionHeader)
    -			http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest)
    -			return
    -		}
    +	tlsOptionsNameUsed := tcp.GetTLSOptionsName(req.Context())
    +	if s.tlsOptionsName != tlsOptionsNameUsed {
    +		log.WithoutContext().
    +			WithField("routerName", s.routerName).
    +			WithField("req.Host", req.Host).
    +			WithField("req.TLS.ServerName", req.TLS.ServerName).
    +			Debugf("TLS options difference: SNI:%s, Header:%s", tlsOptionsNameUsed, s.tlsOptionsName)
    +		http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest)
    +		return
     	}
     
     	s.next.ServeHTTP(rw, req)
     }
    -
    -func getHost(req *http.Request) string {
    -	h := requestdecorator.GetCNAMEFlatten(req.Context())
    -	if h != "" {
    -		return h
    -	}
    -
    -	h = requestdecorator.GetCanonizedHost(req.Context())
    -	if h != "" {
    -		return h
    -	}
    -
    -	host, _, err := net.SplitHostPort(req.Host)
    -	if err != nil {
    -		host = req.Host
    -	}
    -
    -	return strings.TrimSpace(host)
    -}
    -
    -func findTLSOptionName(tlsOptionsForHost map[string]string, host string, fqdn bool) string {
    -	name := findTLSOptName(tlsOptionsForHost, host, fqdn)
    -	if name != "" {
    -		return name
    -	}
    -
    -	name = findTLSOptName(tlsOptionsForHost, strings.ToLower(host), fqdn)
    -	if name != "" {
    -		return name
    -	}
    -
    -	return traefiktls.DefaultTLSConfigName
    -}
    -
    -func findTLSOptName(tlsOptionsForHost map[string]string, host string, fqdn bool) string {
    -	if tlsOptions, ok := tlsOptionsForHost[host]; ok {
    -		return tlsOptions
    -	}
    -
    -	if !fqdn {
    -		return ""
    -	}
    -
    -	if last := len(host) - 1; last >= 0 && host[last] == '.' {
    -		if tlsOptions, ok := tlsOptionsForHost[host[:last]]; ok {
    -			return tlsOptions
    -		}
    -
    -		return ""
    -	}
    -
    -	if tlsOptions, ok := tlsOptionsForHost[host+"."]; ok {
    -		return tlsOptions
    -	}
    -
    -	return ""
    -}
    
  • pkg/middlewares/snicheck/snicheck_test.go+0 59 removed
    @@ -1,59 +0,0 @@
    -package snicheck
    -
    -import (
    -	"net/http"
    -	"net/http/httptest"
    -	"testing"
    -
    -	"github.com/stretchr/testify/assert"
    -)
    -
    -func TestSNICheck_ServeHTTP(t *testing.T) {
    -	testCases := []struct {
    -		desc              string
    -		tlsOptionsForHost map[string]string
    -		host              string
    -		expected          int
    -	}{
    -		{
    -			desc:     "no TLS options",
    -			expected: http.StatusOK,
    -		},
    -		{
    -			desc: "with TLS options",
    -			tlsOptionsForHost: map[string]string{
    -				"example.com": "foo",
    -			},
    -			expected: http.StatusOK,
    -		},
    -		{
    -			desc: "server name and host doesn't have the same TLS configuration",
    -			tlsOptionsForHost: map[string]string{
    -				"example.com": "foo",
    -			},
    -			host:     "example.com",
    -			expected: http.StatusMisdirectedRequest,
    -		},
    -	}
    -
    -	for _, test := range testCases {
    -		t.Run(test.desc, func(t *testing.T) {
    -			t.Parallel()
    -
    -			next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
    -
    -			sniCheck := New(test.tlsOptionsForHost, next)
    -
    -			req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
    -			if test.host != "" {
    -				req.Host = test.host
    -			}
    -
    -			recorder := httptest.NewRecorder()
    -
    -			sniCheck.ServeHTTP(recorder, req)
    -
    -			assert.Equal(t, test.expected, recorder.Code)
    -		})
    -	}
    -}
    
  • pkg/muxer/tcp/mux.go+3 3 modified
    @@ -61,10 +61,10 @@ type ConnData struct {
     }
     
     // NewConnData builds a connData struct from the given parameters.
    -func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (ConnData, error) {
    -	remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
    +func NewConnData(serverName string, remoteAddr net.Addr, alpnProtos []string) (ConnData, error) {
    +	remoteIP, _, err := net.SplitHostPort(remoteAddr.String())
     	if err != nil {
    -		return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
    +		return ConnData{}, fmt.Errorf("parsing remote address %q: %w", remoteAddr.String(), err)
     	}
     
     	// as per https://datatracker.ietf.org/doc/html/rfc6066:
    
  • pkg/muxer/tcp/mux_test.go+1 1 modified
    @@ -532,7 +532,7 @@ func Test_addTCPRoute(t *testing.T) {
     				remoteAddr: fakeAddr{addr: addr},
     			}
     
    -			connData, err := NewConnData(test.serverName, conn, test.protos)
    +			connData, err := NewConnData(test.serverName, conn.RemoteAddr(), test.protos)
     			require.NoError(t, err)
     
     			matchingHandler, _ := router.Match(connData)
    
  • pkg/server/aggregator.go+88 8 modified
    @@ -1,13 +1,16 @@
     package server
     
     import (
    +	"context"
    +	"fmt"
     	"slices"
     
     	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
     	"github.com/traefik/traefik/v2/pkg/config/dynamic"
     	"github.com/traefik/traefik/v2/pkg/log"
    +	httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
     	"github.com/traefik/traefik/v2/pkg/server/provider"
    -	"github.com/traefik/traefik/v2/pkg/tls"
    +	traefiktls "github.com/traefik/traefik/v2/pkg/tls"
     )
     
     func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoints []string) dynamic.Configuration {
    @@ -31,8 +34,8 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
     			Services: make(map[string]*dynamic.UDPService),
     		},
     		TLS: &dynamic.TLSConfiguration{
    -			Stores:  make(map[string]tls.Store),
    -			Options: make(map[string]tls.Options),
    +			Stores:  make(map[string]traefiktls.Store),
    +			Options: make(map[string]traefiktls.Options),
     		},
     	}
     
    @@ -101,7 +104,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
     			}
     
     			for key, store := range configuration.TLS.Stores {
    -				if key != tls.DefaultTLSStoreName {
    +				if key != traefiktls.DefaultTLSStoreName {
     					key = provider.MakeQualifiedName(pvd, key)
     				} else {
     					defaultTLSStoreProviders = append(defaultTLSStoreProviders, pvd)
    @@ -123,19 +126,96 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
     
     	if len(defaultTLSStoreProviders) > 1 {
     		log.WithoutContext().Errorf("Default TLS Store defined in multiple providers: %v", defaultTLSStoreProviders)
    -		delete(conf.TLS.Stores, tls.DefaultTLSStoreName)
    +		delete(conf.TLS.Stores, traefiktls.DefaultTLSStoreName)
     	}
     
     	if len(defaultTLSOptionProviders) == 0 {
    -		conf.TLS.Options[tls.DefaultTLSConfigName] = tls.DefaultTLSOptions
    +		conf.TLS.Options[traefiktls.DefaultTLSConfigName] = traefiktls.DefaultTLSOptions
     	} else if len(defaultTLSOptionProviders) > 1 {
     		log.WithoutContext().Errorf("Default TLS Options defined in multiple providers %v", defaultTLSOptionProviders)
     		// We do not set an empty tls.TLS{} as above so that we actually get a "cascading failure" later on,
     		// i.e. routers depending on this missing TLS option will fail to initialize as well.
    -		delete(conf.TLS.Options, tls.DefaultTLSConfigName)
    +		delete(conf.TLS.Options, traefiktls.DefaultTLSConfigName)
     	}
     
    -	return conf
    +	return resolveHTTPTLSOptions(conf)
    +}
    +
    +func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration {
    +	if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 {
    +		return cfg
    +	}
    +
    +	rts := make(map[string]*dynamic.Router)
    +
    +	// Keyed by domain, then by options reference.
    +	// The actual source of truth for what TLS options will actually be used for the connection.
    +	// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
    +	// options that occur for a given host name, so that later on we can set relevant
    +	// errors and logging for all the routers concerned (i.e. wrongly configured).
    +	tlsOptionsForHostSNI := map[string]map[string][]string{}
    +
    +	for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers {
    +		rts[routerHTTPName] = routerHTTPConfig.DeepCopy()
    +
    +		if routerHTTPConfig.TLS == nil {
    +			continue
    +		}
    +
    +		ctxRouter := log.With(provider.AddInContext(context.Background(), routerHTTPName), log.Str(log.RouterName, routerHTTPName))
    +		logger := log.FromContext(ctxRouter)
    +
    +		tlsOptionsName := traefiktls.DefaultTLSConfigName
    +		if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
    +			tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
    +		}
    +
    +		domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
    +		if err != nil {
    +			routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
    +			logger.Error(routerErr)
    +			continue
    +		}
    +
    +		if len(domains) == 0 {
    +			rts[routerHTTPName].TLS.ResolvedOptions = "default"
    +			logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
    +		}
    +
    +		for _, domain := range domains {
    +			// domain is already in lower case thanks to the domain parsing
    +			if tlsOptionsForHostSNI[domain] == nil {
    +				tlsOptionsForHostSNI[domain] = make(map[string][]string)
    +			}
    +			tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName)
    +		}
    +	}
    +
    +	for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
    +		if len(tlsConfigs) == 1 {
    +			for optionsName, v := range tlsConfigs {
    +				log.WithoutContext().Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
    +				for _, s := range v {
    +					rts[s].TLS.ResolvedOptions = optionsName
    +				}
    +			}
    +			continue
    +		}
    +
    +		// multiple tlsConfigs
    +		routers := make([]string, 0, len(tlsConfigs))
    +		for _, v := range tlsConfigs {
    +			for _, s := range v {
    +				rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
    +				routers = append(routers, s)
    +			}
    +		}
    +
    +		log.WithoutContext().Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
    +	}
    +
    +	cfg.HTTP.Routers = rts
    +	return cfg
     }
     
     func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
    
  • pkg/server/router/router.go+7 0 modified
    @@ -16,6 +16,7 @@ import (
     	"github.com/traefik/traefik/v2/pkg/middlewares/denyrouterrecursion"
     	metricsMiddle "github.com/traefik/traefik/v2/pkg/middlewares/metrics"
     	"github.com/traefik/traefik/v2/pkg/middlewares/recovery"
    +	"github.com/traefik/traefik/v2/pkg/middlewares/snicheck"
     	"github.com/traefik/traefik/v2/pkg/middlewares/tracing"
     	httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
     	"github.com/traefik/traefik/v2/pkg/server/middleware"
    @@ -229,6 +230,12 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
     		})
     	}
     
    +	if router.TLS != nil {
    +		chain = chain.Append(func(next http.Handler) (http.Handler, error) {
    +			return snicheck.New(routerName, router.TLS.ResolvedOptions, next), nil
    +		})
    +	}
    +
     	return chain.Extend(*mHandler).Append(tHandler).Then(sHandler)
     }
     
    
  • pkg/server/router/tcp/manager.go+24 83 modified
    @@ -2,7 +2,6 @@ package tcp
     
     import (
     	"context"
    -	"crypto/tls"
     	"errors"
     	"fmt"
     	"math"
    @@ -11,7 +10,6 @@ import (
     
     	"github.com/traefik/traefik/v2/pkg/config/runtime"
     	"github.com/traefik/traefik/v2/pkg/log"
    -	"github.com/traefik/traefik/v2/pkg/middlewares/snicheck"
     	httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
     	tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
     	"github.com/traefik/traefik/v2/pkg/server/provider"
    @@ -91,11 +89,6 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls
     	return make(map[string]map[string]*runtime.RouterInfo)
     }
     
    -type nameAndConfig struct {
    -	routerName string // just so we have it as additional information when logging
    -	TLSConfig  *tls.Config
    -}
    -
     func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) {
     	// Build a new Router.
     	router, err := NewRouter()
    @@ -113,18 +106,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
     		log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
     	}
     
    -	// Keyed by domain. The source of truth for doing SNI checking (domain fronting).
    -	// As soon as there's (at least) two different tlsOptions found for the same domain,
    -	// we set the value to the default TLS conf.
    -	tlsOptionsForHost := map[string]string{}
    -
    -	// Keyed by domain, then by options reference.
    -	// The actual source of truth for what TLS options will actually be used for the connection.
    -	// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
    -	// options that occur for a given host name, so that later on we can set relevant
    -	// errors and logging for all the routers concerned (i.e. wrongly configured).
    -	tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
    -
     	for routerHTTPName, routerHTTPConfig := range configsHTTP {
     		if routerHTTPConfig.TLS == nil {
     			continue
    @@ -133,11 +114,6 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
     		ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName))
     		logger := log.FromContext(ctxRouter)
     
    -		tlsOptionsName := traefiktls.DefaultTLSConfigName
    -		if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
    -			tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
    -		}
    -
     		domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
     		if err != nil {
     			routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
    @@ -152,7 +128,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
     			// This is only about choosing the TLS configuration.
     			// The actual routing will be done further on by the HTTPS handler.
     			// See examples below.
    -			router.AddHTTPTLSConfig("*", defaultTLSConf)
    +			router.AddHTTPTLSConfig("*", defaultTLSConf, traefiktls.DefaultTLSConfigName)
     
     			// The server name (from a Host(SNI) rule) is the only parameter (available in HTTP routing rules) on which we can map a TLS config,
     			// because it is the only one accessible before decryption (we obtain it during the ClientHello).
    @@ -180,79 +156,43 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
     			logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
     		}
     
    +		// Even if the TLS options mismatch between the configured and the resolved one is handled in the aggregator
    +		// we also have to handle it here to be able to mark the router in error.
    +		tlsOptionsName := traefiktls.DefaultTLSConfigName
    +		if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
    +			tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
    +		}
    +
    +		if routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
    +			routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
    +		}
    +
     		// Even though the error is seemingly ignored (aside from logging it),
     		// we actually rely later on the fact that a tls config is nil (which happens when an error is returned) to take special steps
     		// when assigning a handler to a route.
    -		tlsConf, tlsConfErr := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
    +		tlsConf, tlsConfErr := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, routerHTTPConfig.TLS.ResolvedOptions)
     		if tlsConfErr != nil {
     			// Note: we do not call AddError here because we already did so when buildRouterHandler errored for the same reason.
     			logger.Error(tlsConfErr)
     		}
     
     		for _, domain := range domains {
    -			// domain is already in lower case thanks to the domain parsing
    -			if tlsOptionsForHostSNI[domain] == nil {
    -				tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
    -			}
    -			tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{
    -				routerName: routerHTTPName,
    -				TLSConfig:  tlsConf,
    -			}
    -
    -			if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName {
    -				// Different tlsOptions on the same domain, so fallback to default
    -				tlsOptionsForHost[domain] = traefiktls.DefaultTLSConfigName
    -			} else {
    -				tlsOptionsForHost[domain] = tlsOptionsName
    -			}
    -		}
    -	}
    -
    -	sniCheck := snicheck.New(tlsOptionsForHost, handlerHTTPS)
    -
    -	// Keep in mind that defaultTLSConf might be nil here.
    -	router.SetHTTPSHandler(sniCheck, defaultTLSConf)
    -
    -	logger := log.FromContext(ctx)
    -	for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
    -		if len(tlsConfigs) == 1 {
    -			var optionsName string
    -			var config *tls.Config
    -			for k, v := range tlsConfigs {
    -				optionsName = k
    -				config = v.TLSConfig
    -				break
    -			}
    -
    -			if config == nil {
    +			if tlsConf == nil {
     				// we use nil config as a signal to insert a handler
     				// that enforces that TLS connection attempts to the corresponding (broken) router should fail.
    -				logger.Debugf("Adding special closing route for %s because broken TLS options %s", hostSNI, optionsName)
    -				router.AddHTTPTLSConfig(hostSNI, nil)
    +				logger.Debugf("Adding special closing route for %s because of a broken TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions)
    +				router.AddHTTPTLSConfig(domain, nil, "")
     				continue
     			}
     
    -			logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
    -			router.AddHTTPTLSConfig(hostSNI, config)
    -			continue
    +			logger.Debugf("Adding route for %s with TLS options %s", domain, routerHTTPConfig.TLS.ResolvedOptions)
    +			router.AddHTTPTLSConfig(domain, tlsConf, routerHTTPConfig.TLS.ResolvedOptions)
     		}
    -
    -		// multiple tlsConfigs
    -
    -		routers := make([]string, 0, len(tlsConfigs))
    -		for _, v := range tlsConfigs {
    -			configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
    -			routers = append(routers, v.routerName)
    -		}
    -
    -		logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
    -		if defaultTLSConf == nil {
    -			logger.Debugf("Adding special closing route for %s because broken default TLS options", hostSNI)
    -		}
    -
    -		router.AddHTTPTLSConfig(hostSNI, defaultTLSConf)
     	}
     
    +	// Keep in mind that defaultTLSConf might be nil here.
    +	router.SetHTTPSHandler(handlerHTTPS, defaultTLSConf)
    +
     	m.addTCPHandlers(ctx, configs, router)
     
     	return router, nil
    @@ -385,8 +325,9 @@ func (m *Manager) addTCPHandlers(ctx context.Context, configs map[string]*runtim
     		}
     
     		handler = &tcp.TLSHandler{
    -			Next:   handler,
    -			Config: tlsConf,
    +			Next:           handler,
    +			Config:         tlsConf,
    +			TLSOptionsName: tlsOptionsName,
     		}
     
     		logger.Debugf("Adding TLS route for %q", routerConfig.Rule)
    
  • pkg/server/router/tcp/manager_test.go+4 296 modified
    @@ -1,14 +1,10 @@
     package tcp
     
     import (
    -	"crypto/tls"
     	"math"
    -	"net/http"
    -	"net/http/httptest"
     	"testing"
     
     	"github.com/stretchr/testify/assert"
    -	"github.com/stretchr/testify/require"
     	"github.com/traefik/traefik/v2/pkg/config/dynamic"
     	"github.com/traefik/traefik/v2/pkg/config/runtime"
     	tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp"
    @@ -129,7 +125,8 @@ func TestRuntimeConfiguration(t *testing.T) {
     						Service:     "foo-service",
     						Rule:        "Host(`bar.foo`)",
     						TLS: &dynamic.RouterTLSConfig{
    -							Options: "foo",
    +							Options:         "foo",
    +							ResolvedOptions: "default",
     						},
     					},
     				},
    @@ -139,7 +136,8 @@ func TestRuntimeConfiguration(t *testing.T) {
     						Service:     "foo-service",
     						Rule:        "Host(`bar.foo`) && PathPrefix(`/path`)",
     						TLS: &dynamic.RouterTLSConfig{
    -							Options: "bar",
    +							Options:         "bar",
    +							ResolvedOptions: "default",
     						},
     					},
     				},
    @@ -396,293 +394,3 @@ func TestRuntimeConfiguration(t *testing.T) {
     		})
     	}
     }
    -
    -func TestDomainFronting(t *testing.T) {
    -	tlsOptionsBase := map[string]traefiktls.Options{
    -		"default": {
    -			MinVersion: "VersionTLS10",
    -		},
    -		"host1@file": {
    -			MinVersion: "VersionTLS12",
    -		},
    -		"host1@crd": {
    -			MinVersion: "VersionTLS12",
    -		},
    -	}
    -
    -	entryPoints := []string{"web"}
    -
    -	tests := []struct {
    -		desc           string
    -		routers        map[string]*runtime.RouterInfo
    -		tlsOptions     map[string]traefiktls.Options
    -		host           string
    -		ServerName     string
    -		expectedStatus int
    -	}{
    -		{
    -			desc: "Request is misdirected when TLS options are different",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -				"router-2@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host2.local`)",
    -						TLS:         &dynamic.RouterTLSConfig{},
    -					},
    -				},
    -			},
    -			tlsOptions:     tlsOptionsBase,
    -			host:           "host1.local",
    -			ServerName:     "host2.local",
    -			expectedStatus: http.StatusMisdirectedRequest,
    -		},
    -		{
    -			desc: "Request is OK when TLS options are the same",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -				"router-2@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host2.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -			},
    -			tlsOptions:     tlsOptionsBase,
    -			host:           "host1.local",
    -			ServerName:     "host2.local",
    -			expectedStatus: http.StatusOK,
    -		},
    -		{
    -			desc: "Default TLS options is used when options are ambiguous for the same host",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -				"router-2@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`) && PathPrefix(`/foo`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "default",
    -						},
    -					},
    -				},
    -				"router-3@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host2.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -			},
    -			tlsOptions:     tlsOptionsBase,
    -			host:           "host1.local",
    -			ServerName:     "host2.local",
    -			expectedStatus: http.StatusMisdirectedRequest,
    -		},
    -		{
    -			desc: "Default TLS options should not be used when options are the same for the same host",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -				"router-2@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`) && PathPrefix(`/bar`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -				"router-3@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host2.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -			},
    -			tlsOptions:     tlsOptionsBase,
    -			host:           "host1.local",
    -			ServerName:     "host2.local",
    -			expectedStatus: http.StatusOK,
    -		},
    -		{
    -			desc: "Request is misdirected when TLS options have the same name but from different providers",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -				"router-2@crd": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host2.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1",
    -						},
    -					},
    -				},
    -			},
    -			tlsOptions:     tlsOptionsBase,
    -			host:           "host1.local",
    -			ServerName:     "host2.local",
    -			expectedStatus: http.StatusMisdirectedRequest,
    -		},
    -		{
    -			desc: "Request is OK when TLS options reference from a different provider is the same",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1@crd",
    -						},
    -					},
    -				},
    -				"router-2@crd": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host2.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1@crd",
    -						},
    -					},
    -				},
    -			},
    -			tlsOptions:     tlsOptionsBase,
    -			host:           "host1.local",
    -			ServerName:     "host2.local",
    -			expectedStatus: http.StatusOK,
    -		},
    -		{
    -			desc: "Request is misdirected when server name is empty and the host name is an FQDN, but router's rule is not",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1@file",
    -						},
    -					},
    -				},
    -			},
    -			tlsOptions: map[string]traefiktls.Options{
    -				"default": {
    -					MinVersion: "VersionTLS13",
    -				},
    -				"host1@file": {
    -					MinVersion: "VersionTLS12",
    -				},
    -			},
    -			host:           "host1.local.",
    -			expectedStatus: http.StatusMisdirectedRequest,
    -		},
    -		{
    -			desc: "Request is misdirected when server name is empty and the host name is not FQDN, but router's rule is",
    -			routers: map[string]*runtime.RouterInfo{
    -				"router-1@file": {
    -					Router: &dynamic.Router{
    -						EntryPoints: entryPoints,
    -						Rule:        "Host(`host1.local.`)",
    -						TLS: &dynamic.RouterTLSConfig{
    -							Options: "host1@file",
    -						},
    -					},
    -				},
    -			},
    -			tlsOptions: map[string]traefiktls.Options{
    -				"default": {
    -					MinVersion: "VersionTLS13",
    -				},
    -				"host1@file": {
    -					MinVersion: "VersionTLS12",
    -				},
    -			},
    -			host:           "host1.local",
    -			expectedStatus: http.StatusMisdirectedRequest,
    -		},
    -	}
    -
    -	for _, test := range tests {
    -		t.Run(test.desc, func(t *testing.T) {
    -			conf := &runtime.Configuration{
    -				Routers: test.routers,
    -			}
    -
    -			serviceManager := tcp.NewManager(conf)
    -
    -			tlsManager := traefiktls.NewManager()
    -			tlsManager.UpdateConfigs(t.Context(), map[string]traefiktls.Store{}, test.tlsOptions, []*traefiktls.CertAndStores{})
    -
    -			httpsHandler := map[string]http.Handler{
    -				"web": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}),
    -			}
    -
    -			middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
    -
    -			routerManager := NewManager(conf, serviceManager, middlewaresBuilder, nil, httpsHandler, tlsManager)
    -
    -			routers := routerManager.BuildHandlers(t.Context(), entryPoints)
    -
    -			router, ok := routers["web"]
    -			require.True(t, ok)
    -
    -			req := httptest.NewRequest(http.MethodGet, "/", nil)
    -			req.Host = test.host
    -			req.TLS = &tls.ConnectionState{
    -				ServerName: test.ServerName,
    -			}
    -
    -			rw := httptest.NewRecorder()
    -
    -			router.GetHTTPSHandler().ServeHTTP(rw, req)
    -
    -			assert.Equal(t, test.expectedStatus, rw.Code)
    -		})
    -	}
    -}
    
  • pkg/server/router/tcp/router.go+34 17 modified
    @@ -17,11 +17,17 @@ import (
     	"github.com/traefik/traefik/v2/pkg/log"
     	tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
     	"github.com/traefik/traefik/v2/pkg/tcp"
    +	traefiktls "github.com/traefik/traefik/v2/pkg/tls"
     )
     
     // errClientHelloRead is used as a sentinel error to break the TLS handshake once we have read the ClientHello.
     var errClientHelloRead = errors.New("client hello successfully read")
     
    +type tlsConfigWithOptionsName struct {
    +	cfg         *tls.Config
    +	optionsName string
    +}
    +
     // Router is a TCP router.
     type Router struct {
     	acmeTLSPassthrough bool
    @@ -48,7 +54,7 @@ type Router struct {
     	httpsTLSConfig *tls.Config // default TLS config
     	// hostHTTPTLSConfig contains TLS configs keyed by SNI.
     	// A nil config is the hint to set up a brokenTLSRouter.
    -	hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI
    +	hostHTTPTLSConfig map[string]tlsConfigWithOptionsName // TLS configs keyed by SNI
     }
     
     // NewRouter returns a new TCP router.
    @@ -75,14 +81,20 @@ func NewRouter() (*Router, error) {
     	}, nil
     }
     
    -// GetTLSGetClientInfo is called after a ClientHello is received from a client.
    -func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) {
    -	return func(info *tls.ClientHelloInfo) (*tls.Config, error) {
    -		if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok {
    -			return tlsConfig, nil
    +// HTTP3TLSConfigMatcherFunc returns a matcher func for HTTP/3 which returns a tls.Config with its corresponding
    +// TLSOptionName matching the given HostSNI in the connection data, or the default TLS config if there is no match.
    +func (r *Router) HTTP3TLSConfigMatcherFunc() func(connData tcpmuxer.ConnData) (*tls.Config, string, error) {
    +	return func(connData tcpmuxer.ConnData) (*tls.Config, string, error) {
    +		h, _ := r.muxerHTTPS.Match(connData)
    +		if h == nil {
    +			return r.httpsTLSConfig, traefiktls.DefaultTLSConfigName, nil
     		}
     
    -		return r.httpsTLSConfig, nil
    +		if tlsHandler, ok := h.(*tcp.TLSHandler); ok {
    +			return tlsHandler.Config, tlsHandler.TLSOptionsName, nil
    +		}
    +
    +		return nil, "", errors.New("matching handler is not a TLSHandler")
     	}
     }
     
    @@ -94,7 +106,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
     	// we would block forever on clientHelloInfo,
     	// which is why we want to detect and handle that case first and foremost.
     	if r.muxerTCP.HasRoutes() && !r.muxerTCPTLS.HasRoutes() && !r.muxerHTTPS.HasRoutes() {
    -		connData, err := tcpmuxer.NewConnData("", conn, nil)
    +		connData, err := tcpmuxer.NewConnData("", conn.RemoteAddr(), nil)
     		if err != nil {
     			log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err)
     			conn.Close()
    @@ -136,7 +148,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
     		log.WithoutContext().Errorf("Error while setting deadline: %v", err)
     	}
     
    -	connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos)
    +	connData, err := tcpmuxer.NewConnData(hello.serverName, conn.RemoteAddr(), hello.protos)
     	if err != nil {
     		log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err)
     		conn.Close()
    @@ -212,12 +224,15 @@ func (r *Router) AddRoute(rule string, priority int, target tcp.Handler) error {
     }
     
     // AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig.
    -func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config) {
    +func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config, optionsName string) {
     	if r.hostHTTPTLSConfig == nil {
    -		r.hostHTTPTLSConfig = map[string]*tls.Config{}
    +		r.hostHTTPTLSConfig = map[string]tlsConfigWithOptionsName{}
     	}
     
    -	r.hostHTTPTLSConfig[sniHost] = config
    +	r.hostHTTPTLSConfig[sniHost] = tlsConfigWithOptionsName{
    +		cfg:         config,
    +		optionsName: optionsName,
    +	}
     }
     
     // GetConn creates a connection proxy with a peeked string.
    @@ -262,12 +277,13 @@ func (t *brokenTLSRouter) ServeTCP(conn tcp.WriteCloser) {
     func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
     	for sniHost, tlsConf := range r.hostHTTPTLSConfig {
     		var tcpHandler tcp.Handler
    -		if tlsConf == nil {
    +		if tlsConf.cfg == nil {
     			tcpHandler = &brokenTLSRouter{}
     		} else {
     			tcpHandler = &tcp.TLSHandler{
    -				Next:   handler,
    -				Config: tlsConf,
    +				Next:           handler,
    +				Config:         tlsConf.cfg,
    +				TLSOptionsName: tlsConf.optionsName,
     			}
     		}
     
    @@ -285,8 +301,9 @@ func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
     	}
     
     	r.httpsForwarder = &tcp.TLSHandler{
    -		Next:   handler,
    -		Config: r.httpsTLSConfig,
    +		Next:           handler,
    +		Config:         r.httpsTLSConfig,
    +		TLSOptionsName: "default",
     	}
     }
     
    
  • pkg/server/router/tcp/router_test.go+17 4 modified
    @@ -2,6 +2,7 @@ package tcp
     
     import (
     	"bytes"
    +	"context"
     	"crypto/tls"
     	"errors"
     	"fmt"
    @@ -20,7 +21,7 @@ import (
     	"github.com/traefik/traefik/v2/pkg/config/runtime"
     	tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp"
     	"github.com/traefik/traefik/v2/pkg/server/service/tcp"
    -	tcp2 "github.com/traefik/traefik/v2/pkg/tcp"
    +	traefiktcp "github.com/traefik/traefik/v2/pkg/tcp"
     	traefiktls "github.com/traefik/traefik/v2/pkg/tls"
     	"github.com/traefik/traefik/v2/pkg/tls/generate"
     )
    @@ -52,7 +53,7 @@ func (h *httpForwarder) Close() error {
     }
     
     // ServeTCP uses the connection to serve it later in "Accept".
    -func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) {
    +func (h *httpForwarder) ServeTCP(conn traefiktcp.WriteCloser) {
     	h.connChan <- conn
     }
     
    @@ -621,6 +622,16 @@ func Test_Routing(t *testing.T) {
     					_, err = fmt.Fprint(w, "HTTPS")
     					require.NoError(t, err)
     				}),
    +
    +				ConnContext: func(ctx context.Context, c net.Conn) context.Context {
    +					if tlsConn, ok := c.(*tls.Conn); ok {
    +						if tlsConnWithOptionsName, ok := tlsConn.NetConn().(traefiktcp.TLSConn); ok {
    +							return traefiktcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName)
    +						}
    +					}
    +
    +					return ctx
    +				},
     			}
     
     			stoppedHTTPS := make(chan struct{})
    @@ -812,7 +823,8 @@ func routerHTTPSPathPrefix(conf *runtime.Configuration) {
     			Service:     "http",
     			Rule:        "PathPrefix(`/`)",
     			TLS: &dynamic.RouterTLSConfig{
    -				Options: "tls10",
    +				Options:         "tls10",
    +				ResolvedOptions: "tls10",
     			},
     		},
     	}
    @@ -826,7 +838,8 @@ func routerHTTPS(conf *runtime.Configuration) {
     			Service:     "http",
     			Rule:        "Host(`foo.bar`)",
     			TLS: &dynamic.RouterTLSConfig{
    -				Options: "tls12",
    +				Options:         "tls12",
    +				ResolvedOptions: "tls12",
     			},
     		},
     	}
    
  • pkg/server/server_entrypoint_tcp.go+10 0 modified
    @@ -2,6 +2,7 @@ package server
     
     import (
     	"context"
    +	"crypto/tls"
     	"errors"
     	"expvar"
     	"fmt"
    @@ -610,6 +611,15 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
     		HTTP2: &http.HTTP2Config{
     			MaxConcurrentStreams: int(configuration.HTTP2.MaxConcurrentStreams),
     		},
    +		ConnContext: func(ctx context.Context, c net.Conn) context.Context {
    +			if tlsConn, ok := c.(*tls.Conn); ok {
    +				if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok {
    +					return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName)
    +				}
    +			}
    +
    +			return ctx
    +		},
     	}
     	if debugConnection || (configuration.Transport != nil && (configuration.Transport.KeepAliveMaxTime > 0 || configuration.Transport.KeepAliveMaxRequests > 0)) {
     		serverHTTP.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
    
  • pkg/server/server_entrypoint_tcp_http3.go+36 7 modified
    @@ -13,7 +13,9 @@ import (
     	"github.com/quic-go/quic-go/http3"
     	"github.com/traefik/traefik/v2/pkg/config/static"
     	"github.com/traefik/traefik/v2/pkg/log"
    +	tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
     	tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
    +	"github.com/traefik/traefik/v2/pkg/tcp"
     )
     
     type http3server struct {
    @@ -22,7 +24,7 @@ type http3server struct {
     	http3conn net.PacketConn
     
     	lock   sync.RWMutex
    -	getter func(info *tls.ClientHelloInfo) (*tls.Config, error)
    +	getter func(data tcpmuxer.ConnData) (*tls.Config, string, error)
     }
     
     func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, httpsServer *httpServer) (*http3server, error) {
    @@ -41,19 +43,27 @@ func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, https
     
     	h3 := &http3server{
     		http3conn: conn,
    -		getter: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
    -			return nil, errors.New("no tls config")
    +		getter: func(data tcpmuxer.ConnData) (*tls.Config, string, error) {
    +			return nil, "", errors.New("no TLS config")
     		},
     	}
     
     	h3.Server = &http3.Server{
     		Addr:      configuration.GetAddress(),
     		Port:      configuration.HTTP3.AdvertisedPort,
     		Handler:   httpsServer.Server.(*http.Server).Handler,
    -		TLSConfig: &tls.Config{GetConfigForClient: h3.getGetConfigForClient},
    +		TLSConfig: &tls.Config{GetConfigForClient: h3.getTLSConfigForClient},
     		QUICConfig: &quic.Config{
     			Allow0RTT: false,
     		},
    +		ConnContext: func(ctx context.Context, c *quic.Conn) context.Context {
    +			tlsOptionsName, err := h3.getTLSOptionsName(c)
    +			if err != nil {
    +				log.WithoutContext().Errorf("Error getting TLS options name for client: %v", err)
    +				return ctx
    +			}
    +			return tcp.AddTLSOptionsNameInContext(ctx, tlsOptionsName)
    +		},
     	}
     
     	previousHandler := httpsServer.Server.(*http.Server).Handler
    @@ -77,17 +87,36 @@ func (e *http3server) Switch(rt *tcprouter.Router) {
     	e.lock.Lock()
     	defer e.lock.Unlock()
     
    -	e.getter = rt.GetTLSGetClientInfo()
    +	e.getter = rt.HTTP3TLSConfigMatcherFunc()
     }
     
     func (e *http3server) Shutdown(_ context.Context) error {
     	// TODO: use e.Server.CloseGracefully() when available.
     	return e.Server.Close()
     }
     
    -func (e *http3server) getGetConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) {
    +func (e *http3server) getTLSConfigForClient(info *tls.ClientHelloInfo) (*tls.Config, error) {
    +	e.lock.RLock()
    +	defer e.lock.RUnlock()
    +
    +	connData, err := tcpmuxer.NewConnData(info.ServerName, info.Conn.RemoteAddr(), info.SupportedProtos)
    +	if err != nil {
    +		return nil, fmt.Errorf("creating ConnData from client hello: %w", err)
    +	}
    +
    +	conf, _, err := e.getter(connData)
    +	return conf, err
    +}
    +
    +func (e *http3server) getTLSOptionsName(c *quic.Conn) (string, error) {
     	e.lock.RLock()
     	defer e.lock.RUnlock()
     
    -	return e.getter(info)
    +	connData, err := tcpmuxer.NewConnData(c.ConnectionState().TLS.ServerName, c.RemoteAddr(), []string{c.ConnectionState().TLS.NegotiatedProtocol})
    +	if err != nil {
    +		return "", fmt.Errorf("creating ConnData from quic Conn: %w", err)
    +	}
    +
    +	_, name, err := e.getter(connData)
    +	return name, err
     }
    
  • pkg/server/server_entrypoint_tcp_http3_test.go+2 2 modified
    @@ -102,7 +102,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) {
     
     	router.AddHTTPTLSConfig("*", &tls.Config{
     		Certificates: []tls.Certificate{tlsCert},
    -	})
    +	}, traefiktls.DefaultTLSConfigName)
     	router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
     		rw.WriteHeader(http.StatusOK)
     	}), nil)
    @@ -164,7 +164,7 @@ func TestHTTP30RTT(t *testing.T) {
     
     	router.AddHTTPTLSConfig("example.com", &tls.Config{
     		Certificates: []tls.Certificate{tlsCert},
    -	})
    +	}, traefiktls.DefaultTLSConfigName)
     	router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
     		rw.WriteHeader(http.StatusOK)
     	}), nil)
    
  • pkg/tcp/tls.go+26 3 modified
    @@ -1,16 +1,39 @@
     package tcp
     
     import (
    +	"context"
     	"crypto/tls"
     )
     
    +// TLSConn is a TLS connection that also carries the name of the TLS config used.
    +type TLSConn struct {
    +	WriteCloser
    +
    +	TLSOptionsName string
    +}
    +
     // TLSHandler handles TLS connections.
     type TLSHandler struct {
    -	Next   Handler
    -	Config *tls.Config
    +	Next           Handler
    +	Config         *tls.Config
    +	TLSOptionsName string
     }
     
     // ServeTCP terminates the TLS connection.
     func (t *TLSHandler) ServeTCP(conn WriteCloser) {
    -	t.Next.ServeTCP(tls.Server(conn, t.Config))
    +	t.Next.ServeTCP(tls.Server(TLSConn{WriteCloser: conn, TLSOptionsName: t.TLSOptionsName}, t.Config))
    +}
    +
    +type tlsOptionsNameKey struct{}
    +
    +func AddTLSOptionsNameInContext(ctx context.Context, name string) context.Context {
    +	return context.WithValue(ctx, tlsOptionsNameKey{}, name)
    +}
    +
    +func GetTLSOptionsName(ctx context.Context) string {
    +	if name, ok := ctx.Value(tlsOptionsNameKey{}).(string); ok {
    +		return name
    +	}
    +
    +	return ""
     }
    

Vulnerability mechanics

Root cause

"SNICheck resolves TLS option names for the HTTP Host header using exact map lookups only, ignoring wildcard entries such as `*.example.com`, so a domain-fronted request bypasses the mTLS check."

Attack vector

An unauthenticated attacker completes a TLS handshake with a permissive SNI (for example `public.example.net`) on the same entrypoint. The attacker then sends an HTTP request with a `Host` header that targets a wildcard-protected backend (for example `api.example.com`). The `SNICheck` middleware in `snicheck.go` resolves the TLS option name for the HTTP `Host` header using exact map lookups only, ignoring the wildcard entry `*.example.com`, so it sees the same default TLS options for both SNI and header; consequently it does not reject the request with a `421 Misdirected Request`. The HTTPS router then dispatches the request to the wildcard route that was configured with `tls.options=mtls`, bypassing mutual TLS client certificate enforcement [ref_id=1][ref_id=2].

Affected code

The vulnerability is in `pkg/middlewares/snicheck/snicheck.go`. The functions `findTLSOptionName` and `findTLSOptName` resolve TLS option names using exact map lookups only, without any wildcard matching for entries such as `*.example.com` [ref_id=1]. The building of the SNI routing in `pkg/server/router/tcp/manager.go` also does not propagate resolved TLS option names for wildcard hosts [patch_id=6192609]. The advisory reports that the router build records exact domain entries only, while the `HostSNI` matching in the HTTPS forwarder is wildcard-aware—creating a mismatch [ref_id=2].

What the fix does

The patches remove the exact-map-lookup logic from `SNICheck` in `pkg/middlewares/snicheck/snicheck.go` and instead pass the already-populated `ResolvedOptions` field (computed in `resolveHTTPTLSOptions` in `pkg/server/aggregator.go`) via `context`. The resolver there iterates over all routers' parsed domains, determines the TLS option name for each host, and handles wildcard entries by marking any host that matches `*.example.com` with the correct qualified option name. If different routers on the same host specify different TLS options, the resolver falls back to the default TLS configuration [patch_id=6192608][patch_id=6192609]. The domain-fronting comparison now directly checks the `ResolvedOptions` string—which is already wildcard-aware—against the TLS option name used during the handshake, closing the bypass.

Preconditions

  • configA protected router uses a wildcard Host/HostSNI rule with router-specific TLSOptions that require mTLS (e.g. `RequireAndVerifyClientCert`).
  • configAnother permissive SNI or default TLS path exists on the same entrypoint that allows a TLS handshake without a client certificate.
  • inputThe client can send an HTTP Host header that differs from the TLS SNI (domain fronting).

Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.