diff --git a/acme/acme.go b/acme/acme.go index 39457fba8..63276abad 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -18,6 +18,8 @@ import ( "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" "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/xenolf/lego/acme" @@ -49,6 +51,7 @@ type ACME struct { checkOnDemandDomain func(domain string) bool jobs *channels.InfiniteChannel TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` + dynamicCerts *safe.Safe } //Domains parse []Domain @@ -99,7 +102,7 @@ func (a *ACME) init() error { acme.Logger = fmtlog.New(ioutil.Discard, "", 0) } // no certificates in TLS config, so we add a default one - cert, err := generateDefaultCertificate() + cert, err := generate.DefaultCertificate() if err != nil { return err } @@ -114,7 +117,7 @@ func (a *ACME) init() error { } // CreateClusterConfig creates a tls.config using ACME configuration in cluster mode -func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error { +func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tls.Config, certs *safe.Safe, checkOnDemandDomain func(domain string) bool) error { err := a.init() if err != nil { return err @@ -123,6 +126,7 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl return errors.New("Empty Store, please provide a key for certs storage") } a.checkOnDemandDomain = checkOnDemandDomain + a.dynamicCerts = certs tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) tlsConfig.GetCertificate = a.getCertificate a.TLSConfig = tlsConfig @@ -234,7 +238,7 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl } // CreateLocalConfig creates a tls.config using local ACME configuration -func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func(domain string) bool) error { +func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, certs *safe.Safe, checkOnDemandDomain func(domain string) bool) error { err := a.init() if err != nil { return err @@ -243,6 +247,7 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func return errors.New("Empty Store, please provide a filename for certs storage") } a.checkOnDemandDomain = checkOnDemandDomain + a.dynamicCerts = certs tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate) tlsConfig.GetCertificate = a.getCertificate a.TLSConfig = tlsConfig @@ -583,11 +588,21 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } // Get provided certificate which check a domains list (Main and SANs) +// from static and dynamic provided certificates func (a *ACME) getProvidedCertificate(domains []string) *tls.Certificate { + log.Debugf("Look for provided certificate to validate %s...", domains) + cert := searchProvidedCertificateForDomains(domains, a.TLSConfig.NameToCertificate) + if cert == nil && a.dynamicCerts != nil && a.dynamicCerts.Get() != nil { + cert = searchProvidedCertificateForDomains(domains, a.dynamicCerts.Get().(*traefikTls.DomainsCertificates).Get().(map[string]*tls.Certificate)) + } + log.Debugf("No provided certificate found for domains %s, get ACME certificate.", domains) + return cert +} + +func searchProvidedCertificateForDomains(domains []string, certs map[string]*tls.Certificate) *tls.Certificate { // Use regex to test for provided certs that might have been added into TLSConfig providedCertMatch := false - log.Debugf("Look for provided certificate to validate %s...", domains) - for k := range a.TLSConfig.NameToCertificate { + for k := range certs { selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$" for _, domainToCheck := range domains { providedCertMatch, _ = regexp.MatchString(selector, domainToCheck) @@ -597,11 +612,10 @@ func (a *ACME) getProvidedCertificate(domains []string) *tls.Certificate { } if providedCertMatch { log.Debugf("Got provided certificate for domains %s", domains) - return a.TLSConfig.NameToCertificate[k] + return certs[k] } } - log.Debugf("No provided certificate found for domains %s, get ACME certificate.", domains) return nil } diff --git a/acme/acme_test.go b/acme/acme_test.go index ff2ad970c..272e2571d 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/containous/traefik/tls/generate" "github.com/stretchr/testify/assert" "github.com/xenolf/lego/acme" ) @@ -70,8 +71,8 @@ func TestDomainsSetAppend(t *testing.T) { } func TestCertificatesRenew(t *testing.T) { - foo1Cert, foo1Key, _ := generateKeyPair("foo1.com", time.Now()) - foo2Cert, foo2Key, _ := generateKeyPair("foo2.com", time.Now()) + foo1Cert, foo1Key, _ := generate.KeyPair("foo1.com", time.Now()) + foo2Cert, foo2Key, _ := generate.KeyPair("foo2.com", time.Now()) domainsCertificates := DomainsCertificates{ lock: sync.RWMutex{}, Certs: []*DomainsCertificate{ @@ -101,7 +102,7 @@ func TestCertificatesRenew(t *testing.T) { }, }, } - foo1Cert, foo1Key, _ = generateKeyPair("foo1.com", time.Now()) + foo1Cert, foo1Key, _ = generate.KeyPair("foo1.com", time.Now()) newCertificate := &Certificate{ Domain: "foo1.com", CertURL: "url", @@ -128,10 +129,10 @@ func TestCertificatesRenew(t *testing.T) { func TestRemoveDuplicates(t *testing.T) { now := time.Now() - fooCert, fooKey, _ := generateKeyPair("foo.com", now) - foo24Cert, foo24Key, _ := generateKeyPair("foo.com", now.Add(24*time.Hour)) - foo48Cert, foo48Key, _ := generateKeyPair("foo.com", now.Add(48*time.Hour)) - barCert, barKey, _ := generateKeyPair("bar.com", now) + fooCert, fooKey, _ := generate.KeyPair("foo.com", now) + foo24Cert, foo24Key, _ := generate.KeyPair("foo.com", now.Add(24*time.Hour)) + foo48Cert, foo48Key, _ := generate.KeyPair("foo.com", now.Add(48*time.Hour)) + barCert, barKey, _ := generate.KeyPair("bar.com", now) domainsCertificates := DomainsCertificates{ lock: sync.RWMutex{}, Certs: []*DomainsCertificate{ diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go index 09b6aa1eb..3afd481e2 100644 --- a/acme/challengeProvider.go +++ b/acme/challengeProvider.go @@ -1,7 +1,15 @@ package acme import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/pem" "fmt" "strings" "sync" @@ -11,6 +19,7 @@ import ( "github.com/containous/traefik/cluster" "github.com/containous/traefik/log" "github.com/containous/traefik/safe" + "github.com/containous/traefik/tls/generate" "github.com/xenolf/lego/acme" ) @@ -60,7 +69,7 @@ func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate func (c *challengeProvider) Present(domain, token, keyAuth string) error { log.Debugf("Challenge Present %s", domain) - cert, _, err := TLSSNI01ChallengeCert(keyAuth) + cert, _, err := tlsSNI01ChallengeCert(keyAuth) if err != nil { return err } @@ -95,3 +104,47 @@ func (c *challengeProvider) CleanUp(domain, token, keyAuth string) error { func (c *challengeProvider) Timeout() (timeout, interval time.Duration) { return 60 * time.Second, 5 * time.Second } + +// tlsSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge +func tlsSNI01ChallengeCert(keyAuth string) (ChallengeCert, string, error) { + // generate a new RSA key for the certificates + var tempPrivKey crypto.PrivateKey + tempPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return ChallengeCert{}, "", err + } + rsaPrivKey := tempPrivKey.(*rsa.PrivateKey) + rsaPrivPEM := pemEncode(rsaPrivKey) + + zBytes := sha256.Sum256([]byte(keyAuth)) + z := hex.EncodeToString(zBytes[:sha256.Size]) + domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:]) + tempCertPEM, err := generate.PemCert(rsaPrivKey, domain, time.Time{}) + if err != nil { + return ChallengeCert{}, "", err + } + + certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) + if err != nil { + return ChallengeCert{}, "", err + } + + return ChallengeCert{Certificate: tempCertPEM, PrivateKey: rsaPrivPEM, certificate: &certificate}, domain, nil +} + +func pemEncode(data interface{}) []byte { + var pemBlock *pem.Block + switch key := data.(type) { + case *ecdsa.PrivateKey: + keyBytes, _ := x509.MarshalECPrivateKey(key) + pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} + case *rsa.PrivateKey: + pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} + case *x509.CertificateRequest: + pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} + case []byte: + pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.([]byte))} + } + + return pem.EncodeToMemory(pemBlock) +} diff --git a/acme/crypto.go b/acme/crypto.go deleted file mode 100644 index e130c7399..000000000 --- a/acme/crypto.go +++ /dev/null @@ -1,133 +0,0 @@ -package acme - -import ( - "crypto" - "crypto/ecdsa" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "crypto/x509" - "crypto/x509/pkix" - "encoding/hex" - "encoding/pem" - "fmt" - "math/big" - "time" -) - -func generateDefaultCertificate() (*tls.Certificate, error) { - randomBytes := make([]byte, 100) - _, err := rand.Read(randomBytes) - if err != nil { - return nil, err - } - zBytes := sha256.Sum256(randomBytes) - z := hex.EncodeToString(zBytes[:sha256.Size]) - domain := fmt.Sprintf("%s.%s.traefik.default", z[:32], z[32:]) - - certPEM, keyPEM, err := generateKeyPair(domain, time.Time{}) - if err != nil { - return nil, err - } - - certificate, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - return nil, err - } - - return &certificate, nil -} - -func generateKeyPair(domain string, expiration time.Time) ([]byte, []byte, error) { - rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)}) - - certPEM, err := generatePemCert(rsaPrivKey, domain, expiration) - if err != nil { - return nil, nil, err - } - return certPEM, keyPEM, nil -} - -func generatePemCert(privKey *rsa.PrivateKey, domain string, expiration time.Time) ([]byte, error) { - derBytes, err := generateDerCert(privKey, expiration, domain) - if err != nil { - return nil, err - } - - return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil -} - -func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return nil, err - } - - if expiration.IsZero() { - expiration = time.Now().Add(365) - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - CommonName: "TRAEFIK DEFAULT CERT", - }, - NotBefore: time.Now(), - NotAfter: expiration, - - KeyUsage: x509.KeyUsageKeyEncipherment, - BasicConstraintsValid: true, - DNSNames: []string{domain}, - } - - return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) -} - -// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge -func TLSSNI01ChallengeCert(keyAuth string) (ChallengeCert, string, error) { - // generate a new RSA key for the certificates - var tempPrivKey crypto.PrivateKey - tempPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return ChallengeCert{}, "", err - } - rsaPrivKey := tempPrivKey.(*rsa.PrivateKey) - rsaPrivPEM := pemEncode(rsaPrivKey) - - zBytes := sha256.Sum256([]byte(keyAuth)) - z := hex.EncodeToString(zBytes[:sha256.Size]) - domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:]) - tempCertPEM, err := generatePemCert(rsaPrivKey, domain, time.Time{}) - if err != nil { - return ChallengeCert{}, "", err - } - - certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) - if err != nil { - return ChallengeCert{}, "", err - } - - return ChallengeCert{Certificate: tempCertPEM, PrivateKey: rsaPrivPEM, certificate: &certificate}, domain, nil -} -func pemEncode(data interface{}) []byte { - var pemBlock *pem.Block - switch key := data.(type) { - case *ecdsa.PrivateKey: - keyBytes, _ := x509.MarshalECPrivateKey(key) - pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} - case *rsa.PrivateKey: - pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} - case *x509.CertificateRequest: - pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} - case []byte: - pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.([]byte))} - } - - return pem.EncodeToMemory(pemBlock) -} diff --git a/cmd/traefik/anonymize/anonymize_config_test.go b/cmd/traefik/anonymize/anonymize_config_test.go index 3ec49cddd..1fa91c9fd 100644 --- a/cmd/traefik/anonymize/anonymize_config_test.go +++ b/cmd/traefik/anonymize/anonymize_config_test.go @@ -26,6 +26,7 @@ import ( "github.com/containous/traefik/provider/web" "github.com/containous/traefik/provider/zk" "github.com/containous/traefik/safe" + traefikTls "github.com/containous/traefik/tls" "github.com/containous/traefik/types" thoas_stats "github.com/thoas/stats" ) @@ -48,10 +49,10 @@ func TestDo_globalConfiguration(t *testing.T) { "foo": { Network: "foo Network", Address: "foo Address", - TLS: &configuration.TLS{ + TLS: &traefikTls.TLS{ MinVersion: "foo MinVersion", CipherSuites: []string{"foo CipherSuites 1", "foo CipherSuites 2", "foo CipherSuites 3"}, - Certificates: configuration.Certificates{ + Certificates: traefikTls.Certificates{ {CertFile: "CertFile 1", KeyFile: "KeyFile 1"}, {CertFile: "CertFile 2", KeyFile: "KeyFile 2"}, }, @@ -91,10 +92,10 @@ func TestDo_globalConfiguration(t *testing.T) { "fii": { Network: "fii Network", Address: "fii Address", - TLS: &configuration.TLS{ + TLS: &traefikTls.TLS{ MinVersion: "fii MinVersion", CipherSuites: []string{"fii CipherSuites 1", "fii CipherSuites 2", "fii CipherSuites 3"}, - Certificates: configuration.Certificates{ + Certificates: traefikTls.Certificates{ {CertFile: "CertFile 1", KeyFile: "KeyFile 1"}, {CertFile: "CertFile 2", KeyFile: "KeyFile 2"}, }, @@ -178,7 +179,7 @@ func TestDo_globalConfiguration(t *testing.T) { config.MaxIdleConnsPerHost = 666 config.IdleTimeout = flaeg.Duration(666 * time.Second) config.InsecureSkipVerify = true - config.RootCAs = configuration.RootCAs{"RootCAs 1", "RootCAs 2", "RootCAs 3"} + config.RootCAs = traefikTls.RootCAs{"RootCAs 1", "RootCAs 2", "RootCAs 3"} config.Retry = &configuration.Retry{ Attempts: 666, } diff --git a/cmd/traefik/bug_test.go b/cmd/traefik/bug_test.go index efe191bcc..3158e8c99 100644 --- a/cmd/traefik/bug_test.go +++ b/cmd/traefik/bug_test.go @@ -6,6 +6,7 @@ import ( "github.com/containous/traefik/cmd/traefik/anonymize" "github.com/containous/traefik/configuration" "github.com/containous/traefik/provider/file" + "github.com/containous/traefik/tls" "github.com/stretchr/testify/assert" ) @@ -21,7 +22,7 @@ func Test_createBugReport(t *testing.T) { File: &file.Provider{ Directory: "BAR", }, - RootCAs: configuration.RootCAs{"fllf"}, + RootCAs: tls.RootCAs{"fllf"}, }, } diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 87aff0b33..e420dca3e 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -26,6 +26,7 @@ import ( "github.com/containous/traefik/safe" "github.com/containous/traefik/server" "github.com/containous/traefik/server/uuid" + traefikTls "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/containous/traefik/version" "github.com/coreos/go-systemd/daemon" @@ -144,7 +145,7 @@ Complete documentation is available at https://traefik.io`, //add custom parsers f.AddParser(reflect.TypeOf(configuration.EntryPoints{}), &configuration.EntryPoints{}) f.AddParser(reflect.TypeOf(configuration.DefaultEntryPoints{}), &configuration.DefaultEntryPoints{}) - f.AddParser(reflect.TypeOf(configuration.RootCAs{}), &configuration.RootCAs{}) + f.AddParser(reflect.TypeOf(traefikTls.RootCAs{}), &traefikTls.RootCAs{}) f.AddParser(reflect.TypeOf(types.Constraints{}), &types.Constraints{}) f.AddParser(reflect.TypeOf(kubernetes.Namespaces{}), &kubernetes.Namespaces{}) f.AddParser(reflect.TypeOf(ecs.Clusters{}), &ecs.Clusters{}) diff --git a/configuration/configuration.go b/configuration/configuration.go index e1432f23a..647dc4bfa 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -1,10 +1,7 @@ package configuration import ( - "crypto/tls" "fmt" - "io/ioutil" - "os" "strings" "time" @@ -25,6 +22,7 @@ import ( "github.com/containous/traefik/provider/rancher" "github.com/containous/traefik/provider/web" "github.com/containous/traefik/provider/zk" + "github.com/containous/traefik/tls" "github.com/containous/traefik/types" ) @@ -64,7 +62,7 @@ type GlobalConfiguration struct { MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" export:"true"` IdleTimeout flaeg.Duration `description:"(Deprecated) maximum amount of time an idle (keep-alive) connection will remain idle before closing itself." export:"true"` // Deprecated InsecureSkipVerify bool `description:"Disable SSL certificate verification" export:"true"` - RootCAs RootCAs `description:"Add cert file for self-signed certificate"` + RootCAs tls.RootCAs `description:"Add cert file for self-signed certificate"` Retry *Retry `description:"Enable retry sending request if network error" export:"true"` HealthCheck *HealthCheckConfig `description:"Health check parameters" export:"true"` RespondingTimeouts *RespondingTimeouts `description:"Timeouts for incoming requests to the Traefik instance" export:"true"` @@ -191,68 +189,6 @@ func (dep *DefaultEntryPoints) Type() string { return "defaultentrypoints" } -// RootCAs hold the CA we want to have in root -type RootCAs []FileOrContent - -// FileOrContent hold a file path or content -type FileOrContent string - -func (f FileOrContent) String() string { - return string(f) -} - -func (f FileOrContent) Read() ([]byte, error) { - var content []byte - if _, err := os.Stat(f.String()); err == nil { - content, err = ioutil.ReadFile(f.String()) - if err != nil { - return nil, err - } - } else { - content = []byte(f) - } - return content, nil -} - -// String is the method to format the flag's value, part of the flag.Value interface. -// The String method's output will be used in diagnostics. -func (r *RootCAs) String() string { - sliceOfString := make([]string, len([]FileOrContent(*r))) - for key, value := range *r { - sliceOfString[key] = value.String() - } - return strings.Join(sliceOfString, ",") -} - -// Set is the method to set the flag value, part of the flag.Value interface. -// Set's argument is a string to be parsed to set the flag. -// It's a comma-separated list, so we split it. -func (r *RootCAs) Set(value string) error { - rootCAs := strings.Split(value, ",") - if len(rootCAs) == 0 { - return fmt.Errorf("bad RootCAs format: %s", value) - } - for _, rootCA := range rootCAs { - *r = append(*r, FileOrContent(rootCA)) - } - return nil -} - -// Get return the EntryPoints map -func (r *RootCAs) Get() interface{} { - return RootCAs(*r) -} - -// SetValue sets the EntryPoints map with val -func (r *RootCAs) SetValue(val interface{}) { - *r = RootCAs(val.(RootCAs)) -} - -// Type is type of the struct -func (r *RootCAs) Type() string { - return "rootcas" -} - // EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...) type EntryPoints map[string]*EntryPoint @@ -268,18 +204,18 @@ func (ep *EntryPoints) String() string { func (ep *EntryPoints) Set(value string) error { result := parseEntryPointsConfiguration(value) - var configTLS *TLS + var configTLS *tls.TLS if len(result["tls"]) > 0 { - certs := Certificates{} + certs := tls.Certificates{} if err := certs.Set(result["tls"]); err != nil { return err } - configTLS = &TLS{ + configTLS = &tls.TLS{ Certificates: certs, } } else if len(result["tls_acme"]) > 0 { - configTLS = &TLS{ - Certificates: Certificates{}, + configTLS = &tls.TLS{ + Certificates: tls.Certificates{}, } } if len(result["ca"]) > 0 { @@ -391,7 +327,7 @@ func (ep *EntryPoints) Type() string { type EntryPoint struct { Network string Address string - TLS *TLS `export:"true"` + TLS *tls.TLS `export:"true"` Redirect *Redirect `export:"true"` Auth *types.Auth `export:"true"` WhitelistSourceRange []string @@ -407,123 +343,6 @@ type Redirect struct { Replacement string } -// TLS configures TLS for an entry point -type TLS struct { - MinVersion string `export:"true"` - CipherSuites []string - Certificates Certificates - ClientCAFiles []string -} - -// MinVersion Map of allowed TLS minimum versions -var MinVersion = map[string]uint16{ - `VersionTLS10`: tls.VersionTLS10, - `VersionTLS11`: tls.VersionTLS11, - `VersionTLS12`: tls.VersionTLS12, -} - -// CipherSuites Map of TLS CipherSuites from crypto/tls -// Available CipherSuites defined at https://golang.org/pkg/crypto/tls/#pkg-constants -var CipherSuites = map[string]uint16{ - `TLS_RSA_WITH_RC4_128_SHA`: tls.TLS_RSA_WITH_RC4_128_SHA, - `TLS_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, - `TLS_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_RSA_WITH_AES_128_CBC_SHA, - `TLS_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_RSA_WITH_AES_256_CBC_SHA, - `TLS_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - `TLS_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - `TLS_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - `TLS_ECDHE_ECDSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, - `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - `TLS_ECDHE_RSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, - `TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, -} - -// Certificates defines traefik certificates type -// Certs and Keys could be either a file path, or the file content itself -type Certificates []Certificate - -//CreateTLSConfig creates a TLS config from Certificate structures -func (certs *Certificates) CreateTLSConfig() (*tls.Config, error) { - config := &tls.Config{} - config.Certificates = []tls.Certificate{} - certsSlice := []Certificate(*certs) - for _, v := range certsSlice { - var err error - - certContent, err := v.CertFile.Read() - if err != nil { - return nil, err - } - - keyContent, err := v.KeyFile.Read() - if err != nil { - return nil, err - } - - cert, err := tls.X509KeyPair(certContent, keyContent) - if err != nil { - return nil, err - } - - config.Certificates = append(config.Certificates, cert) - } - return config, nil -} - -// String is the method to format the flag's value, part of the flag.Value interface. -// The String method's output will be used in diagnostics. -func (certs *Certificates) String() string { - if len(*certs) == 0 { - return "" - } - var result []string - for _, certificate := range *certs { - result = append(result, certificate.CertFile.String()+","+certificate.KeyFile.String()) - } - return strings.Join(result, ";") -} - -// Set is the method to set the flag value, part of the flag.Value interface. -// Set's argument is a string to be parsed to set the flag. -// It's a comma-separated list, so we split it. -func (certs *Certificates) Set(value string) error { - certificates := strings.Split(value, ";") - for _, certificate := range certificates { - files := strings.Split(certificate, ",") - if len(files) != 2 { - return fmt.Errorf("bad certificates format: %s", value) - } - *certs = append(*certs, Certificate{ - CertFile: FileOrContent(files[0]), - KeyFile: FileOrContent(files[1]), - }) - } - return nil -} - -// Type is type of the struct -func (certs *Certificates) Type() string { - return "certificates" -} - -// Certificate holds a SSL cert/key pair -// Certs and Key could be either a file path, or the file content itself -type Certificate struct { - CertFile FileOrContent - KeyFile FileOrContent -} - // Retry contains request retry config type Retry struct { Attempts int `description:"Number of attempts" export:"true"` diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 215036b06..fc0e95480 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -7,6 +7,7 @@ import ( "github.com/containous/flaeg" "github.com/containous/traefik/provider" "github.com/containous/traefik/provider/file" + "github.com/containous/traefik/tls" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -150,12 +151,12 @@ func TestEntryPoints_Set(t *testing.T) { TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, }, WhitelistSourceRange: []string{"Range"}, - TLS: &TLS{ + TLS: &tls.TLS{ ClientCAFiles: []string{"car"}, - Certificates: Certificates{ + Certificates: tls.Certificates{ { - CertFile: FileOrContent("goo"), - KeyFile: FileOrContent("gii"), + CertFile: tls.FileOrContent("goo"), + KeyFile: tls.FileOrContent("gii"), }, }, }, @@ -180,12 +181,12 @@ func TestEntryPoints_Set(t *testing.T) { TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, }, WhitelistSourceRange: []string{"Range"}, - TLS: &TLS{ + TLS: &tls.TLS{ ClientCAFiles: []string{"car"}, - Certificates: Certificates{ + Certificates: tls.Certificates{ { - CertFile: FileOrContent("goo"), - KeyFile: FileOrContent("gii"), + CertFile: tls.FileOrContent("goo"), + KeyFile: tls.FileOrContent("gii"), }, }, }, diff --git a/docs/configuration/backends/file.md b/docs/configuration/backends/file.md index 16f83d528..ff77385ed 100644 --- a/docs/configuration/backends/file.md +++ b/docs/configuration/backends/file.md @@ -8,6 +8,8 @@ You have three choices: - [Rules in a Separate File](/configuration/backends/file/#rules-in-a-separate-file) - [Multiple `.toml` Files](/configuration/backends/file/#multiple-toml-files) +The configuration file allows managing both backends/frontends and HTTPS certificates (which are not [Let's Encrypt](https://letsencrypt.org) certificates generated through Træfik). + ## Simple Add your configuration at the end of the global configuration file `traefik.toml`: @@ -23,12 +25,9 @@ defaultEntryPoints = ["http", "https"] [entryPoints.https] address = ":443" [entryPoints.https.tls] - [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.com.cert" - KeyFile = "integration/fixtures/https/snitest.com.key" - [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.org.cert" - KeyFile = "integration/fixtures/https/snitest.org.key" + [[entryPoints.https.tls.certificates]] + certFile = "integration/fixtures/https/snitest.org.cert" + keyFile = "integration/fixtures/https/snitest.org.key" [file] @@ -81,8 +80,19 @@ defaultEntryPoints = ["http", "https"] entrypoints = ["http", "https"] # overrides defaultEntryPoints backend = "backend2" rule = "Path:/test" + +# HTTPS certificate +[[tlsConfiguration]] +entryPoints = ["https"] + [tlsConfiguration.certificate] + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" ``` +!!! note + adding certificates directly to the entrypoint is still maintained but certificates declared in this way cannot be managed dynamically. + It's recommended to use the file provider to declare certificates. + ## Rules in a Separate File Put your rules in a separate file, for example `rules.toml`: @@ -97,12 +107,6 @@ Put your rules in a separate file, for example `rules.toml`: [entryPoints.https] address = ":443" [entryPoints.https.tls] - [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.com.cert" - KeyFile = "integration/fixtures/https/snitest.com.key" - [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.org.cert" - KeyFile = "integration/fixtures/https/snitest.org.key" [file] filename = "rules.toml" @@ -149,11 +153,23 @@ filename = "rules.toml" entrypoints = ["http", "https"] # overrides defaultEntryPoints backend = "backend2" rule = "Path:/test" +# HTTPS certificate +[[tlsConfiguration]] +entryPoints = ["https"] + [tlsConfiguration.certificate] + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" + +[[tlsConfiguration]] +entryPoints = ["https"] + [[tlsConfiguration.certificates]] + certFile = "integration/fixtures/https/snitest.org.cert" + keyFile = "integration/fixtures/https/snitest.org.key" ``` ## Multiple `.toml` Files -You could have multiple `.toml` files in a directory: +You could have multiple `.toml` files in a directory (and recursively in its sub-directories): ```toml [file] diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index 40074a3d9..47f017baa 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -27,11 +27,11 @@ To redirect an http entrypoint to an https entrypoint (with SNI support). address = ":443" [entryPoints.https.tls] [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.com.cert" - KeyFile = "integration/fixtures/https/snitest.com.key" + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.org.cert" - KeyFile = "integration/fixtures/https/snitest.org.key" + certFile = "integration/fixtures/https/snitest.org.cert" + keyFile = "integration/fixtures/https/snitest.org.key" ``` !!! note @@ -53,6 +53,23 @@ To redirect an entrypoint rewriting the URL. !!! note Please note that `regex` and `replacement` do not have to be set in the `redirect` structure if an entrypoint is defined for the redirection (they will not be used in this case). +## TLS + +Define an entrypoint with SNI support. + +```toml +[entryPoints] + [entryPoints.https] + address = ":443" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" +``` + +!!! note + If an empty TLS configuration is done, default self-signed certificates are generated. + ## TLS Mutual Authentication Only accept clients that present a certificate signed by a specified Certificate Authority (CA). @@ -71,11 +88,11 @@ In the example below both `snitest.com` and `snitest.org` will require client ce [entryPoints.https.tls] ClientCAFiles = ["tests/clientca1.crt", "tests/clientca2.crt"] [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.com.cert" - KeyFile = "integration/fixtures/https/snitest.com.key" + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.org.cert" - KeyFile = "integration/fixtures/https/snitest.org.key" + certFile = "integration/fixtures/https/snitest.org.cert" + keyFile = "integration/fixtures/https/snitest.org.key" ``` ## Authentication diff --git a/docs/user-guide/docker-and-lets-encrypt.md b/docs/user-guide/docker-and-lets-encrypt.md index baec79b72..9f1dce240 100644 --- a/docs/user-guide/docker-and-lets-encrypt.md +++ b/docs/user-guide/docker-and-lets-encrypt.md @@ -78,7 +78,7 @@ Let's take a look at a simple `traefik.toml` configuration as well before we'll ```toml debug = false -checkNewVersion = true + logLevel = "ERROR" defaultEntryPoints = ["https","http"] diff --git a/docs/user-guide/kv-config.md b/docs/user-guide/kv-config.md index 6afd82d7c..3874cb266 100644 --- a/docs/user-guide/kv-config.md +++ b/docs/user-guide/kv-config.md @@ -76,13 +76,13 @@ defaultEntryPoints = ["http", "https"] address = ":443" [entryPoints.https.tls] [[entryPoints.https.tls.certificates]] - CertFile = "integration/fixtures/https/snitest.com.cert" - KeyFile = "integration/fixtures/https/snitest.com.key" + certFile = "integration/fixtures/https/snitest.com.cert" + keyFile = "integration/fixtures/https/snitest.com.key" [[entryPoints.https.tls.certificates]] - CertFile = """-----BEGIN CERTIFICATE----- + certFile = """-----BEGIN CERTIFICATE----- -----END CERTIFICATE-----""" - KeyFile = """-----BEGIN CERTIFICATE----- + keyFile = """-----BEGIN CERTIFICATE----- -----END CERTIFICATE-----""" diff --git a/integration/acme_test.go b/integration/acme_test.go index 61e246500..52326f7ca 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -92,6 +92,26 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C s.retrieveAcmeCertificate(c, testCase) } +// Test OnDemand option with a wildcard provided certificate +func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithDynamicWildcard(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml", + onDemand: true, + domainToCheck: wildcardDomain} + + s.retrieveAcmeCertificate(c, testCase) +} + +// Test onHostRule option with a wildcard provided certificate +func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithDynamicWildcard(c *check.C) { + testCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml", + onDemand: false, + domainToCheck: wildcardDomain} + + s.retrieveAcmeCertificate(c, testCase) +} + // Doing an HTTPS request and test the response certificate func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) { file := s.adaptFile(c, testCase.traefikConfFilePath, struct { diff --git a/integration/fixtures/acme/acme_provided.toml b/integration/fixtures/acme/acme_provided.toml index dcd067df4..a478722b0 100644 --- a/integration/fixtures/acme/acme_provided.toml +++ b/integration/fixtures/acme/acme_provided.toml @@ -9,8 +9,8 @@ defaultEntryPoints = ["http", "https"] address = ":5001" [entryPoints.https.tls] [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/acme/ssl/wildcard.crt" - KeyFile = "fixtures/acme/ssl/wildcard.key" + certFile = "fixtures/acme/ssl/wildcard.crt" + keyFile = "fixtures/acme/ssl/wildcard.key" [acme] email = "test@traefik.io" diff --git a/integration/fixtures/acme/acme_provided_dynamic.toml b/integration/fixtures/acme/acme_provided_dynamic.toml new file mode 100644 index 000000000..a849b7a92 --- /dev/null +++ b/integration/fixtures/acme/acme_provided_dynamic.toml @@ -0,0 +1,23 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http", "https"] + +[entryPoints] + [entryPoints.http] + address = ":8080" + [entryPoints.https] + address = ":5001" + [entryPoints.https.tls] + + +[acme] +email = "test@traefik.io" +storage = "/dev/null" +entryPoint = "https" +onDemand = {{.OnDemand}} +OnHostRule = {{.OnHostRule}} +caServer = "http://{{.BoulderHost}}:4000/directory" + +[file] +filename = "fixtures/acme/certificates.toml" +watch = true diff --git a/integration/fixtures/acme/certificates.toml b/integration/fixtures/acme/certificates.toml new file mode 100644 index 000000000..5e0693f96 --- /dev/null +++ b/integration/fixtures/acme/certificates.toml @@ -0,0 +1,16 @@ +[backends] + [backends.backend] + [backends.backend.servers.server1] + url = "http://127.0.0.1:9010" + +[frontends] + [frontends.frontend] + backend = "backend" + [frontends.frontend.routes.test] + rule = "Host:traefik.acme.wtf" + +[[tlsConfiguration]] +entryPoints = ["https"] + [tlsConfiguration.certificate] + certFile = "fixtures/acme/ssl/wildcard.crt" + keyFile = "fixtures/acme/ssl/wildcard.key" \ No newline at end of file diff --git a/integration/fixtures/grpc/config.toml b/integration/fixtures/grpc/config.toml index 9fc82b676..4a37aa80f 100644 --- a/integration/fixtures/grpc/config.toml +++ b/integration/fixtures/grpc/config.toml @@ -7,8 +7,8 @@ RootCAs = [ """{{ .CertContent }}""" ] address = ":4443" [entryPoints.https.tls] [[entryPoints.https.tls.certificates]] - CertFile = """{{ .CertContent }}""" - KeyFile = """{{ .KeyContent }}""" + certFile = """{{ .CertContent }}""" + keyFile = """{{ .KeyContent }}""" [web] diff --git a/integration/fixtures/grpc/config_insecure.toml b/integration/fixtures/grpc/config_insecure.toml index 66285ce06..524ab20b7 100644 --- a/integration/fixtures/grpc/config_insecure.toml +++ b/integration/fixtures/grpc/config_insecure.toml @@ -7,8 +7,8 @@ InsecureSkipVerify = true address = ":4443" [entryPoints.https.tls] [[entryPoints.https.tls.certificates]] - CertFile = """{{ .CertContent }}""" - KeyFile = """{{ .KeyContent }}""" + certFile = """{{ .CertContent }}""" + keyFile = """{{ .KeyContent }}""" [web] diff --git a/integration/fixtures/https/clientca/https_1ca1config.toml b/integration/fixtures/https/clientca/https_1ca1config.toml index a25c0c75b..5370ed080 100644 --- a/integration/fixtures/https/clientca/https_1ca1config.toml +++ b/integration/fixtures/https/clientca/https_1ca1config.toml @@ -8,11 +8,11 @@ defaultEntryPoints = ["https"] [entryPoints.https.tls] ClientCAFiles = ["fixtures/https/clientca/ca1.crt"] [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.com.cert" - KeyFile = "fixtures/https/snitest.com.key" + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.org.cert" - KeyFile = "fixtures/https/snitest.org.key" + certFile = "fixtures/https/snitest.org.cert" + keyFile = "fixtures/https/snitest.org.key" [web] address = ":8080" diff --git a/integration/fixtures/https/clientca/https_2ca1config.toml b/integration/fixtures/https/clientca/https_2ca1config.toml index 9211cf8dd..5d1e4e9e9 100644 --- a/integration/fixtures/https/clientca/https_2ca1config.toml +++ b/integration/fixtures/https/clientca/https_2ca1config.toml @@ -8,11 +8,11 @@ defaultEntryPoints = ["https"] [entryPoints.https.tls] ClientCAFiles = ["fixtures/https/clientca/ca1and2.crt"] [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.com.cert" - KeyFile = "fixtures/https/snitest.com.key" + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.org.cert" - KeyFile = "fixtures/https/snitest.org.key" + certFile = "fixtures/https/snitest.org.cert" + keyFile = "fixtures/https/snitest.org.key" [web] address = ":8080" diff --git a/integration/fixtures/https/clientca/https_2ca2config.toml b/integration/fixtures/https/clientca/https_2ca2config.toml index 2eec0a82a..66bdbdb54 100644 --- a/integration/fixtures/https/clientca/https_2ca2config.toml +++ b/integration/fixtures/https/clientca/https_2ca2config.toml @@ -8,11 +8,11 @@ defaultEntryPoints = ["https"] [entryPoints.https.tls] ClientCAFiles = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"] [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.com.cert" - KeyFile = "fixtures/https/snitest.com.key" + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.org.cert" - KeyFile = "fixtures/https/snitest.org.key" + certFile = "fixtures/https/snitest.org.cert" + keyFile = "fixtures/https/snitest.org.key" [web] address = ":8080" diff --git a/integration/fixtures/https/dynamic_https.toml b/integration/fixtures/https/dynamic_https.toml new file mode 100644 index 000000000..aa513ba6d --- /dev/null +++ b/integration/fixtures/https/dynamic_https.toml @@ -0,0 +1,67 @@ +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://127.0.0.1:9010" + [backends.backend2] + [backends.backend2.servers.server1] + url = "http://127.0.0.1:9020" + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host:snitest.com" + [frontends.frontend2] + backend = "backend2" + [frontends.frontend2.routes.test_2] + rule = "Host:snitest.org" + +[[tlsConfiguration]] +entryPoints = ["https"] + [tlsConfiguration.certificate] + certFile = """-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIJALAYHG/vGqWEMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV +BAMMC3NuaXRlc3Qub3JnMB4XDTE1MTEyMzIyMDU0NFoXDTI1MTEyMDIyMDU0NFow +FjEUMBIGA1UEAwwLc25pdGVzdC5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC8b2Qv68Xnv4wgJ6HNupxABSUA5KXmv9g7pwwsFMSOK15o2qGFzx/x +9loIi5pMIYIy4SVwJNrYUi772nCYMqSIVXlwct/CE70j2Jb2geIHu3jHbFWXruWb +W1tGGUYzvnsOUziPE3rLWa/NObNYLLlUKJaxfHrxnpuKpQUsXzoLl25cJEVr4jg2 +ZITpdraxaBLisdlWY7EwwHBLu2nxH5Rn+nIjenFfdUwKF9s5dGy63tfBc8LX9yJk ++kOwy1al/Wxa0DUb6rSt0QDCcD+rXnjk2zWPtsHz1btwtqM+FLtN5z0Lmnx7DF3C +tCf1TMzduzZ6aeHk77zc664ZQun5cH33AgMBAAGjUDBOMB0GA1UdDgQWBBRn/nNz +PUsmDKmKv3GGo3km5KKvUDAfBgNVHSMEGDAWgBRn/nNzPUsmDKmKv3GGo3km5KKv +UDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBkuutIcbBdESgvNLLr +k/8HUDuFm72lYHZFE+c76CxqYN52w02NCTiq1InoDUvqZXb/StATBwRRduTUPCj9 +KUkC7pOjAFxjzjExsHrtZSq01WinrxNI+qSKvI8jFngMHnwN1omTt7/D7nxeW5Of +FJTkElnxtELAGHoIwZ+bKprnexefpn9UW84VJvJ2crSR63vBvdTrgsrEGW6kQj1I +62laDpax4+x8t2h+sfG6uNIA1cFrG8Sk+O2Bi3ogB7Y/4e8r6WA23IRP+aSv0J2b +k5fvuuXbIc979pQOoO03zG0S7Wpmpsw+9dQB9TOxGITOLfCZwEuIhnv+M9lLqCks +7H2A +-----END CERTIFICATE-----""" + keyFile = """-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r/YO6cMLBTEjiteaNqh +hc8f8fZaCIuaTCGCMuElcCTa2FIu+9pwmDKkiFV5cHLfwhO9I9iW9oHiB7t4x2xV +l67lm1tbRhlGM757DlM4jxN6y1mvzTmzWCy5VCiWsXx68Z6biqUFLF86C5duXCRF +a+I4NmSE6Xa2sWgS4rHZVmOxMMBwS7tp8R+UZ/pyI3pxX3VMChfbOXRsut7XwXPC +1/ciZPpDsMtWpf1sWtA1G+q0rdEAwnA/q1545Ns1j7bB89W7cLajPhS7Tec9C5p8 +ewxdwrQn9UzM3bs2emnh5O+83OuuGULp+XB99wIDAQABAoIBAGOn9bByXQQnhZAr +5aLMIn6pOdyzEBptM4q42fMmOJ2HyjJiDjKaTCbHRu5mBoBk6FrIP+iDVUo6jKad +7BZSEjoYGlWiKzyU+97NWWmdX1D/kOzHGq1RzhTPyAHWtA4Bm0sEMFFa2AJbuGIt +NfBYFtuva6MKVmsamuBETewdoLEnxzzDFcuOaxXRfTC/ikWcYyB4KEWA5fjroUQC +Llo9/UTGTkh1Hynv9AXY6Qia/RbrIQjKveKCRj6PjxyE/qN9qfmngczz2pK0hRhL +Z+K06y8G+Yj1I1zm5jNg1kakVQKoBsnaYkmIUBUSmWv6ERotedOWtOAMlOKa+0l2 +DS7Ou2ECgYEA91doi+3XrMVsgyTEm/ArzEyRUfM5dCSvBCRFhO7QQp2OYAbjJk5S +pmdpqmwTsXNNMU+XNkWCLug5pk0PTJwP0mVLE2fLYqCCXoyaMltQ0Yk2gaun/RwE +w5EfyMwOQakLFY/ODvduQfyNpaoWgFz4/WPNTVNCGs04LepSGKaFNy0CgYEAwwgV +jKeFA+QZGooTInyk07ZlAbenEPu/c2y3UUFxclP0CjP2/VBOpz9B62vhzCKbjD1c +/L3x1CKC4n4lbeyHi4vrF69LX9SHr1Jm0SUtyKeV3g0EAzIWI0HFhVUkMvtbibQ4 +HXrLVCJO77xetQ7RQszss1z9g3WotAAiBMiQgDMCgYBTLjoilOIrYFmV4Q+dwa95 +DWbxwHJZ9NxG8EvQ4N95B7OR578Matqwy6ZlgeM9kiErrDCWN9oIHGEG5HN4uCM6 +BoaxB/8GNCSj13Uj6kHLtfF2ulvMa1fOzUd7J+TDgC4SGkKaFewmlOCuDf1zPdEe +pimtD4rzqIB0MJFbaOT0IQKBgDBPjlb7IB3ooLdMQJUoXwP6iGa2gXHZioEjCv3b +wihZ13e3i5UQEYuoRcH1RUd1wyYoBSKuQnsT2WwVZ1wlXSYaELAbQgaI9NtfBA0G +sqKjsKICg13vSECPiEgQ4Rin3vLra4MR6c/7d6Y2+RbMhtWPQYrkm/+2Y4XDCqo4 +rGK1AoGAOFZ3RVhuwXzFdKNe32LM1wm1eZ7waxjI4bQS2xUN/3C/uWS7A3LaSlc3 +eRG3DaVpez4DQVupZDHMgxJUYqqKynUj6GD1YiaxGROj3TYCu6e7OxyhalhCllSu +w/X5M802XqzLjeec5zHoZDfknnAkgR9MsxZYmZPFaDyL6GOKUB8= +-----END RSA PRIVATE KEY-----""" diff --git a/integration/fixtures/https/dynamic_https_sni.toml b/integration/fixtures/https/dynamic_https_sni.toml new file mode 100644 index 000000000..19471cc8a --- /dev/null +++ b/integration/fixtures/https/dynamic_https_sni.toml @@ -0,0 +1,16 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["https"] + +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + +[web] + address = ":8080" + +[file] + +fileName = "{{.DynamicConfFileName}}" +watch = true \ No newline at end of file diff --git a/integration/fixtures/https/https_sni.toml b/integration/fixtures/https/https_sni.toml index c88febc7c..855a69ba3 100644 --- a/integration/fixtures/https/https_sni.toml +++ b/integration/fixtures/https/https_sni.toml @@ -7,11 +7,11 @@ defaultEntryPoints = ["https"] address = ":4443" [entryPoints.https.tls] [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.com.cert" - KeyFile = "fixtures/https/snitest.com.key" + certFile = "fixtures/https/snitest.com.cert" + keyFile = "fixtures/https/snitest.com.key" [[entryPoints.https.tls.certificates]] - CertFile = "fixtures/https/snitest.org.cert" - KeyFile = "fixtures/https/snitest.org.key" + certFile = "fixtures/https/snitest.org.cert" + keyFile = "fixtures/https/snitest.org.key" [web] address = ":8080" diff --git a/integration/fixtures/websocket/config_https.toml b/integration/fixtures/websocket/config_https.toml index 97be4964a..73ef23f2c 100644 --- a/integration/fixtures/websocket/config_https.toml +++ b/integration/fixtures/websocket/config_https.toml @@ -8,8 +8,8 @@ InsecureSkipVerify=true address = ":8000" [entryPoints.wss.tls] [[entryPoints.wss.tls.certificates]] - CertFile = "resources/tls/local.cert" - KeyFile = "resources/tls/local.key" + certFile = "resources/tls/local.cert" + keyFile = "resources/tls/local.key" [web] address = ":8080" diff --git a/integration/https_test.go b/integration/https_test.go index 3d277ec94..cb5907f1e 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -1,14 +1,19 @@ package integration import ( + "bytes" "crypto/tls" + "fmt" "net" "net/http" "net/http/httptest" "os" "time" + "github.com/BurntSushi/toml" "github.com/containous/traefik/integration/try" + traefikTls "github.com/containous/traefik/tls" + "github.com/containous/traefik/types" "github.com/go-check/check" checker "github.com/vdemeester/shakers" ) @@ -293,10 +298,10 @@ func (s *HTTPSSuite) TestWithRootCAsContentForHTTPSOnBackend(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains(backend.URL)) + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains(backend.URL)) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8081/ping", 1000*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8081/ping", 1*time.Second, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) } @@ -315,10 +320,10 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains(backend.URL)) + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains(backend.URL)) c.Assert(err, checker.IsNil) - err = try.GetRequest("http://127.0.0.1:8081/ping", 1000*time.Millisecond, try.StatusCodeIs(http.StatusOK)) + err = try.GetRequest("http://127.0.0.1:8081/ping", 1*time.Second, try.StatusCodeIs(http.StatusOK)) c.Assert(err, checker.IsNil) } @@ -338,3 +343,203 @@ func startTestServer(port string, statusCode int) (ts *httptest.Server) { ts.Start() return ts } + +// TestWithSNIConfigRoute involves a client sending HTTPS requests with +// SNI hostnames of "snitest.org" and "snitest.com". The test verifies +// that traefik routes the requests to the expected backends thanks to given certificate if possible +// otherwise thanks to the default one. +func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) { + dynamicConfFileName := s.adaptFile(c, "fixtures/https/dynamic_https.toml", struct{}{}) + defer os.Remove(dynamicConfFileName) + confFileName := s.adaptFile(c, "fixtures/https/dynamic_https_sni.toml", struct { + DynamicConfFileName string + }{ + DynamicConfFileName: dynamicConfFileName, + }) + defer os.Remove(confFileName) + cmd, display := s.traefikCmd(withConfigFile(confFileName)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("Host:"+tr1.TLSClientConfig.ServerName)) + 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, 500*time.Millisecond, try.StatusCodeIs(http.StatusNoContent)) + c.Assert(err, checker.IsNil) + err = try.GetRequest(backend2.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusResetContent)) + c.Assert(err, checker.IsNil) + + client := &http.Client{Transport: tr1} + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + resp, err := client.Do(req) + c.Assert(err, checker.IsNil) + // snitest.org certificate must be used yet + c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, check.Equals, tr1.TLSClientConfig.ServerName) + // Expected a 204 (from backend1) + c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent) + + client = &http.Client{Transport: tr2} + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + resp, err = client.Do(req) + c.Assert(err, checker.IsNil) + // snitest.com certificate does not exist, default certificate has to be used + c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Not(check.Equals), tr2.TLSClientConfig.ServerName) + // Expected a 205 (from backend2) + c.Assert(resp.StatusCode, checker.Equals, http.StatusNoContent) +} + +// TestWithSNIConfigRoute involves a client sending HTTPS requests with +// SNI hostnames of "snitest.org" and "snitest.com". The test verifies +// that traefik updates its configuration when the HTTPS configuration is modified and +// it routes the requests to the expected backends thanks to given certificate if possible +// otherwise thanks to the default one. +func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { + dynamicConfFileName := s.adaptFile(c, "fixtures/https/dynamic_https.toml", struct{}{}) + defer os.Remove(dynamicConfFileName) + confFileName := s.adaptFile(c, "fixtures/https/dynamic_https_sni.toml", struct { + DynamicConfFileName string + }{ + DynamicConfFileName: dynamicConfFileName, + }) + defer os.Remove(confFileName) + cmd, display := s.traefikCmd(withConfigFile(confFileName)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("Host:"+tr2.TLSClientConfig.ServerName)) + 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, 500*time.Millisecond, try.StatusCodeIs(http.StatusNoContent)) + c.Assert(err, checker.IsNil) + err = try.GetRequest(backend2.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusResetContent)) + c.Assert(err, checker.IsNil) + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + client := &http.Client{Transport: tr1} + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + // Change certificates configuration file content + modifyCertificateConfFileContent(c, tr1.TLSClientConfig.ServerName, dynamicConfFileName) + var resp *http.Response + err = try.Do(30*time.Second, func() error { + resp, err = client.Do(req) + + // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\ + req.Close = true + + if err != nil { + return err + } + + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + if cn != tr1.TLSClientConfig.ServerName { + return fmt.Errorf("domain %s found in place of %s", cn, tr1.TLSClientConfig.ServerName) + } + + return nil + }) + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) + client = &http.Client{Transport: tr2} + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + + err = try.Do(60*time.Second, func() error { + resp, err = client.Do(req) + + // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\ + req.Close = true + + if err != nil { + return err + } + + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + if cn == tr2.TLSClientConfig.ServerName { + return fmt.Errorf("domain %s found in place of default one", tr2.TLSClientConfig.ServerName) + } + + return nil + }) + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) +} + +// modifyCertificateConfFileContent replaces the content of a HTTPS configuration file. +func modifyCertificateConfFileContent(c *check.C, certFileName, confFileName string) { + tlsConf := types.Configuration{ + TLSConfiguration: []*traefikTls.Configuration{ + { + Certificate: &traefikTls.Certificate{ + CertFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".cert"), + KeyFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".key"), + }, + EntryPoints: []string{"https"}, + }, + }, + } + var confBuffer bytes.Buffer + e := toml.NewEncoder(&confBuffer) + err := e.Encode(tlsConf) + c.Assert(err, checker.IsNil) + + f, err := os.OpenFile("./"+confFileName, os.O_WRONLY, os.ModeExclusive) + c.Assert(err, checker.IsNil) + defer func() { + f.Close() + }() + f.Truncate(0) + _, err = f.Write(confBuffer.Bytes()) + c.Assert(err, checker.IsNil) +} diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 9c0e2da68..d5a701bc0 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -523,6 +523,11 @@ func (p *Provider) getMaxConnExtractorFunc(container dockerData) string { } func (p *Provider) containerFilter(container dockerData) bool { + if !isContainerEnabled(container, p.ExposedByDefault) { + log.Debugf("Filtering disabled container %s", container.Name) + return false + } + var err error portLabel := "traefik.port label" if p.hasServices(container) { @@ -536,11 +541,6 @@ func (p *Provider) containerFilter(container dockerData) bool { return false } - if !isContainerEnabled(container, p.ExposedByDefault) { - log.Debugf("Filtering disabled container %s", container.Name) - return false - } - constraintTags := strings.Split(container.Labels[types.LabelTags], ",") if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok { if failingConstraint != nil { diff --git a/provider/file/file.go b/provider/file/file.go index 678a51b3a..6744cf489 100644 --- a/provider/file/file.go +++ b/provider/file/file.go @@ -3,13 +3,16 @@ package file import ( "fmt" "io/ioutil" + "os" "path" + "path/filepath" "strings" "github.com/BurntSushi/toml" "github.com/containous/traefik/log" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" + "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "gopkg.in/fsnotify.v1" ) @@ -37,7 +40,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s if p.Directory != "" { watchItem = p.Directory } else { - watchItem = p.Filename + watchItem = filepath.Dir(p.Filename) } if err := p.addWatcher(pool, watchItem, configurationChan, p.watcherCallback); err != nil { @@ -63,7 +66,15 @@ func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationCh case <-stop: return case evt := <-watcher.Events: - callback(configurationChan, evt) + if p.Directory == "" { + _, evtFileName := filepath.Split(evt.Name) + _, confFileName := filepath.Split(p.Filename) + if evtFileName == confFileName { + callback(configurationChan, evt) + } + } else { + callback(configurationChan, evt) + } case err := <-watcher.Errors: log.Errorf("Watcher event error: %s", err) } @@ -92,28 +103,39 @@ func loadFileConfig(filename string) (*types.Configuration, error) { return configuration, nil } -func loadFileConfigFromDirectory(directory string) (*types.Configuration, error) { +func loadFileConfigFromDirectory(directory string, configuration *types.Configuration) (*types.Configuration, error) { fileList, err := ioutil.ReadDir(directory) if err != nil { - return nil, fmt.Errorf("unable to read directory %s: %v", directory, err) + return configuration, fmt.Errorf("unable to read directory %s: %v", directory, err) } - configuration := &types.Configuration{ - Frontends: make(map[string]*types.Frontend), - Backends: make(map[string]*types.Backend), + if configuration == nil { + configuration = &types.Configuration{ + Frontends: make(map[string]*types.Frontend), + Backends: make(map[string]*types.Backend), + TLSConfiguration: make([]*tls.Configuration, 0), + } } - for _, file := range fileList { - if !strings.HasSuffix(file.Name(), ".toml") { + configTLSMaps := make(map[*tls.Configuration]struct{}) + for _, item := range fileList { + + if item.IsDir() { + configuration, err = loadFileConfigFromDirectory(filepath.Join(directory, item.Name()), configuration) + if err != nil { + return configuration, fmt.Errorf("unable to load content configuration from subdirectory %s: %v", item, err) + } + continue + } else if !strings.HasSuffix(item.Name(), ".toml") { continue } var c *types.Configuration - c, err = loadFileConfig(path.Join(directory, file.Name())) + c, err = loadFileConfig(path.Join(directory, item.Name())) if err != nil { - return nil, err + return configuration, err } for backendName, backend := range c.Backends { @@ -131,12 +153,33 @@ func loadFileConfigFromDirectory(directory string) (*types.Configuration, error) configuration.Frontends[frontendName] = frontend } } - } + for _, conf := range c.TLSConfiguration { + if _, exists := configTLSMaps[conf]; exists { + log.Warnf("TLS Configuration %v already configured, skipping", conf) + } else { + configTLSMaps[conf] = struct{}{} + } + } + + } + for conf := range configTLSMaps { + configuration.TLSConfiguration = append(configuration.TLSConfiguration, conf) + } return configuration, nil } func (p *Provider) watcherCallback(configurationChan chan<- types.ConfigMessage, event fsnotify.Event) { + watchItem := p.Filename + if p.Directory != "" { + watchItem = p.Directory + } + + if _, err := os.Stat(watchItem); err != nil { + log.Debugf("Unable to watch %s : %v", watchItem, err) + return + } + configuration, err := p.loadConfig() if err != nil { @@ -149,7 +192,7 @@ func (p *Provider) watcherCallback(configurationChan chan<- types.ConfigMessage, func (p *Provider) loadConfig() (*types.Configuration, error) { if p.Directory != "" { - return loadFileConfigFromDirectory(p.Directory) + return loadFileConfigFromDirectory(p.Directory, nil) } return loadFileConfig(p.Filename) diff --git a/provider/file/file_test.go b/provider/file/file_test.go index 02aada5ce..af622d741 100644 --- a/provider/file/file_test.go +++ b/provider/file/file_test.go @@ -20,13 +20,15 @@ func TestProvideSingleFileAndWatch(t *testing.T) { expectedNumFrontends := 2 expectedNumBackends := 2 + expectedNumTLSConf := 2 tempFile := createFile(t, tempDir, "simple.toml", createFrontendConfiguration(expectedNumFrontends), - createBackendConfiguration(expectedNumBackends)) + createBackendConfiguration(expectedNumBackends), + createTLSConfiguration(expectedNumTLSConf)) - configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends) + configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf) provide(configurationChan, watch, withFile(tempFile)) @@ -37,14 +39,15 @@ func TestProvideSingleFileAndWatch(t *testing.T) { // Now test again with single frontend and backend expectedNumFrontends = 1 expectedNumBackends = 1 + expectedNumTLSConf = 1 createFile(t, tempDir, "simple.toml", createFrontendConfiguration(expectedNumFrontends), - createBackendConfiguration(expectedNumBackends)) + createBackendConfiguration(expectedNumBackends), + createTLSConfiguration(expectedNumTLSConf)) - // Must fail because we don't watch the change - err = waitForSignal(signal, 2*time.Second, "single frontend and backend") + err = waitForSignal(signal, 2*time.Second, "single frontend, backend, TLS configuration") assert.NoError(t, err) } @@ -54,13 +57,15 @@ func TestProvideSingleFileAndNotWatch(t *testing.T) { expectedNumFrontends := 2 expectedNumBackends := 2 + expectedNumTLSConf := 2 tempFile := createFile(t, tempDir, "simple.toml", createFrontendConfiguration(expectedNumFrontends), - createBackendConfiguration(expectedNumBackends)) + createBackendConfiguration(expectedNumBackends), + createTLSConfiguration(expectedNumTLSConf)) - configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends) + configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf) provide(configurationChan, withFile(tempFile)) @@ -71,14 +76,16 @@ func TestProvideSingleFileAndNotWatch(t *testing.T) { // Now test again with single frontend and backend expectedNumFrontends = 1 expectedNumBackends = 1 + expectedNumTLSConf = 1 createFile(t, tempDir, "simple.toml", createFrontendConfiguration(expectedNumFrontends), - createBackendConfiguration(expectedNumBackends)) + createBackendConfiguration(expectedNumBackends), + createTLSConfiguration(expectedNumTLSConf)) // Must fail because we don't watch the changes - err = waitForSignal(signal, 2*time.Second, "single frontend and backend") + err = waitForSignal(signal, 2*time.Second, "single frontend, backend and TLS configuration") assert.Error(t, err) } @@ -88,11 +95,13 @@ func TestProvideDirectoryAndWatch(t *testing.T) { expectedNumFrontends := 2 expectedNumBackends := 2 + expectedNumTLSConf := 2 tempFile1 := createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends)) tempFile2 := createRandomFile(t, tempDir, createBackendConfiguration(expectedNumBackends)) + tempFile3 := createRandomFile(t, tempDir, createTLSConfiguration(expectedNumTLSConf)) - configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends) + configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf) provide(configurationChan, watch, withDirectory(tempDir)) @@ -103,6 +112,7 @@ func TestProvideDirectoryAndWatch(t *testing.T) { // Now remove the backends file expectedNumFrontends = 2 expectedNumBackends = 0 + expectedNumTLSConf = 2 os.Remove(tempFile2.Name()) err = waitForSignal(signal, 2*time.Second, "remove the backends file") assert.NoError(t, err) @@ -110,22 +120,34 @@ func TestProvideDirectoryAndWatch(t *testing.T) { // Now remove the frontends file expectedNumFrontends = 0 expectedNumBackends = 0 + expectedNumTLSConf = 2 os.Remove(tempFile1.Name()) err = waitForSignal(signal, 2*time.Second, "remove the frontends file") assert.NoError(t, err) + + // Now remove the TLS configuration file + expectedNumFrontends = 0 + expectedNumBackends = 0 + expectedNumTLSConf = 0 + os.Remove(tempFile3.Name()) + err = waitForSignal(signal, 2*time.Second, "remove the TLS configuration file") + assert.NoError(t, err) } func TestProvideDirectoryAndNotWatch(t *testing.T) { tempDir := createTempDir(t, "testdir") + tempTLSDir := createSubDir(t, tempDir, "tls") defer os.RemoveAll(tempDir) expectedNumFrontends := 2 expectedNumBackends := 2 + expectedNumTLSConf := 2 createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends)) tempFile2 := createRandomFile(t, tempDir, createBackendConfiguration(expectedNumBackends)) + createRandomFile(t, tempTLSDir, createTLSConfiguration(expectedNumTLSConf)) - configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends) + configurationChan, signal := createConfigurationRoutine(t, &expectedNumFrontends, &expectedNumBackends, &expectedNumTLSConf) provide(configurationChan, withDirectory(tempDir)) @@ -136,6 +158,7 @@ func TestProvideDirectoryAndNotWatch(t *testing.T) { // Now remove the backends file expectedNumFrontends = 2 expectedNumBackends = 0 + expectedNumTLSConf = 2 os.Remove(tempFile2.Name()) // Must fail because we don't watch the changes @@ -144,7 +167,7 @@ func TestProvideDirectoryAndNotWatch(t *testing.T) { } -func createConfigurationRoutine(t *testing.T, expectedNumFrontends *int, expectedNumBackends *int) (chan types.ConfigMessage, chan interface{}) { +func createConfigurationRoutine(t *testing.T, expectedNumFrontends *int, expectedNumBackends *int, expectedNumTLSConfigurations *int) (chan types.ConfigMessage, chan interface{}) { configurationChan := make(chan types.ConfigMessage) signal := make(chan interface{}) @@ -154,6 +177,7 @@ func createConfigurationRoutine(t *testing.T, expectedNumFrontends *int, expecte assert.Equal(t, "file", data.ProviderName) assert.Len(t, data.Configuration.Frontends, *expectedNumFrontends) assert.Len(t, data.Configuration.Backends, *expectedNumBackends) + assert.Len(t, data.Configuration.TLSConfiguration, *expectedNumTLSConfigurations) signal <- nil } }) @@ -207,6 +231,7 @@ func createRandomFile(t *testing.T, tempDir string, contents ...string) *os.File // createFile Helper func createFile(t *testing.T, tempDir string, name string, contents ...string) *os.File { + t.Helper() fileName := path.Join(tempDir, name) tempFile, err := os.Create(fileName) @@ -231,6 +256,7 @@ func createFile(t *testing.T, tempDir string, name string, contents ...string) * // createTempDir Helper func createTempDir(t *testing.T, dir string) string { + t.Helper() d, err := ioutil.TempDir("", dir) if err != nil { t.Fatal(err) @@ -238,6 +264,16 @@ func createTempDir(t *testing.T, dir string) string { return d } +// createDir Helper +func createSubDir(t *testing.T, rootDir, dir string) string { + t.Helper() + err := os.Mkdir(rootDir+"/"+dir, 0775) + if err != nil { + t.Fatal(err) + } + return rootDir + "/" + dir +} + // createFrontendConfiguration Helper func createFrontendConfiguration(n int) string { conf := "[frontends]\n" @@ -260,3 +296,17 @@ func createBackendConfiguration(n int) string { } return conf } + +// createTLSConfiguration Helper +func createTLSConfiguration(n int) string { + var conf string + for i := 1; i <= n; i++ { + conf += fmt.Sprintf(`[[TLSConfiguration]] + EntryPoints = ["https"] + [TLSConfiguration.Certificate] + CertFile = "integration/fixtures/https/snitest%[1]d.com.cert" + KeyFile = "integration/fixtures/https/snitest%[1]d.com.key" +`, i) + } + return conf +} diff --git a/server/server.go b/server/server.go index beda9d6e6..24c8507d7 100644 --- a/server/server.go +++ b/server/server.go @@ -33,6 +33,7 @@ import ( "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" "github.com/containous/traefik/server/cookie" + traefikTls "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/containous/traefik/whitelist" "github.com/streamrail/concurrent-map" @@ -66,6 +67,8 @@ type Server struct { leadership *cluster.Leadership defaultForwardingRoundTripper http.RoundTripper metricsRegistry metrics.Registry + lastReceivedConfiguration *safe.Safe + lastConfigs cmap.ConcurrentMap } type serverEntryPoints map[string]*serverEntryPoint @@ -74,6 +77,7 @@ type serverEntryPoint struct { httpServer *http.Server listener net.Listener httpRouter *middlewares.HandlerSwitcher + certs safe.Safe } type serverRoute struct { @@ -101,6 +105,8 @@ func NewServer(globalConfiguration configuration.GlobalConfiguration) *Server { server.globalConfiguration = globalConfiguration server.routinesPool = safe.NewPool(context.Background()) server.defaultForwardingRoundTripper = createHTTPTransport(globalConfiguration) + server.lastReceivedConfiguration = safe.New(time.Unix(0, 0)) + server.lastConfigs = cmap.New() server.metricsRegistry = metrics.NewVoidRegistry() if globalConfiguration.Web != nil && globalConfiguration.Web.Metrics != nil { @@ -165,7 +171,7 @@ func createHTTPTransport(globalConfiguration configuration.GlobalConfiguration) return transport } -func createRootCACertPool(rootCAs configuration.RootCAs) *x509.CertPool { +func createRootCACertPool(rootCAs traefikTls.RootCAs) *x509.CertPool { roots := x509.NewCertPool() for _, cert := range rootCAs { @@ -317,51 +323,54 @@ func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newS } func (server *Server) listenProviders(stop chan bool) { - lastReceivedConfiguration := safe.New(time.Unix(0, 0)) - lastConfigs := cmap.New() for { select { case <-stop: return case configMsg, ok := <-server.configurationChan: - if !ok { + if !ok || configMsg.Configuration == nil { return } - server.defaultConfigurationValues(configMsg.Configuration) - currentConfigurations := server.currentConfigurations.Get().(types.Configurations) - jsonConf, _ := json.Marshal(configMsg.Configuration) - log.Debugf("Configuration received from provider %s: %s", configMsg.ProviderName, string(jsonConf)) - if configMsg.Configuration == nil || configMsg.Configuration.Backends == nil && configMsg.Configuration.Frontends == nil { - log.Infof("Skipping empty Configuration for provider %s", configMsg.ProviderName) - } else if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) { - log.Infof("Skipping same configuration for provider %s", configMsg.ProviderName) - } else { - lastConfigs.Set(configMsg.ProviderName, &configMsg) - lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time) - providersThrottleDuration := time.Duration(server.globalConfiguration.ProvidersThrottleDuration) - if time.Now().After(lastReceivedConfigurationValue.Add(providersThrottleDuration)) { - log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration.String()) - // last config received more than n s ago - server.configurationValidatedChan <- configMsg - } else { - log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration) - safe.Go(func() { - <-time.After(providersThrottleDuration) - lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time) - if time.Now().After(lastReceivedConfigurationValue.Add(time.Duration(providersThrottleDuration))) { - log.Debugf("Waited for %s config, OK", configMsg.ProviderName) - if lastConfig, ok := lastConfigs.Get(configMsg.ProviderName); ok { - server.configurationValidatedChan <- *lastConfig.(*types.ConfigMessage) - } - } - }) - } - lastReceivedConfiguration.Set(time.Now()) - } + server.preLoadConfiguration(configMsg) } } } +func (server *Server) preLoadConfiguration(configMsg types.ConfigMessage) { + server.defaultConfigurationValues(configMsg.Configuration) + currentConfigurations := server.currentConfigurations.Get().(types.Configurations) + jsonConf, _ := json.Marshal(configMsg.Configuration) + log.Debugf("Configuration received from provider %s: %s", configMsg.ProviderName, string(jsonConf)) + if configMsg.Configuration == nil || configMsg.Configuration.Backends == nil && configMsg.Configuration.Frontends == nil && configMsg.Configuration.TLSConfiguration == nil { + log.Infof("Skipping empty Configuration for provider %s", configMsg.ProviderName) + } else if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) { + log.Infof("Skipping same configuration for provider %s", configMsg.ProviderName) + } else { + server.lastConfigs.Set(configMsg.ProviderName, &configMsg) + lastReceivedConfigurationValue := server.lastReceivedConfiguration.Get().(time.Time) + providersThrottleDuration := time.Duration(server.globalConfiguration.ProvidersThrottleDuration) + if time.Now().After(lastReceivedConfigurationValue.Add(providersThrottleDuration)) { + log.Debugf("Last %s configuration received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration.String()) + // last config received more than n server ago + server.configurationValidatedChan <- configMsg + } else { + log.Debugf("Last %s configuration received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration.String()) + safe.Go(func() { + <-time.After(providersThrottleDuration) + lastReceivedConfigurationValue := server.lastReceivedConfiguration.Get().(time.Time) + if time.Now().After(lastReceivedConfigurationValue.Add(time.Duration(providersThrottleDuration))) { + log.Debugf("Waited for %s configuration, OK", configMsg.ProviderName) + if lastConfig, ok := server.lastConfigs.Get(configMsg.ProviderName); ok { + server.configurationValidatedChan <- *lastConfig.(*types.ConfigMessage) + } + } + }) + } + // Update the last configuration loading time + server.lastReceivedConfiguration.Set(time.Now()) + } +} + func (server *Server) defaultConfigurationValues(configuration *types.Configuration) { if configuration == nil || configuration.Frontends == nil { return @@ -376,34 +385,73 @@ func (server *Server) listenConfigurations(stop chan bool) { case <-stop: return case configMsg, ok := <-server.configurationValidatedChan: - if !ok { + if !ok || configMsg.Configuration == nil { return } - currentConfigurations := server.currentConfigurations.Get().(types.Configurations) - - // Copy configurations to new map so we don't change current if LoadConfig fails - newConfigurations := make(types.Configurations) - for k, v := range currentConfigurations { - newConfigurations[k] = v - } - newConfigurations[configMsg.ProviderName] = configMsg.Configuration - - newServerEntryPoints, err := server.loadConfig(newConfigurations, server.globalConfiguration) - if err == nil { - for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints { - server.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler()) - log.Infof("Server configuration reloaded on %s", server.serverEntryPoints[newServerEntryPointName].httpServer.Addr) - } - server.currentConfigurations.Set(newConfigurations) - server.postLoadConfig() - } else { - log.Error("Error loading new configuration, aborted ", err) - } + server.loadConfiguration(configMsg) } } } -func (server *Server) postLoadConfig() { +// loadConfiguration manages dynamically frontends, backends and TLS configurations +func (server *Server) loadConfiguration(configMsg types.ConfigMessage) { + currentConfigurations := server.currentConfigurations.Get().(types.Configurations) + + // Copy configurations to new map so we don't change current if LoadConfig fails + newConfigurations := make(types.Configurations) + for k, v := range currentConfigurations { + newConfigurations[k] = v + } + newConfigurations[configMsg.ProviderName] = configMsg.Configuration + + newServerEntryPoints, err := server.loadConfig(newConfigurations, server.globalConfiguration) + if err == nil { + for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints { + server.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler()) + if &newServerEntryPoint.certs != nil { + server.serverEntryPoints[newServerEntryPointName].certs.Set(newServerEntryPoint.certs.Get()) + } + log.Infof("Server configuration reloaded on %s", server.serverEntryPoints[newServerEntryPointName].httpServer.Addr) + } + server.currentConfigurations.Set(newConfigurations) + server.postLoadConfiguration() + } else { + log.Error("Error loading new configuration, aborted ", err) + } +} + +// loadHTTPSConfiguration add/delete HTTPS certificate managed dynamically +func (server *Server) loadHTTPSConfiguration(configurations types.Configurations) (map[string]*traefikTls.DomainsCertificates, error) { + newEPCertificates := make(map[string]*traefikTls.DomainsCertificates) + // Get all certificates + for _, configuration := range configurations { + if configuration.TLSConfiguration != nil && len(configuration.TLSConfiguration) > 0 { + if err := traefikTls.SortTLSConfigurationPerEntryPoints(configuration.TLSConfiguration, newEPCertificates); err != nil { + return nil, err + } + } + } + return newEPCertificates, nil +} + +// getCertificate allows to customize tlsConfig.Getcertificate behaviour to get the certificates inserted dynamically +func (s *serverEntryPoint) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if s.certs.Get() != nil { + domainToCheck := types.CanonicalDomain(clientHello.ServerName) + for domains, cert := range *s.certs.Get().(*traefikTls.DomainsCertificates) { + for _, domain := range strings.Split(domains, ",") { + selector := "^" + strings.Replace(domain, "*.", "[^\\.]*\\.?", -1) + "$" + domainCheck, _ := regexp.MatchString(selector, domainToCheck) + if domainCheck { + return cert, nil + } + } + } + } + return nil, nil +} + +func (server *Server) postLoadConfiguration() { if server.globalConfiguration.ACME == nil { return } @@ -418,8 +466,8 @@ func (server *Server) postLoadConfig() { // check if one of the frontend entrypoints is configured with TLS // and is configured with ACME ACMEEnabled := false - for _, entrypoint := range frontend.EntryPoints { - if server.globalConfiguration.ACME.EntryPoint == entrypoint && server.globalConfiguration.EntryPoints[entrypoint].TLS != nil { + for _, entryPoint := range frontend.EntryPoints { + if server.globalConfiguration.ACME.EntryPoint == entryPoint && server.globalConfiguration.EntryPoints[entryPoint].TLS != nil { ACMEEnabled = true break } @@ -508,12 +556,12 @@ func (server *Server) startProviders() { } } -func createClientTLSConfig(tlsOption *configuration.TLS) (*tls.Config, error) { +func createClientTLSConfig(entryPointName string, tlsOption *traefikTls.TLS) (*tls.Config, error) { if tlsOption == nil { return nil, errors.New("no TLS provided") } - config, err := tlsOption.Certificates.CreateTLSConfig() + config, _, err := tlsOption.Certificates.CreateTLSConfig(entryPointName) if err != nil { return nil, err } @@ -536,16 +584,22 @@ func createClientTLSConfig(tlsOption *configuration.TLS) (*tls.Config, error) { } // creates a TLS config that allows terminating HTTPS for multiple domains using SNI -func (server *Server) createTLSConfig(entryPointName string, tlsOption *configuration.TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) { +func (server *Server) createTLSConfig(entryPointName string, tlsOption *traefikTls.TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) { if tlsOption == nil { return nil, nil } - config, err := tlsOption.Certificates.CreateTLSConfig() + config, epDomainsCertificates, err := tlsOption.Certificates.CreateTLSConfig(entryPointName) if err != nil { return nil, err } - + epDomainsCertificatesTmp := new(traefikTls.DomainsCertificates) + if epDomainsCertificates[entryPointName] != nil { + epDomainsCertificatesTmp = epDomainsCertificates[entryPointName] + } else { + *epDomainsCertificatesTmp = make(map[string]*tls.Certificate) + } + server.serverEntryPoints[entryPointName].certs.Set(epDomainsCertificatesTmp) // ensure http2 enabled config.NextProtos = []string{"h2", "http/1.1"} @@ -578,12 +632,12 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur return false } if server.leadership == nil { - err := server.globalConfiguration.ACME.CreateLocalConfig(config, checkOnDemandDomain) + err := server.globalConfiguration.ACME.CreateLocalConfig(config, &server.serverEntryPoints[entryPointName].certs, checkOnDemandDomain) if err != nil { return nil, err } } else { - err := server.globalConfiguration.ACME.CreateClusterConfig(server.leadership, config, checkOnDemandDomain) + err := server.globalConfiguration.ACME.CreateClusterConfig(server.leadership, config, &server.serverEntryPoints[entryPointName].certs, checkOnDemandDomain) if err != nil { return nil, err } @@ -592,6 +646,8 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur } else { return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + " for ACME configuration") } + } else { + config.GetCertificate = server.serverEntryPoints[entryPointName].getCertificate } if len(config.Certificates) == 0 { return nil, errors.New("No certificates found for TLS entrypoint " + entryPointName) @@ -600,7 +656,7 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur // in each certificate and populates the config.NameToCertificate map. config.BuildNameToCertificate() //Set the minimum TLS version if set in the config TOML - if minConst, exists := configuration.MinVersion[server.globalConfiguration.EntryPoints[entryPointName].TLS.MinVersion]; exists { + if minConst, exists := traefikTls.MinVersion[server.globalConfiguration.EntryPoints[entryPointName].TLS.MinVersion]; exists { config.PreferServerCipherSuites = true config.MinVersion = minConst } @@ -609,7 +665,7 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur //if our list of CipherSuites is defined in the entrypoint config, we can re-initilize the suites list as empty config.CipherSuites = make([]uint16, 0) for _, cipher := range server.globalConfiguration.EntryPoints[entryPointName].TLS.CipherSuites { - if cipherConst, exists := configuration.CipherSuites[cipher]; exists { + if cipherConst, exists := traefikTls.CipherSuites[cipher]; exists { config.CipherSuites = append(config.CipherSuites, cipherConst) } else { //CipherSuite listed in the toml does not exist in our listed @@ -617,7 +673,6 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur } } } - return config, nil } @@ -721,9 +776,9 @@ func (server *Server) buildEntryPoints(globalConfiguration configuration.GlobalC // getRoundTripper will either use server.defaultForwardingRoundTripper or create a new one // given a custom TLS configuration is passed and the passTLSCert option is set to true. -func (server *Server) getRoundTripper(globalConfiguration configuration.GlobalConfiguration, passTLSCert bool, tls *configuration.TLS) (http.RoundTripper, error) { +func (server *Server) getRoundTripper(entryPointName string, globalConfiguration configuration.GlobalConfiguration, passTLSCert bool, tls *traefikTls.TLS) (http.RoundTripper, error) { if passTLSCert { - tlsConfig, err := createClientTLSConfig(tls) + tlsConfig, err := createClientTLSConfig(entryPointName, tls) if err != nil { log.Errorf("Failed to create TLSClientConfig: %s", err) return nil, err @@ -802,7 +857,7 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf if backends[entryPointName+frontend.Backend] == nil { log.Debugf("Creating backend %s", frontend.Backend) - roundTripper, err := server.getRoundTripper(globalConfiguration, frontend.PassTLSCert, entryPoint.TLS) + roundTripper, err := server.getRoundTripper(entryPointName, globalConfiguration, frontend.PassTLSCert, entryPoint.TLS) if err != nil { log.Errorf("Failed to create RoundTripper for frontend %s: %v", frontendName, err) log.Errorf("Skipping frontend %s...", frontendName) @@ -1019,11 +1074,19 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf } } healthcheck.GetHealthCheck().SetBackendsConfiguration(server.routinesPool.Ctx(), backendsHealthCheck) - //sort routes - for _, serverEntryPoint := range serverEntryPoints { + // Get new certificates list sorted per entrypoints + // Update certificates + entryPointsCertificates, err := server.loadHTTPSConfiguration(configurations) + //sort routes and update certificates + for serverEntryPointName, serverEntryPoint := range serverEntryPoints { serverEntryPoint.httpRouter.GetHandler().SortRoutes() + _, exists := entryPointsCertificates[serverEntryPointName] + if exists { + serverEntryPoint.certs.Set(entryPointsCertificates[serverEntryPointName]) + } } - return serverEntryPoints, nil + + return serverEntryPoints, err } func configureLBServers(lb healthcheck.LoadBalancer, config *types.Configuration, frontend *types.Frontend) error { diff --git a/server/server_test.go b/server/server_test.go index f661a44a3..f8aa82c2b 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -16,6 +16,7 @@ import ( "github.com/containous/traefik/metrics" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/testhelpers" + "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" @@ -24,6 +25,44 @@ import ( "github.com/vulcand/oxy/roundrobin" ) +// LocalhostCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// generated from src/crypto/tls: +// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var ( + localhostCert = tls.FileOrContent(`-----BEGIN CERTIFICATE----- +MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw +MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB +iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 +iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul +rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO +BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw +AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA +AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 +tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs +h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM +fblo6RBxUQ== +-----END CERTIFICATE-----`) + + // LocalhostKey is the private key for localhostCert. + localhostKey = tls.FileOrContent(`-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9 +SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB +l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB +AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet +3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb +uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H +qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp +jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY +fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U +fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU +y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX +qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo +f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== +-----END RSA PRIVATE KEY-----`) +) + type testLoadBalancer struct{} func (lb *testLoadBalancer) RemoveServer(u *url.URL) error { @@ -241,6 +280,15 @@ func TestServerLoadConfigHealthCheckOptions(t *testing.T) { HealthCheck: healthCheck, }, }, + TLSConfiguration: []*tls.Configuration{ + { + Certificate: &tls.Certificate{ + CertFile: localhostCert, + KeyFile: localhostKey, + }, + EntryPoints: []string{"http"}, + }, + }, }, } @@ -413,6 +461,15 @@ func TestServerLoadConfigEmptyBasicAuth(t *testing.T) { }, }, }, + TLSConfiguration: []*tls.Configuration{ + { + Certificate: &tls.Certificate{ + CertFile: localhostCert, + KeyFile: localhostKey, + }, + EntryPoints: []string{"http"}, + }, + }, }, } diff --git a/tls/certificate.go b/tls/certificate.go new file mode 100644 index 000000000..09a69bb57 --- /dev/null +++ b/tls/certificate.go @@ -0,0 +1,206 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + "sort" + "strings" + + "github.com/containous/traefik/log" + "github.com/containous/traefik/tls/generate" +) + +var ( + // MinVersion Map of allowed TLS minimum versions + MinVersion = map[string]uint16{ + `VersionTLS10`: tls.VersionTLS10, + `VersionTLS11`: tls.VersionTLS11, + `VersionTLS12`: tls.VersionTLS12, + } + + // CipherSuites Map of TLS CipherSuites from crypto/tls + // Available CipherSuites defined at https://golang.org/pkg/crypto/tls/#pkg-constants + CipherSuites = map[string]uint16{ + `TLS_RSA_WITH_RC4_128_SHA`: tls.TLS_RSA_WITH_RC4_128_SHA, + `TLS_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + `TLS_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_RSA_WITH_AES_128_CBC_SHA, + `TLS_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_RSA_WITH_AES_256_CBC_SHA, + `TLS_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + `TLS_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + `TLS_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + `TLS_ECDHE_ECDSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + `TLS_ECDHE_RSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + `TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + } +) + +// Certificate holds a SSL cert/key pair +// Certs and Key could be either a file path, or the file content itself +type Certificate struct { + CertFile FileOrContent + KeyFile FileOrContent +} + +// Certificates defines traefik certificates type +// Certs and Keys could be either a file path, or the file content itself +type Certificates []Certificate + +// FileOrContent hold a file path or content +type FileOrContent string + +func (f FileOrContent) String() string { + return string(f) +} + +func (f FileOrContent) Read() ([]byte, error) { + var content []byte + if _, err := os.Stat(f.String()); err == nil { + content, err = ioutil.ReadFile(f.String()) + if err != nil { + return nil, err + } + } else { + content = []byte(f) + } + return content, nil +} + +// CreateTLSConfig creates a TLS config from Certificate structures +func (c *Certificates) CreateTLSConfig(entryPointName string) (*tls.Config, map[string]*DomainsCertificates, error) { + config := &tls.Config{} + domainsCertificates := make(map[string]*DomainsCertificates) + if c.isEmpty() { + config.Certificates = make([]tls.Certificate, 0) + cert, err := generate.DefaultCertificate() + if err != nil { + return nil, nil, err + } + config.Certificates = append(config.Certificates, *cert) + } else { + for _, certificate := range *c { + err := certificate.AppendCertificates(domainsCertificates, entryPointName) + if err != nil { + return nil, nil, err + } + for _, certDom := range domainsCertificates { + for _, cert := range certDom.Get().(map[string]*tls.Certificate) { + config.Certificates = append(config.Certificates, *cert) + } + } + } + } + return config, domainsCertificates, nil +} + +// isEmpty checks if the certificates list is empty +func (c *Certificates) isEmpty() bool { + if len(*c) == 0 { + return true + } + var key int + for _, cert := range *c { + if len(cert.CertFile.String()) != 0 && len(cert.KeyFile.String()) != 0 { + break + } + key++ + } + return key == len(*c) +} + +// AppendCertificates appends a Certificate to a certificates map sorted by entrypoints +func (c *Certificate) AppendCertificates(certs map[string]*DomainsCertificates, ep string) error { + + certContent, err := c.CertFile.Read() + if err != nil { + return err + } + + keyContent, err := c.KeyFile.Read() + if err != nil { + return err + } + tlsCert, err := tls.X509KeyPair(certContent, keyContent) + if err != nil { + return err + } + + parsedCert, _ := x509.ParseCertificate(tlsCert.Certificate[0]) + + certKey := parsedCert.Subject.CommonName + if parsedCert.DNSNames != nil { + sort.Strings(parsedCert.DNSNames) + certKey += fmt.Sprintf("%s,%s", parsedCert.Subject.CommonName, strings.Join(parsedCert.DNSNames, ",")) + } + + certExists := false + if certs[ep] == nil { + certs[ep] = new(DomainsCertificates) + *certs[ep] = make(map[string]*tls.Certificate) + } else { + for domains := range *certs[ep] { + if domains == certKey { + certExists = true + break + } + } + } + if certExists { + log.Warnf("Into EntryPoint %s, try to add certificate for domains which already have a certificate (%s). The new certificate will not be append to the EntryPoint.", ep, certKey) + } else { + log.Debugf("Add certificate for domains %s", certKey) + err = certs[ep].add(certKey, &tlsCert) + } + + return err +} + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (c *Certificates) String() string { + if len(*c) == 0 { + return "" + } + var result []string + for _, certificate := range *c { + result = append(result, certificate.CertFile.String()+","+certificate.KeyFile.String()) + } + return strings.Join(result, ";") +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +// It's a comma-separated list, so we split it. +func (c *Certificates) Set(value string) error { + certificates := strings.Split(value, ";") + for _, certificate := range certificates { + files := strings.Split(certificate, ",") + if len(files) != 2 { + return fmt.Errorf("bad certificates format: %s", value) + } + *c = append(*c, Certificate{ + CertFile: FileOrContent(files[0]), + KeyFile: FileOrContent(files[1]), + }) + } + return nil +} + +// Type is type of the struct +func (c *Certificates) Type() string { + return "certificates" +} diff --git a/tls/generate/generate.go b/tls/generate/generate.go new file mode 100644 index 000000000..842327dab --- /dev/null +++ b/tls/generate/generate.go @@ -0,0 +1,91 @@ +package generate + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "encoding/pem" + "fmt" + "math/big" + "time" +) + +// DefaultCertificate generates random TLS certificates +func DefaultCertificate() (*tls.Certificate, error) { + randomBytes := make([]byte, 100) + _, err := rand.Read(randomBytes) + if err != nil { + return nil, err + } + zBytes := sha256.Sum256(randomBytes) + z := hex.EncodeToString(zBytes[:sha256.Size]) + domain := fmt.Sprintf("%s.%s.traefik.default", z[:32], z[32:]) + + certPEM, keyPEM, err := KeyPair(domain, time.Time{}) + if err != nil { + return nil, err + } + + certificate, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return nil, err + } + + return &certificate, nil +} + +// KeyPair generates cert and key files +func KeyPair(domain string, expiration time.Time) ([]byte, []byte, error) { + rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)}) + + certPEM, err := PemCert(rsaPrivKey, domain, expiration) + if err != nil { + return nil, nil, err + } + return certPEM, keyPEM, nil +} + +// PemCert generates PEM cert file +func PemCert(privKey *rsa.PrivateKey, domain string, expiration time.Time) ([]byte, error) { + derBytes, err := derCert(privKey, expiration, domain) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil +} + +func derCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + if expiration.IsZero() { + expiration = time.Now().Add(365) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "TRAEFIK DEFAULT CERT", + }, + NotBefore: time.Now(), + NotAfter: expiration, + + KeyUsage: x509.KeyUsageKeyEncipherment, + BasicConstraintsValid: true, + DNSNames: []string{domain}, + } + + return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) +} diff --git a/tls/tls.go b/tls/tls.go new file mode 100644 index 000000000..bfe9c8c1a --- /dev/null +++ b/tls/tls.go @@ -0,0 +1,94 @@ +package tls + +import ( + "crypto/tls" + "fmt" + "strings" +) + +// TLS configures TLS for an entry point +type TLS struct { + MinVersion string `export:"true"` + CipherSuites []string + Certificates Certificates + ClientCAFiles []string +} + +// RootCAs hold the CA we want to have in root +type RootCAs []FileOrContent + +// DomainsCertificates allows mapping TLS certificates to a list of domains +type DomainsCertificates map[string]*tls.Certificate + +// Configuration allows mapping a TLS certificate to a list of entrypoints +type Configuration struct { + EntryPoints []string + Certificate *Certificate +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +// It's a comma-separated list, so we split it. +func (dc *DomainsCertificates) add(domain string, cert *tls.Certificate) error { + dc.Get().(map[string]*tls.Certificate)[domain] = cert + return nil +} + +// Get method allow getting the map stored into the DomainsCertificates +func (dc *DomainsCertificates) Get() interface{} { + return map[string]*tls.Certificate(*dc) +} + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (r *RootCAs) String() string { + sliceOfString := make([]string, len([]FileOrContent(*r))) + for key, value := range *r { + sliceOfString[key] = value.String() + } + return strings.Join(sliceOfString, ",") +} + +// Set is the method to set the flag value, part of the flag.Value interface. +// Set's argument is a string to be parsed to set the flag. +// It's a comma-separated list, so we split it. +func (r *RootCAs) Set(value string) error { + rootCAs := strings.Split(value, ",") + if len(rootCAs) == 0 { + return fmt.Errorf("bad RootCAs format: %s", value) + } + for _, rootCA := range rootCAs { + *r = append(*r, FileOrContent(rootCA)) + } + return nil +} + +// Get return the RootCAs list +func (r *RootCAs) Get() interface{} { + return RootCAs(*r) +} + +// SetValue sets the RootCAs with val +func (r *RootCAs) SetValue(val interface{}) { + *r = RootCAs(val.(RootCAs)) +} + +// Type is type of the struct +func (r *RootCAs) Type() string { + return "rootcas" +} + +// SortTLSConfigurationPerEntryPoints converts TLS configuration sorted by Certificates into TLS configuration sorted by EntryPoints +func SortTLSConfigurationPerEntryPoints(configurations []*Configuration, epConfiguration map[string]*DomainsCertificates) error { + if epConfiguration == nil { + epConfiguration = make(map[string]*DomainsCertificates) + } + for _, conf := range configurations { + for _, ep := range conf.EntryPoints { + if err := conf.Certificate.AppendCertificates(epConfiguration, ep); err != nil { + return err + } + } + } + return nil +} diff --git a/types/types.go b/types/types.go index eb731d1e2..e27df5754 100644 --- a/types/types.go +++ b/types/types.go @@ -13,6 +13,7 @@ import ( "github.com/containous/flaeg" "github.com/containous/traefik/log" + traefikTls "github.com/containous/traefik/tls" "github.com/docker/libkv/store" "github.com/ryanuber/go-glob" ) @@ -188,8 +189,9 @@ type Configurations map[string]*Configuration // Configuration of a provider. type Configuration struct { - Backends map[string]*Backend `json:"backends,omitempty"` - Frontends map[string]*Frontend `json:"frontends,omitempty"` + Backends map[string]*Backend `json:"backends,omitempty"` + Frontends map[string]*Frontend `json:"frontends,omitempty"` + TLSConfiguration []*traefikTls.Configuration `json:"tlsConfiguration,omitempty"` } // ConfigMessage hold configuration information exchanged between parts of traefik.