From 689f120410279e8b7516d7a4a575ef5294f80b9b Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Fri, 6 Jul 2018 02:30:03 -0600 Subject: [PATCH] Improve TLS Handshake --- acme/acme.go | 11 -- cmd/traefik/traefik.go | 5 +- configuration/entrypoints.go | 11 ++ docs/configuration/entrypoints.md | 39 ++++- integration/acme_test.go | 2 - .../https/dynamic_https_sni_default_cert.toml | 43 +++++ .../https/https_sni_default_cert.toml | 37 ++++ .../fixtures/https/https_sni_strict.toml | 28 +++ .../fixtures/https/wildcard.snitest.com.cert | 29 ++++ .../fixtures/https/wildcard.snitest.com.key | 52 ++++++ .../fixtures/https/www.snitest.com.cert | 29 ++++ .../fixtures/https/www.snitest.com.key | 52 ++++++ integration/https_test.go | 161 ++++++++++++++++++ server/server.go | 72 +++++--- server/server_configuration.go | 36 +++- server/server_configuration_test.go | 2 +- tls/certificate.go | 16 +- tls/certificate_store.go | 108 +++++++++++- tls/certificate_store_test.go | 134 +++++++++++++++ tls/tls.go | 12 +- 20 files changed, 819 insertions(+), 60 deletions(-) create mode 100644 integration/fixtures/https/dynamic_https_sni_default_cert.toml create mode 100644 integration/fixtures/https/https_sni_default_cert.toml create mode 100644 integration/fixtures/https/https_sni_strict.toml create mode 100644 integration/fixtures/https/wildcard.snitest.com.cert create mode 100644 integration/fixtures/https/wildcard.snitest.com.key create mode 100644 integration/fixtures/https/www.snitest.com.cert create mode 100644 integration/fixtures/https/www.snitest.com.key create mode 100644 tls/certificate_store_test.go diff --git a/acme/acme.go b/acme/acme.go index 4c2361155..318a8583e 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -22,7 +22,6 @@ import ( "github.com/containous/traefik/log" acmeprovider "github.com/containous/traefik/provider/acme" "github.com/containous/traefik/safe" - "github.com/containous/traefik/tls/generate" "github.com/containous/traefik/types" "github.com/containous/traefik/version" "github.com/eapache/channels" @@ -57,7 +56,6 @@ type ACME struct { ACMELogging bool `description:"Enable debug logging of ACME actions."` OverrideCertificates bool `description:"Enable to override certificates in key-value store when using storeconfig"` client *acme.Client - defaultCertificate *tls.Certificate store cluster.Store challengeHTTPProvider *challengeHTTPProvider challengeTLSProvider *challengeTLSProvider @@ -76,14 +74,6 @@ func (a *ACME) init() error { legolog.Logger = fmtlog.New(ioutil.Discard, "", 0) } - // no certificates in TLS config, so we add a default one - cert, err := generate.DefaultCertificate() - if err != nil { - return err - } - - a.defaultCertificate = cert - a.jobs = channels.NewInfiniteChannel() return nil } @@ -131,7 +121,6 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl a.dynamicCerts = certs a.challengeTLSProvider = &challengeTLSProvider{store: a.store} - tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) tlsConfig.GetCertificate = a.getCertificate a.TLSConfig = tlsConfig diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 8e0dad24b..e721ff397 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -209,10 +209,7 @@ func runCmd(globalConfiguration *configuration.GlobalConfiguration, configFile s entryPoint.OnDemandListener = acmeprovider.ListenRequest } - entryPoint.CertificateStore = &traefiktls.CertificateStore{ - DynamicCerts: &safe.Safe{}, - StaticCerts: &safe.Safe{}, - } + entryPoint.CertificateStore = traefiktls.NewCertificateStore() acmeprovider.SetCertificateStore(entryPoint.CertificateStore) } diff --git a/configuration/entrypoints.go b/configuration/entrypoints.go index dd4fbd43f..7a5f679b8 100644 --- a/configuration/entrypoints.go +++ b/configuration/entrypoints.go @@ -247,6 +247,17 @@ func makeEntryPointTLS(result map[string]string) (*tls.TLS, error) { if len(result["tls_ciphersuites"]) > 0 { configTLS.CipherSuites = strings.Split(result["tls_ciphersuites"], ",") } + + if len(result["tls_snistrict"]) > 0 { + configTLS.SniStrict = toBool(result, "tls_snistrict") + } + + if len(result["tls_defaultcertificate_cert"]) > 0 && len(result["tls_defaultcertificate_key"]) > 0 { + configTLS.DefaultCertificate = &tls.Certificate{ + CertFile: tls.FileOrContent(result["tls_defaultcertificate_cert"]), + KeyFile: tls.FileOrContent(result["tls_defaultcertificate_key"]), + } + } } return configTLS, nil diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index cc0f17c9b..5704c27d2 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -111,6 +111,9 @@ TLS:/my/path/foo.cert,/my/path/foo.key;/my/path/goo.cert,/my/path/goo.key;/my/pa TLS TLS.MinVersion:VersionTLS11 TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384 +TLS.SniStrict:true +TLS.DefaultCertificate.Cert:path/to/foo.cert +TLS.DefaultCertificate.Key:path/to/foo.key CA:car CA.Optional:true Redirect.EntryPoint:https @@ -212,7 +215,7 @@ Define an entrypoint with SNI support. ``` !!! note - If an empty TLS configuration is done, default self-signed certificates are generated. + If an empty TLS configuration is provided, default self-signed certificates are generated. ### Dynamic Certificates @@ -375,6 +378,40 @@ To specify an https entry point with a minimum TLS version, and specifying an ar keyFile = "integration/fixtures/https/snitest.org.key" ``` +## Strict SNI Checking + +To enable strict SNI checking, so that connections cannot be made if a matching certificate does not exist. + +```toml +[entryPoints] + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + sniStrict = true + [[entryPoints.https.tls.certificates]] + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" +``` + +## Default Certificate + +To enable a default certificate to serve, so that connections without SNI or without a matching domain will be served this certificate. + +```toml +[entryPoints] + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + [entryPoints.https.tls.defaultCertificate] + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" +``` + +!!! note + There can only be one `defaultCertificate` set per entrypoint. + Use a single set of square brackets `[ ]`, instead of the two needed for normal certificates. + If no default certificate is provided, a self-signed certificate will be generated by Traefik, and used instead. + ## Compression To enable compression support using gzip format. diff --git a/integration/acme_test.go b/integration/acme_test.go index 913ee0f5c..7aba14da6 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -272,8 +272,6 @@ func (s *AcmeSuite) TestHTTP01OnDemand(c *check.C) { } func (s *AcmeSuite) TestHTTP01OnDemandStaticCertificatesWithWildcard(c *check.C) { - // FIXME flaky - c.Skip("Flaky behavior will be fixed in the next PR") testCase := acmeTestCase{ traefikConfFilePath: "fixtures/acme/acme_tls.toml", template: templateModel{ diff --git a/integration/fixtures/https/dynamic_https_sni_default_cert.toml b/integration/fixtures/https/dynamic_https_sni_default_cert.toml new file mode 100644 index 000000000..b0e9a98cc --- /dev/null +++ b/integration/fixtures/https/dynamic_https_sni_default_cert.toml @@ -0,0 +1,43 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["https"] + +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + [entryPoints.https.tls.defaultCertificate] + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" + +[api] + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://127.0.0.1:9010" + weight = 1 + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host:snitest.com" + [frontends.frontend2] + backend = "backend1" + [frontends.frontend2.routes.test_1] + rule = "Host:www.snitest.com" + +[[tls]] + entryPoints = ["https"] + [tls.certificate] + certFile = "fixtures/https/wildcard.snitest.com.cert" + keyFile = "fixtures/https/wildcard.snitest.com.key" + +[[tls]] + entryPoints = ["https"] + [tls.certificate] + certFile = "fixtures/https/www.snitest.com.cert" + keyFile = "fixtures/https/www.snitest.com.key" diff --git a/integration/fixtures/https/https_sni_default_cert.toml b/integration/fixtures/https/https_sni_default_cert.toml new file mode 100644 index 000000000..17430aa6c --- /dev/null +++ b/integration/fixtures/https/https_sni_default_cert.toml @@ -0,0 +1,37 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["https"] + +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + [entryPoints.https.tls.defaultCertificate] + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" + [[entryPoints.https.tls.certificates]] + certFile = "fixtures/https/wildcard.snitest.com.cert" + keyFile = "fixtures/https/wildcard.snitest.com.key" + [[entryPoints.https.tls.certificates]] + certFile = "fixtures/https/www.snitest.com.cert" + keyFile = "fixtures/https/www.snitest.com.key" + +[api] + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://127.0.0.1:9010" + weight = 1 + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host:snitest.com" + [frontends.frontend2] + backend = "backend1" + [frontends.frontend2.routes.test_1] + rule = "Host:www.snitest.com" diff --git a/integration/fixtures/https/https_sni_strict.toml b/integration/fixtures/https/https_sni_strict.toml new file mode 100644 index 000000000..068bc8eca --- /dev/null +++ b/integration/fixtures/https/https_sni_strict.toml @@ -0,0 +1,28 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["https"] + +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + sniStrict = true + [entryPoints.https.tls.defaultCertificate] + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" + +[api] + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://127.0.0.1:9010" + weight = 1 + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host:snitest.com" diff --git a/integration/fixtures/https/wildcard.snitest.com.cert b/integration/fixtures/https/wildcard.snitest.com.cert new file mode 100644 index 000000000..fa20c99dd --- /dev/null +++ b/integration/fixtures/https/wildcard.snitest.com.cert @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE4DCCAsgCCQCBCSnAJ0he3jANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQUwxFjAUBgNVBAMMDSouc25pdGVzdC5jb20wHhcNMTgwNjE5 +MjAyMTEzWhcNMTgwNzE5MjAyMTEzWjAyMQswCQYDVQQGEwJVUzELMAkGA1UECAwC +QUwxFjAUBgNVBAMMDSouc25pdGVzdC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDOuys7ZjGVxwY/Rp5OMECkYNTOfZ1CEyAL0+pod5cd8et7k0cc +T+tEP/25rNR9N0d3/AmtPX3XTy1MA05bYGD07cD+8meANJ+rLGMfcWh8QbBCFAWZ ++zWkOSwpwW/DvI+67FvxHNa04u3Wv2qUld6qb0mSuZi9hKQ2s6/L3o/SxtDL/G4N +rW71ZCkIwfzkIqvh6KWYQCZvOAIF+BFVZ2UgLbRD7/RYDrIIOrNAkW8O26C7Tck/ +JhDHDCOmfNkYqfhUW+D+GgoCi38uJqngZyxypscKdaA6SM2oFoo1jCShxDrOhvJU +rE/l+T2Xtyr+FupJUv93iowibUwHWR5YwRNYOPkdDSG3oSKxz5xzi7Qa8L2fI7wo +A50TDVh2AvmMCUufd5adS70bLYBfxdFNmnUhH+LHbg4v83K1eR4xMiWjpvLZ6Oub +ufVJF6s5QEw+3K3s31UPbjzam073afSMLBpfHOsbwvcb1MBYWvf0intQo3a8MvYZ +DCj3Y7W1Vw8lbn4v1N37KSLSNMMX1SyKxK6386t/AHwFuCM9ygI6f/l/XERL1B61 +qj9rZngKOo2cW3Yjj+oUETF3nHmcCwKBYTiWLswBI3fg6oFHTMocypY3eKhiyVaU +mf9kBMgkDGUjWrAfOEuW9jCaDnag+Yy4XUXOlc/XaT9M2Ajvpfh9gYxWIQIDAQAB +MA0GCSqGSIb3DQEBCwUAA4ICAQC3ut8Qeq3pYt/OGTATaUcYxrfqezW5hJN6bVfr +/+UN+B0DfGd1/gKRUmb/t3RtmeMctTW6F21c8jyXBObjOhYVV6aE6iF61Uopozux ++VZq8H3VJ5Qiu/Yfb5dh0iGf9srREeSAkUHBuJ9qAosM6iJsoaxQuhw/yDSxrhhg +3jS850EZYEt9ZFjz3IdSnPCiLYqu+wMOCfT2sqBD3S8JCohTdpzuvKI2KaaY5drW +NK4mrpJIjucfZIqbA1hbd/lCqzI4jW6i86GFhoikxxWbJCEuSWOiLJtxVnOvQgxi +qOnOIMx88ivIxrZUHTvy2ncv/RH0q5qsaQddPkY+ll1E+1T7L7CeMTAMANS2vdlH +nwJgsQqowisLYJQ4ztsbvpZung2szwx4ImASICYF5aVkbuNJd+lRVUoHEfSYNIYM +Rtjteu48lYFzoBMwl6TFJA2yvL1LNaTE4/zTDgGx21aDHK14J3eIG+05wZE8AXkC +lNGsY6n2Fn6yLK9nLxcOkpGyY62ndZwDEezqr0liOz+CKBeSZwk+VFxcgE3uGo+r +DUcfLaU7Lx5KovP1DYAKJR3caSPAIVPnQth2kunCNs4kD7370JmmnvTlS7CTeUBT +1P7wLh3QMrq826lGtLXNMasR1w+Q6jVx7HoOD2HFRbJHFv10R/GuWpAWAD8X5m92 +a/HFDg== +-----END CERTIFICATE----- diff --git a/integration/fixtures/https/wildcard.snitest.com.key b/integration/fixtures/https/wildcard.snitest.com.key new file mode 100644 index 000000000..3084b7e88 --- /dev/null +++ b/integration/fixtures/https/wildcard.snitest.com.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDOuys7ZjGVxwY/ +Rp5OMECkYNTOfZ1CEyAL0+pod5cd8et7k0ccT+tEP/25rNR9N0d3/AmtPX3XTy1M +A05bYGD07cD+8meANJ+rLGMfcWh8QbBCFAWZ+zWkOSwpwW/DvI+67FvxHNa04u3W +v2qUld6qb0mSuZi9hKQ2s6/L3o/SxtDL/G4NrW71ZCkIwfzkIqvh6KWYQCZvOAIF ++BFVZ2UgLbRD7/RYDrIIOrNAkW8O26C7Tck/JhDHDCOmfNkYqfhUW+D+GgoCi38u +JqngZyxypscKdaA6SM2oFoo1jCShxDrOhvJUrE/l+T2Xtyr+FupJUv93iowibUwH +WR5YwRNYOPkdDSG3oSKxz5xzi7Qa8L2fI7woA50TDVh2AvmMCUufd5adS70bLYBf +xdFNmnUhH+LHbg4v83K1eR4xMiWjpvLZ6OubufVJF6s5QEw+3K3s31UPbjzam073 +afSMLBpfHOsbwvcb1MBYWvf0intQo3a8MvYZDCj3Y7W1Vw8lbn4v1N37KSLSNMMX +1SyKxK6386t/AHwFuCM9ygI6f/l/XERL1B61qj9rZngKOo2cW3Yjj+oUETF3nHmc +CwKBYTiWLswBI3fg6oFHTMocypY3eKhiyVaUmf9kBMgkDGUjWrAfOEuW9jCaDnag ++Yy4XUXOlc/XaT9M2Ajvpfh9gYxWIQIDAQABAoICAHEDax/evw6lLaobveD6iewS +r2Nu0jBT6jntEIEpl2gcX2I/4ij9G51E6jy92a/WL3DNTLDzI783noimagiUCIz9 +CHuXIrO4kOzvqASBZ+A9vNByx5kk9m8ffiAZijLT+zLxkVWfMVTTlbfHDsnJoF9F +1U+rvG8meusYker+cVuFqpFJHxTFEhp+Ndx+x/QjbBlkqFox/5DfamO++CLbEjJk +Kd7V55rX9cV/6YxLtQ3HTPf4DyNBePyHi1mxeLD+Ai6Dx9zBeWVoww8EvetaG7dV +qwvxv7T9JchVAhtB0KjKcGeE6CcXx9ntxhkRXiRnfI63G8dK607KtzxxIKDec+bU +O0A9F3DCU1qQcNsHhKButgN3SAKu8lERTpa1Y/Wu9YOmXRwexRtS8D2ktFjYyERJ +NUkU1WST707avYxNi5SfVr2tCpMtiERzqVdsgExBoQcliJmtb2r3qhz4TI0Q6MjT +R1icUYfQv4+xzO0TMP3+8DLWxg2t7f3082b2ig29N6z/jD67U6jzc8hxwrPqvq2b +ubD7YcIfRWwRbaieypEymtqTW7uc+Qs6z4brC8hTdAjOlOn8HN92gN8E9ilpyjam +QZQpMD5OSeF0cDfrgMkXvcv5xrHUjfWf0KSMYqVDz2mp3101WExKpMiBv9dHJmVm +XsCa5UW5o4CGK4SM6LmNAoIBAQDoyymGgFL+DMvp1dlpakZxz4mE/+qK03/YKaOU +TSY/g1szwnB1z+cKybsCdRWfqQVq2N23dOq/3Afu2hs4F71ZbNZpRn2JBuH8wa7S +V6K8N95He/zjr/lz3p6qOxcmG1m84HDJEJPE4aNcq/qpBSQ/hzc8YGdNxthBiSQ4 +FgJMCnnyOYAwYZvOQqdGI/LTGh+vr/CaEn5Zco9CZPLuOGSTtvblKKY1zSwttg4/ +ZLb2ebq9HK9zoD0IDMx7zfPC+P/MhHPGR2HHPSHh653k7TRjpD3u30DNQDF0YWvK +uYgLI1MReofShBA8I/rwEPe8nDn6KLmv3KoLt6M6Ied6YLEPAoIBAQDjVuYC67FL +i7wDLSLLjWBYGTS6r1XwKppc73wQv7YTNvPUWrhxvRXlTv6laBVUmbmllDuDdmXI +TOyQB4rKN2KIv+Cdt+itmEAMcbfVM7wIIP32MOyZP3D/nd95MfSS06+M/D30DCFA +U+Oi4XA3NN6reXbASYjt0wNsgXDuYHrZpOB00LHEHIWvfLtf9VMWQPgVSPU1T4Bf +0LSKRkE6Zl1ZY9RoH9U25APuCEpR1+SBusMqhZdtNTogfrEtmrOs15FoRdzm1E9G +E9Zt7C06A6tTN9YOcckIjHMCrwPKdiAgiQVy7gThbMZk2qHuhz38xtgmAlBLhl9+ +6pwwMA5j2iXPAoIBAQCHqV2ZtE6pHmv26Vi5xeUnjfpmN31HSdnG7v0U/6C6gqIz +l6xR+8Z40vbYh8MCOE2f5qHOt6PWCzPUTeZu2ebOpk6NKzcdE5W+5mAq1EdRyH0Q +y4Ckb3i/vYxZR/ZFjsrM9z7C7ZYvtg6tgsuglA57tyDJXqTU/nwoNPOWe7z681/9 +eOTrTPavTMiOZ4Sq4R52E+Hy57QaDFjQKGQpz1NNgeJ/ySCTWe3U9bN33gmBuY7J +hl340/i9KDhCLdNQXCs11Dpj4lVo9oc4UUbCkjlll+E/w3rQIgiv+dYHXfeaBgvy +s6VTWQLdCVrDbB/zGlfvIKyVf9LY4TuONRPgjVihAoIBAATG0aRkEWCV+ghTDXUb +blfLh8kYYATg0Ed9nKy5anjy4aKnmVKCd5BO3ZjaHACgDj+FYs67UR4pR5srHWZs +TXy0E2Mc9x2Wolnglc07/gpprwxaMM5zf8tPJN/mBc6D9h9POXoEOzqfyJumgvYV +/Uu7DJyzrtXYZi0Edzv6+PnTtgeeTu3g74olY8Z7YBiKmuvPkZ9iIT9iIjj5iutQ +NUvohhD+AjvaBJ8eu3kGwT1ckDc3gVwBD0yZfN2Jb5cFHIAFX8PV2CiPyCSdHsIm +S5Y/CRdamq+8S7pVtQ2u97PXTS8CA0Y9Q9ngoiBh5RKHlwkNaWR82UrQYSG+EL9W +WQ8CggEAONeHx+9BeeIpu6jXjs2GqGuLgYbPSwoAo3StO5O+3Q1EgORv6n29xasv +s+/IJBqhKeYFSNhXFvsyaacOMRwY8+vpr8FgKrytSlc86OEjGPXss6Zl7RuLvk/8 +S3wm593Lx3GLIfVIX+S2naurxq4Td9oDeKukjD7sKtOy8DhmLbLC48t5P/FoThZH +PUqyLJ5XDf+pSV31Z2LqTwYdgKOqqTTJFvZLUzYZDL5Yd2nHrfvwF6H70zzuee0t +Hp7QFDD14ZSMv+QOjkwenqyj1O87JJpPKH7NLRaaEk+gI7yttwavbxJYjFUWOrRB +F6gCgvoJLFw+v5SAX5kx3hxj+QYH5w== +-----END PRIVATE KEY----- diff --git a/integration/fixtures/https/www.snitest.com.cert b/integration/fixtures/https/www.snitest.com.cert new file mode 100644 index 000000000..8e3a1546e --- /dev/null +++ b/integration/fixtures/https/www.snitest.com.cert @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE5DCCAswCCQDXCA89wY62zzANBgkqhkiG9w0BAQsFADA0MQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQUwxGDAWBgNVBAMMD3d3dy5zbml0ZXN0LmNvbTAeFw0xODA2 +MTkyMDIyMTRaFw0xODA3MTkyMDIyMTRaMDQxCzAJBgNVBAYTAlVTMQswCQYDVQQI +DAJBTDEYMBYGA1UEAwwPd3d3LnNuaXRlc3QuY29tMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA+vPeeTESpGmzGHvyR4kCdGlmJjA9x230ghFU2tdCMl1C +aAR3uaZWxg9ldAiu54yvX3ViV/BMpNyQu6Knb293W5wcxidi8aHXcqACRLNtwwmn +NMX48Su3OvnU7Dc/fi0mpQLyblxXloCyOG/gtNjzZXVwrn3weMCe/XsvxkpcAOJz +7ZZrXCsrQ8pk5V0vMgryQ19zMc+uK3aAPQ+ePFjraWlVH2rOxtzRBGnVM864J9XR +tL0ZOAD2gdu4CVIt4xiU24E7W8jfZ3CTePERKhSCBGnkO4roPmRiNwgnP0Wk5lrR +kOQkhh4JF+GPMy4IDf6elCmEpnCT39+p36vRSP9sip1OctdfuVyCJMYgb1YCh4k2 +5CMR+MrkzxzrB2Spl46he5mGkVWXssr70F/gFrIeZPUweh7OBDHnS7twWfhhsElP +QYOXpJBWjWJkUKANDqWxM+ObUA+Kjdgk5NEOvQs7yVxpGB8Z9yK+OIJ0k77QDazD +VIWhjxjlwgpJW4KALn9xXkUKLhsn7P3hrEDkpTYnr0g22cgPjsgnAFfVVkcloeRi +pSfFINIJUBFLGtU0GSyqPJ9aj8CpZZe798nyt6FpSq9AuA2DF0MoECjNbch6C2gi +VUqNyuCVjUezw9VtKy3M16GYtnMSsNOY6tnkvfXeXmLrQlfsBs01a8DQBcmOK2MC +AwEAATANBgkqhkiG9w0BAQsFAAOCAgEAkugSNyzc+Y6MBE4/Y+Bz5HrGtKIweuar +7F70fBk9PgWpKIBJC8s+xJRgBXMFAy5HXZir1tNWvCeJhjCbBZRnpvKvDD61gBcM +odde6BLc4r8cRT5l0rILA01cVwyr3C3TzRREThInqNLSsnf845jA9TB9YKN2P6QB +TT4j3VMVRlR6OL9EaAUpIWHgKPfqXfbgPQ6rfPrQQGxvZbkL2g85IkpPH+DecN42 +PK53YZG6NW1+V2Z3agvc2/4qskqoVNdpe3JkafNicokDXTVd24MNtUemWzP3gq0i +tv75zgcwLBVVOP43mVFo5e+xZgdS65ZrWyJVL2PG929gARJSEXjLHs9avRXlpXeE +tBpCRC5gwvq2fnC7tVbKcbYyH3lr5u3nlfRlfsomoSACC6fw2cQKnp+us/+BsVyA +ntqrGxqC/WbQ/LHtk/YJfwFSnuzEPGClKx/F7+EoDETZGAf526VkxLTKtDPmwh5N +HFJpeczPE2IdxdaNdOnERUB5xeSDXnObTe3e8jIfpxF0rppGo5Dxw2tfhFscQGBM +Cs6cT9gkfX71P81JjrFrbx0bWWDf8N5meNqKqcZNTI15+dDGKXfjr9YbgtI9HHYa +Dhb+ondnii+KAcFchC7vCgDvG+bOuWxfM9N808bsBoPXvrKF+iWsOFeKmiV1B2OT +w0ZLNJ3AW5o= +-----END CERTIFICATE----- diff --git a/integration/fixtures/https/www.snitest.com.key b/integration/fixtures/https/www.snitest.com.key new file mode 100644 index 000000000..c12820aa6 --- /dev/null +++ b/integration/fixtures/https/www.snitest.com.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +3zJ/pivaO6YjG22SZ5fXH5BkKWnnwGa9 +-----END PRIVATE KEY----- diff --git a/integration/https_test.go b/integration/https_test.go index f93c28c77..8729549af 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -115,6 +115,167 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent) } +// 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. +func (s *HTTPSSuite) TestWithSNIStrictNotMatchedRequest(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_sni_strict.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/providers", 500*time.Millisecond, try.BodyContains("Host:snitest.com")) + c.Assert(err, checker.IsNil) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + NextProtos: []string{"h2", "http/1.1"}, + } + // Connection with no matching certificate should fail + _, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) + c.Assert(err, checker.NotNil, check.Commentf("failed to connect to server")) +} + +// TestWithDefaultCertificate 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 returns the default certificate. +func (s *HTTPSSuite) TestWithDefaultCertificate(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_sni_default_cert.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/providers", 500*time.Millisecond, try.BodyContains("Host:snitest.com")) + c.Assert(err, checker.IsNil) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + NextProtos: []string{"h2", "http/1.1"}, + } + conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) + c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) + + defer conn.Close() + err = conn.Handshake() + c.Assert(err, checker.IsNil, check.Commentf("TLS handshake error")) + + cs := conn.ConnectionState() + err = cs.PeerCertificates[0].VerifyHostname("snitest.com") + c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate")) + + proto := cs.NegotiatedProtocol + c.Assert(proto, checker.Equals, "h2") +} + +// TestWithDefaultCertificateNoSNI involves a client sending a request with no ServerName +// which does not match the CN of 'snitest.com.crt'. The test +// verifies that traefik returns the default certificate. +func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_sni_default_cert.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/providers", 500*time.Millisecond, try.BodyContains("Host:snitest.com")) + c.Assert(err, checker.IsNil) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"h2", "http/1.1"}, + } + conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) + c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) + + defer conn.Close() + err = conn.Handshake() + c.Assert(err, checker.IsNil, check.Commentf("TLS handshake error")) + + cs := conn.ConnectionState() + err = cs.PeerCertificates[0].VerifyHostname("snitest.com") + c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate")) + + proto := cs.NegotiatedProtocol + c.Assert(proto, checker.Equals, "h2") +} + +// TestWithOverlappingCertificate involves a client sending a SNI hostname of +// "www.snitest.com", which matches the CN of two static certificates: +// 'wildcard.snitest.com.crt', and `www.snitest.com.crt`. The test +// verifies that traefik returns the non-wildcard certificate. +func (s *HTTPSSuite) TestWithOverlappingStaticCertificate(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_sni_default_cert.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/providers", 500*time.Millisecond, try.BodyContains("Host:snitest.com")) + c.Assert(err, checker.IsNil) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "www.snitest.com", + NextProtos: []string{"h2", "http/1.1"}, + } + conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) + c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) + + defer conn.Close() + err = conn.Handshake() + c.Assert(err, checker.IsNil, check.Commentf("TLS handshake error")) + + cs := conn.ConnectionState() + err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com") + c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate")) + + proto := cs.NegotiatedProtocol + c.Assert(proto, checker.Equals, "h2") +} + +// TestWithOverlappingCertificate involves a client sending a SNI hostname of +// "www.snitest.com", which matches the CN of two dynamic certificates: +// 'wildcard.snitest.com.crt', and `www.snitest.com.crt`. The test +// verifies that traefik returns the non-wildcard certificate. +func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/dynamic_https_sni_default_cert.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/providers", 500*time.Millisecond, try.BodyContains("Host:snitest.com")) + c.Assert(err, checker.IsNil) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "www.snitest.com", + NextProtos: []string{"h2", "http/1.1"}, + } + conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) + c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) + + defer conn.Close() + err = conn.Handshake() + c.Assert(err, checker.IsNil, check.Commentf("TLS handshake error")) + + cs := conn.ConnectionState() + err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com") + c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate")) + + proto := cs.NegotiatedProtocol + c.Assert(proto, checker.Equals, "h2") +} + // TestWithClientCertificateAuthentication // The client can send a certificate signed by a CA trusted by the server but it's optional func (s *HTTPSSuite) TestWithClientCertificateAuthentication(c *check.C) { diff --git a/server/server.go b/server/server.go index 9a929a7e5..62399d0b9 100644 --- a/server/server.go +++ b/server/server.go @@ -15,7 +15,6 @@ import ( "os" "os/signal" "reflect" - "strings" "sync" "time" @@ -79,7 +78,7 @@ type serverEntryPoint struct { httpServer *h2c.Server listener net.Listener httpRouter *middlewares.HandlerSwitcher - certs *safe.Safe + certs *traefiktls.CertificateStore onDemandListener func(string) (*tls.Certificate, error) tlsALPNGetter func(string) (*tls.Certificate, error) } @@ -276,19 +275,13 @@ func (s *Server) AddListener(listener func(types.Configuration)) { // getCertificate allows to customize tlsConfig.GetCertificate behaviour to get the certificates inserted dynamically func (s *serverEntryPoint) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { - domainToCheck := types.CanonicalDomain(clientHello.ServerName) - - if s.certs.Get() != nil { - for domains, cert := range s.certs.Get().(map[string]*tls.Certificate) { - for _, certDomain := range strings.Split(domains, ",") { - if types.MatchDomain(domainToCheck, certDomain) { - return cert, nil - } - } - } - log.Debugf("No certificate provided dynamically can check the domain %q, a per default certificate will be used.", domainToCheck) + bestCertificate := s.certs.GetBestCertificate(clientHello) + if bestCertificate != nil { + return bestCertificate, nil } + domainToCheck := types.CanonicalDomain(clientHello.ServerName) + if s.tlsALPNGetter != nil { cert, err := s.tlsALPNGetter(domainToCheck) if err != nil { @@ -300,11 +293,17 @@ func (s *serverEntryPoint) getCertificate(clientHello *tls.ClientHelloInfo) (*tl } } - if s.onDemandListener != nil { + if s.onDemandListener != nil && len(domainToCheck) > 0 { + // Only check for an onDemandCert if there is a domain name return s.onDemandListener(domainToCheck) } - return nil, nil + if s.certs.SniStrict { + return nil, fmt.Errorf("strict SNI enabled - No certificate found for domain: %q, closing connection", domainToCheck) + } + + log.Debugf("Serving default cert for request: %q", domainToCheck) + return s.certs.DefaultCertificate, nil } func (s *Server) startProvider() { @@ -335,7 +334,7 @@ func (s *Server) createTLSConfig(entryPointName string, tlsOption *traefiktls.TL return nil, err } - s.serverEntryPoints[entryPointName].certs.Set(make(map[string]*tls.Certificate)) + s.serverEntryPoints[entryPointName].certs.DynamicCerts.Set(make(map[string]*tls.Certificate)) // ensure http2 enabled config.NextProtos = []string{"h2", "http/1.1", acme.ACMETLS1Protocol} @@ -345,6 +344,7 @@ func (s *Server) createTLSConfig(entryPointName string, tlsOption *traefiktls.TL tlsOption.ClientCA.Files = tlsOption.ClientCAFiles tlsOption.ClientCA.Optional = false } + if len(tlsOption.ClientCA.Files) > 0 { pool := x509.NewCertPool() for _, caFile := range tlsOption.ClientCA.Files { @@ -376,7 +376,7 @@ func (s *Server) createTLSConfig(entryPointName string, tlsOption *traefiktls.TL return false } - err := s.globalConfiguration.ACME.CreateClusterConfig(s.leadership, config, s.serverEntryPoints[entryPointName].certs, checkOnDemandDomain) + err := s.globalConfiguration.ACME.CreateClusterConfig(s.leadership, config, s.serverEntryPoints[entryPointName].certs.DynamicCerts, checkOnDemandDomain) if err != nil { return nil, err } @@ -385,17 +385,16 @@ func (s *Server) createTLSConfig(entryPointName string, tlsOption *traefiktls.TL config.GetCertificate = s.serverEntryPoints[entryPointName].getCertificate } - if len(config.Certificates) == 0 { - return nil, fmt.Errorf("no certificates found for TLS entrypoint %s", entryPointName) + if len(config.Certificates) != 0 { + certMap := s.buildNameOrIPToCertificate(config.Certificates) + + if s.entryPoints[entryPointName].CertificateStore != nil { + s.entryPoints[entryPointName].CertificateStore.StaticCerts.Set(certMap) + } } - // BuildNameToCertificate parses the CommonName and SubjectAlternateName fields - // in each certificate and populates the config.NameToCertificate map. - config.BuildNameToCertificate() - - if s.entryPoints[entryPointName].CertificateStore != nil { - s.entryPoints[entryPointName].CertificateStore.StaticCerts.Set(config.NameToCertificate) - } + // Remove certs from the TLS config object + config.Certificates = []tls.Certificate{} // Set the minimum TLS version if set in the config TOML if minConst, exists := traefiktls.MinVersion[s.entryPoints[entryPointName].Configuration.TLS.MinVersion]; exists { @@ -593,3 +592,24 @@ func stopMetricsClients() { metrics.StopStatsd() metrics.StopInfluxDB() } + +func (s *Server) buildNameOrIPToCertificate(certs []tls.Certificate) map[string]*tls.Certificate { + certMap := make(map[string]*tls.Certificate) + for i := range certs { + cert := &certs[i] + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + continue + } + if len(x509Cert.Subject.CommonName) > 0 { + certMap[x509Cert.Subject.CommonName] = cert + } + for _, san := range x509Cert.DNSNames { + certMap[san] = cert + } + for _, ipSan := range x509Cert.IPAddresses { + certMap[ipSan.String()] = cert + } + } + return certMap +} diff --git a/server/server_configuration.go b/server/server_configuration.go index a6083098c..5b4030b65 100644 --- a/server/server_configuration.go +++ b/server/server_configuration.go @@ -19,8 +19,8 @@ import ( "github.com/containous/traefik/middlewares" "github.com/containous/traefik/middlewares/pipelining" "github.com/containous/traefik/rules" - "github.com/containous/traefik/safe" traefiktls "github.com/containous/traefik/tls" + "github.com/containous/traefik/tls/generate" "github.com/containous/traefik/types" "github.com/eapache/channels" "github.com/sirupsen/logrus" @@ -55,11 +55,12 @@ func (s *Server) loadConfiguration(configMsg types.ConfigMessage) { s.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler()) if s.entryPoints[newServerEntryPointName].Configuration.TLS == nil { - if newServerEntryPoint.certs.Get() != nil { + if newServerEntryPoint.certs.ContainsCertificates() { log.Debugf("Certificates not added to non-TLS entryPoint %s.", newServerEntryPointName) } } else { - s.serverEntryPoints[newServerEntryPointName].certs.Set(newServerEntryPoint.certs.Get()) + s.serverEntryPoints[newServerEntryPointName].certs.DynamicCerts.Set(newServerEntryPoint.certs.DynamicCerts.Get()) + s.serverEntryPoints[newServerEntryPointName].certs.ResetCache() } log.Infof("Server configuration reloaded on %s", s.serverEntryPoints[newServerEntryPointName].httpServer.Addr) } @@ -123,7 +124,7 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura for serverEntryPointName, serverEntryPoint := range serverEntryPoints { serverEntryPoint.httpRouter.GetHandler().SortRoutes() if _, exists := entryPointsCertificates[serverEntryPointName]; exists { - serverEntryPoint.certs.Set(entryPointsCertificates[serverEntryPointName]) + serverEntryPoint.certs.DynamicCerts.Set(entryPointsCertificates[serverEntryPointName]) } } @@ -559,10 +560,33 @@ func (s *Server) buildServerEntryPoints() map[string]*serverEntryPoint { onDemandListener: entryPoint.OnDemandListener, tlsALPNGetter: entryPoint.TLSALPNGetter, } + if entryPoint.CertificateStore != nil { - serverEntryPoints[entryPointName].certs = entryPoint.CertificateStore.DynamicCerts + serverEntryPoints[entryPointName].certs = entryPoint.CertificateStore } else { - serverEntryPoints[entryPointName].certs = &safe.Safe{} + serverEntryPoints[entryPointName].certs = traefiktls.NewCertificateStore() + } + + if entryPoint.Configuration.TLS != nil { + serverEntryPoints[entryPointName].certs.SniStrict = entryPoint.Configuration.TLS.SniStrict + + if entryPoint.Configuration.TLS.DefaultCertificate != nil { + cert, err := tls.LoadX509KeyPair(entryPoint.Configuration.TLS.DefaultCertificate.CertFile.String(), entryPoint.Configuration.TLS.DefaultCertificate.KeyFile.String()) + if err != nil { + } + serverEntryPoints[entryPointName].certs.DefaultCertificate = &cert + } else { + cert, err := generate.DefaultCertificate() + if err != nil { + } + serverEntryPoints[entryPointName].certs.DefaultCertificate = cert + } + if len(entryPoint.Configuration.TLS.Certificates) > 0 { + config, _ := entryPoint.Configuration.TLS.Certificates.CreateTLSConfig(entryPointName) + certMap := s.buildNameOrIPToCertificate(config.Certificates) + serverEntryPoints[entryPointName].certs.StaticCerts.Set(certMap) + + } } } return serverEntryPoints diff --git a/server/server_configuration_test.go b/server/server_configuration_test.go index abe22a1d9..2a932f195 100644 --- a/server/server_configuration_test.go +++ b/server/server_configuration_test.go @@ -215,7 +215,7 @@ func TestServerLoadCertificateWithDefaultEntryPoint(t *testing.T) { srv := NewServer(globalConfig, nil, entryPoints) if mapEntryPoints, err := srv.loadConfig(dynamicConfigs, globalConfig); err != nil { t.Fatalf("got error: %s", err) - } else if mapEntryPoints["https"].certs.Get() == nil { + } else if !mapEntryPoints["https"].certs.ContainsCertificates() { t.Fatal("got error: https entryPoint must have TLS certificates.") } } diff --git a/tls/certificate.go b/tls/certificate.go index 626810760..f680c78f5 100644 --- a/tls/certificate.go +++ b/tls/certificate.go @@ -152,16 +152,28 @@ func (c *Certificate) AppendCertificates(certs map[string]map[string]*tls.Certif parsedCert, _ := x509.ParseCertificate(tlsCert.Certificate[0]) - certKey := parsedCert.Subject.CommonName + var SANs []string + if parsedCert.Subject.CommonName != "" { + SANs = append(SANs, parsedCert.Subject.CommonName) + } if parsedCert.DNSNames != nil { sort.Strings(parsedCert.DNSNames) for _, dnsName := range parsedCert.DNSNames { if dnsName != parsedCert.Subject.CommonName { - certKey += fmt.Sprintf(",%s", dnsName) + SANs = append(SANs, dnsName) } } } + if parsedCert.IPAddresses != nil { + for _, ip := range parsedCert.IPAddresses { + if ip.String() != parsedCert.Subject.CommonName { + SANs = append(SANs, ip.String()) + } + } + + } + certKey := strings.Join(SANs, ",") certExists := false if certs[ep] == nil { diff --git a/tls/certificate_store.go b/tls/certificate_store.go index c70e1710f..6ddb9407c 100644 --- a/tls/certificate_store.go +++ b/tls/certificate_store.go @@ -2,14 +2,32 @@ package tls import ( "crypto/tls" + "net" + "sort" + "strings" + "time" + "github.com/containous/traefik/log" "github.com/containous/traefik/safe" + "github.com/patrickmn/go-cache" ) // CertificateStore store for dynamic and static certificates type CertificateStore struct { - DynamicCerts *safe.Safe - StaticCerts *safe.Safe + DynamicCerts *safe.Safe + StaticCerts *safe.Safe + DefaultCertificate *tls.Certificate + CertCache *cache.Cache + SniStrict bool +} + +// NewCertificateStore create a store for dynamic and static certificates +func NewCertificateStore() *CertificateStore { + return &CertificateStore{ + StaticCerts: &safe.Safe{}, + DynamicCerts: &safe.Safe{}, + CertCache: cache.New(1*time.Hour, 10*time.Minute), + } } // GetAllDomains return a slice with all the certificate domain @@ -31,3 +49,89 @@ func (c CertificateStore) GetAllDomains() []string { } return allCerts } + +// GetBestCertificate returns the best match certificate, and caches the response +func (c CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *tls.Certificate { + domainToCheck := strings.ToLower(strings.TrimSpace(clientHello.ServerName)) + if len(domainToCheck) == 0 { + // If no ServerName is provided, Check for local IP address matches + host, _, err := net.SplitHostPort(clientHello.Conn.LocalAddr().String()) + if err != nil { + log.Debugf("Could not split host/port: %v", err) + } + domainToCheck = strings.TrimSpace(host) + } + + if cert, ok := c.CertCache.Get(domainToCheck); ok { + return cert.(*tls.Certificate) + } + + matchedCerts := map[string]*tls.Certificate{} + if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil { + for domains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) { + for _, certDomain := range strings.Split(domains, ",") { + if MatchDomain(domainToCheck, certDomain) { + matchedCerts[certDomain] = cert + } + } + } + } + + if c.StaticCerts != nil && c.StaticCerts.Get() != nil { + for domains, cert := range c.StaticCerts.Get().(map[string]*tls.Certificate) { + for _, certDomain := range strings.Split(domains, ",") { + if MatchDomain(domainToCheck, certDomain) { + matchedCerts[certDomain] = cert + } + } + } + } + + if len(matchedCerts) > 0 { + // sort map by keys + keys := make([]string, 0, len(matchedCerts)) + for k := range matchedCerts { + keys = append(keys, k) + } + sort.Strings(keys) + + // cache best match + c.CertCache.SetDefault(domainToCheck, matchedCerts[keys[len(keys)-1]]) + return matchedCerts[keys[len(keys)-1]] + } + + return nil +} + +// ContainsCertificates checks if there are any certs in the store +func (c CertificateStore) ContainsCertificates() bool { + return c.StaticCerts.Get() != nil || c.DynamicCerts.Get() != nil +} + +// ResetCache clears the cache in the store +func (c CertificateStore) ResetCache() { + if c.CertCache != nil { + c.CertCache.Flush() + } +} + +// MatchDomain return true if a domain match the cert domain +func MatchDomain(domain string, certDomain string) bool { + if domain == certDomain { + return true + } + + for len(certDomain) > 0 && certDomain[len(certDomain)-1] == '.' { + certDomain = certDomain[:len(certDomain)-1] + } + + labels := strings.Split(domain, ".") + for i := range labels { + labels[i] = "*" + candidate := strings.Join(labels, ".") + if certDomain == candidate { + return true + } + } + return false +} diff --git a/tls/certificate_store_test.go b/tls/certificate_store_test.go new file mode 100644 index 000000000..9915133fa --- /dev/null +++ b/tls/certificate_store_test.go @@ -0,0 +1,134 @@ +package tls + +import ( + "crypto/tls" + "fmt" + "strings" + "testing" + "time" + + "github.com/containous/traefik/safe" + "github.com/patrickmn/go-cache" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetBestCertificate(t *testing.T) { + testCases := []struct { + desc string + domainToCheck string + staticCert string + dynamicCert string + expectedCert string + }{ + { + desc: "Empty Store, returns no certs", + domainToCheck: "snitest.com", + staticCert: "", + dynamicCert: "", + expectedCert: "", + }, + { + desc: "Empty static cert store", + domainToCheck: "snitest.com", + staticCert: "", + dynamicCert: "snitest.com", + expectedCert: "snitest.com", + }, + { + desc: "Empty dynamic cert store", + domainToCheck: "snitest.com", + staticCert: "snitest.com", + dynamicCert: "", + expectedCert: "snitest.com", + }, + { + desc: "Best Match", + domainToCheck: "snitest.com", + staticCert: "snitest.com", + dynamicCert: "snitest.org", + expectedCert: "snitest.com", + }, + { + desc: "Best Match with wildcard dynamic and exact static", + domainToCheck: "www.snitest.com", + staticCert: "www.snitest.com", + dynamicCert: "*.snitest.com", + expectedCert: "www.snitest.com", + }, + { + desc: "Best Match with wildcard static and exact dynamic", + domainToCheck: "www.snitest.com", + staticCert: "*.snitest.com", + dynamicCert: "www.snitest.com", + expectedCert: "www.snitest.com", + }, + { + desc: "Best Match with static wildcard only", + domainToCheck: "www.snitest.com", + staticCert: "*.snitest.com", + dynamicCert: "", + expectedCert: "*.snitest.com", + }, + { + desc: "Best Match with dynamic wildcard only", + domainToCheck: "www.snitest.com", + staticCert: "", + dynamicCert: "*.snitest.com", + expectedCert: "*.snitest.com", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + staticMap := map[string]*tls.Certificate{} + dynamicMap := map[string]*tls.Certificate{} + + if test.staticCert != "" { + cert, err := loadTestCert(test.staticCert) + require.NoError(t, err) + staticMap[test.staticCert] = cert + } + + if test.dynamicCert != "" { + cert, err := loadTestCert(test.dynamicCert) + require.NoError(t, err) + dynamicMap[test.dynamicCert] = cert + } + + store := &CertificateStore{ + DynamicCerts: safe.New(dynamicMap), + StaticCerts: safe.New(staticMap), + CertCache: cache.New(1*time.Hour, 10*time.Minute), + } + + var expected *tls.Certificate + if test.expectedCert != "" { + cert, err := loadTestCert(test.expectedCert) + require.NoError(t, err) + expected = cert + } + + clientHello := &tls.ClientHelloInfo{ + ServerName: test.domainToCheck, + } + + actual := store.GetBestCertificate(clientHello) + assert.Equal(t, expected, actual) + }) + } +} + +func loadTestCert(certName string) (*tls.Certificate, error) { + staticCert, err := tls.LoadX509KeyPair( + fmt.Sprintf("../integration/fixtures/https/%s.cert", strings.Replace(certName, "*", "wildcard", -1)), + fmt.Sprintf("../integration/fixtures/https/%s.key", strings.Replace(certName, "*", "wildcard", -1)), + ) + if err != nil { + return nil, err + } + + return &staticCert, nil +} diff --git a/tls/tls.go b/tls/tls.go index dc103da7e..0760c870a 100644 --- a/tls/tls.go +++ b/tls/tls.go @@ -22,11 +22,13 @@ type ClientCA struct { // TLS configures TLS for an entry point type TLS struct { - MinVersion string `export:"true"` - CipherSuites []string - Certificates Certificates - ClientCAFiles []string // Deprecated - ClientCA ClientCA + MinVersion string `export:"true"` + CipherSuites []string + Certificates Certificates + ClientCAFiles []string // Deprecated + ClientCA ClientCA + DefaultCertificate *Certificate + SniStrict bool `export:"true"` } // RootCAs hold the CA we want to have in root