TLSOptions: handle conflict: same host name, different TLS options

Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
This commit is contained in:
mpl 2019-07-03 19:22:05 +02:00 committed by Traefiker Bot
parent 9db9143366
commit 39aae4167e
6 changed files with 193 additions and 8 deletions

View file

@ -327,9 +327,15 @@ Traefik will terminate the SSL connections (meaning that it will send decrypted
#### `Options`
The `Options` field enables fine-grained control of the TLS parameters.
The `Options` field enables fine-grained control of the TLS parameters.
It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied only if a `Host` rule is defined.
!!! note "Server Name Association"
Even though one might get the impression that a TLS options reference is mapped to a router, or a router rule, one should realize that it is actually mapped only to the host name found in the `Host` part of the rule. Of course, there could also be several `Host` parts in a rule, in which case the TLS options reference would be mapped to as many host names.
Another thing to keep in mind is: the TLS option is picked from the mapping mentioned above and based on the server name provided during the TLS handshake, and it all happens before routing actually occurs.
??? example "Configuring the TLS options"
```toml tab="TOML"
@ -369,6 +375,40 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied
- TLS_RSA_WITH_AES_256_GCM_SHA384
```
!!! important "Conflicting TLS Options"
Since a TLS options reference is mapped to a host name, if a configuration introduces a situation where the same host name (from a `Host` rule) gets matched with two TLS options references, a conflict occurs, such as in the example below:
```toml tab="TOML"
[http.routers]
[http.routers.routerfoo]
rule = "Host(`snitest.com`) && Path(`/foo`)"
[http.routers.routerfoo.tls]
options="foo"
[http.routers]
[http.routers.routerbar]
rule = "Host(`snitest.com`) && Path(`/bar`)"
[http.routers.routerbar.tls]
options="bar"
```
```yaml tab="YAML"
http:
routers:
routerfoo:
rule: "Host(`snitest.com`) && Path(`/foo`)"
tls:
options: foo
routerbar:
rule: "Host(`snitest.com`) && Path(`/bar`)"
tls:
options: bar
```
If that happens, both mappings are discarded, and the host name (`snitest.com` in this case) for these routers gets associated with the default TLS options instead.
## Configuring TCP Routers
### General

View file

@ -42,6 +42,21 @@ spec:
singular: middleware
scope: Namespaced
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: tlsoptions.traefik.containo.us
spec:
group: traefik.containo.us
version: v1alpha1
names:
kind: TLSOption
plural: tlsoptions
singular: tlsoption
scope: Namespaced
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
@ -97,6 +112,14 @@ rules:
- get
- list
- watch
- apiGroups:
- traefik.containo.us
resources:
- tlsoptions
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding

View file

@ -35,6 +35,18 @@
[http.routers.router3.tls]
options = "unknown"
[http.routers.router4]
service = "service1"
rule = "Host(`snitest.net`)"
[http.routers.router4.tls]
options = "foo"
[http.routers.router5]
service = "service1"
rule = "Host(`snitest.net`)"
[http.routers.router5.tls]
options = "baz"
[http.services]
[http.services.service1]
[http.services.service1.loadBalancer]
@ -59,5 +71,11 @@
[tls.options.foo]
minversion = "VersionTLS11"
[tls.options.baz]
minversion = "VersionTLS11"
[tls.options.bar]
minversion = "VersionTLS12"
[tls.options.default]
minversion = "VersionTLS12"

View file

@ -195,6 +195,72 @@ func (s *HTTPSSuite) TestWithTLSOptions(c *check.C) {
c.Assert(err, checker.IsNil)
}
// TestWithConflictingTLSOptions checks that routers with same SNI but different TLS options get fallbacked to the default TLS options.
func (s *HTTPSSuite) TestWithConflictingTLSOptions(c *check.C) {
cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_tls_options.toml"))
defer display(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// wait for Traefik
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)"))
c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent)
backend2 := startTestServer("9020", http.StatusResetContent)
defer backend1.Close()
defer backend2.Close()
err = try.GetRequest(backend1.URL, 1*time.Second, try.StatusCodeIs(http.StatusNoContent))
c.Assert(err, checker.IsNil)
err = try.GetRequest(backend2.URL, 1*time.Second, try.StatusCodeIs(http.StatusResetContent))
c.Assert(err, checker.IsNil)
tr4 := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS11,
ServerName: "snitest.net",
},
}
trDefault := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MaxVersion: tls.VersionTLS12,
ServerName: "snitest.net",
},
}
// With valid TLS options and request
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
req.Host = trDefault.TLSClientConfig.ServerName
req.Header.Set("Host", trDefault.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
err = try.RequestWithTransport(req, 30*time.Second, trDefault, try.StatusCodeIs(http.StatusNoContent))
c.Assert(err, checker.IsNil)
// With a bad TLS version
req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil)
req.Host = tr4.TLSClientConfig.ServerName
req.Header.Set("Host", tr4.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
client := http.Client{
Transport: tr4,
}
_, err = client.Do(req)
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, "protocol version not supported")
// 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 option instead", tr4.TLSClientConfig.ServerName)))
c.Assert(err, checker.IsNil)
}
// TestWithSNIStrictNotMatchedRequest involves a client sending a SNI hostname of
// "snitest.org", which does not match the CN of 'snitest.com.crt'. The test
// verifies that traefik closes the connection.

View file

@ -2,6 +2,7 @@ package tcp
import (
"context"
"crypto/tls"
"fmt"
"net/http"
@ -11,7 +12,7 @@ import (
"github.com/containous/traefik/pkg/server/internal"
tcpservice "github.com/containous/traefik/pkg/server/service/tcp"
"github.com/containous/traefik/pkg/tcp"
"github.com/containous/traefik/pkg/tls"
traefiktls "github.com/containous/traefik/pkg/tls"
)
// NewManager Creates a new Manager
@ -19,7 +20,7 @@ func NewManager(conf *config.RuntimeConfiguration,
serviceManager *tcpservice.Manager,
httpHandlers map[string]http.Handler,
httpsHandlers map[string]http.Handler,
tlsManager *tls.Manager,
tlsManager *traefiktls.Manager,
) *Manager {
return &Manager{
serviceManager: serviceManager,
@ -35,7 +36,7 @@ type Manager struct {
serviceManager *tcpservice.Manager
httpHandlers map[string]http.Handler
httpsHandlers map[string]http.Handler
tlsManager *tls.Manager
tlsManager *traefiktls.Manager
conf *config.RuntimeConfiguration
}
@ -90,6 +91,12 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
type nameAndConfig struct {
routerName string // just so we have it as additional information when logging
TLSConfig *tls.Config
}
// Keyed by domain, then by options reference.
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName {
continue
@ -107,7 +114,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
}
if len(domains) == 0 {
logger.Warnf("The 'default' TLS options will be applied instead of %q as no domain has been found in the rule", routerHTTPConfig.TLS.Options)
logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the hostSNI of each request", routerHTTPConfig.Rule)
}
for _, domain := range domains {
@ -123,12 +130,44 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
logger.Debug(err)
continue
}
router.AddRouteHTTPTLS(domain, tlsConf)
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
}
tlsOptionsForHostSNI[domain][routerHTTPConfig.TLS.Options] = nameAndConfig{
routerName: routerHTTPName,
TLSConfig: tlsConf,
}
}
}
}
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
}
logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
router.AddRouteHTTPTLS(hostSNI, config)
} else {
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
// TODO: properly deal with critical errors VS non-critical errors
if configsHTTP[v.routerName].Err != "" {
configsHTTP[v.routerName].Err += "\n"
}
configsHTTP[v.routerName].Err += fmt.Sprintf("found different TLS options for routers on the same host %v, so using the default TLS option instead", hostSNI)
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)
router.AddRouteHTTPTLS(hostSNI, defaultTLSConf)
}
}
for routerName, routerConfig := range configs {
ctxRouter := log.With(internal.AddProviderInContext(ctx, routerName), log.Str(log.RouterName, routerName))
logger := log.FromContext(ctxRouter)

View file

@ -90,7 +90,6 @@ func (r *Router) AddRouteHTTPTLS(sniHost string, config *tls.Config) {
if r.hostHTTPTLSConfig == nil {
r.hostHTTPTLSConfig = map[string]*tls.Config{}
}
log.Debugf("adding route %s with minversion %d", sniHost, config.MinVersion)
r.hostHTTPTLSConfig[sniHost] = config
}