Make the TLS certificates management dynamic.
This commit is contained in:
parent
f6aa147c78
commit
c469e669fd
36 changed files with 1257 additions and 513 deletions
28
acme/acme.go
28
acme/acme.go
|
@ -18,6 +18,8 @@ import (
|
||||||
"github.com/containous/traefik/cluster"
|
"github.com/containous/traefik/cluster"
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/log"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
|
traefikTls "github.com/containous/traefik/tls"
|
||||||
|
"github.com/containous/traefik/tls/generate"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/eapache/channels"
|
"github.com/eapache/channels"
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
|
@ -49,6 +51,7 @@ type ACME struct {
|
||||||
checkOnDemandDomain func(domain string) bool
|
checkOnDemandDomain func(domain string) bool
|
||||||
jobs *channels.InfiniteChannel
|
jobs *channels.InfiniteChannel
|
||||||
TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"`
|
TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"`
|
||||||
|
dynamicCerts *safe.Safe
|
||||||
}
|
}
|
||||||
|
|
||||||
//Domains parse []Domain
|
//Domains parse []Domain
|
||||||
|
@ -99,7 +102,7 @@ func (a *ACME) init() error {
|
||||||
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
||||||
}
|
}
|
||||||
// no certificates in TLS config, so we add a default one
|
// no certificates in TLS config, so we add a default one
|
||||||
cert, err := generateDefaultCertificate()
|
cert, err := generate.DefaultCertificate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -114,7 +117,7 @@ func (a *ACME) init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateClusterConfig creates a tls.config using ACME configuration in cluster mode
|
// 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()
|
err := a.init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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")
|
return errors.New("Empty Store, please provide a key for certs storage")
|
||||||
}
|
}
|
||||||
a.checkOnDemandDomain = checkOnDemandDomain
|
a.checkOnDemandDomain = checkOnDemandDomain
|
||||||
|
a.dynamicCerts = certs
|
||||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
||||||
tlsConfig.GetCertificate = a.getCertificate
|
tlsConfig.GetCertificate = a.getCertificate
|
||||||
a.TLSConfig = tlsConfig
|
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
|
// 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()
|
err := a.init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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")
|
return errors.New("Empty Store, please provide a filename for certs storage")
|
||||||
}
|
}
|
||||||
a.checkOnDemandDomain = checkOnDemandDomain
|
a.checkOnDemandDomain = checkOnDemandDomain
|
||||||
|
a.dynamicCerts = certs
|
||||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
||||||
tlsConfig.GetCertificate = a.getCertificate
|
tlsConfig.GetCertificate = a.getCertificate
|
||||||
a.TLSConfig = tlsConfig
|
a.TLSConfig = tlsConfig
|
||||||
|
@ -583,11 +588,21 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get provided certificate which check a domains list (Main and SANs)
|
// 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 {
|
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
|
// Use regex to test for provided certs that might have been added into TLSConfig
|
||||||
providedCertMatch := false
|
providedCertMatch := false
|
||||||
log.Debugf("Look for provided certificate to validate %s...", domains)
|
for k := range certs {
|
||||||
for k := range a.TLSConfig.NameToCertificate {
|
|
||||||
selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$"
|
selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$"
|
||||||
for _, domainToCheck := range domains {
|
for _, domainToCheck := range domains {
|
||||||
providedCertMatch, _ = regexp.MatchString(selector, domainToCheck)
|
providedCertMatch, _ = regexp.MatchString(selector, domainToCheck)
|
||||||
|
@ -597,11 +612,10 @@ func (a *ACME) getProvidedCertificate(domains []string) *tls.Certificate {
|
||||||
}
|
}
|
||||||
if providedCertMatch {
|
if providedCertMatch {
|
||||||
log.Debugf("Got provided certificate for domains %s", domains)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/tls/generate"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/xenolf/lego/acme"
|
"github.com/xenolf/lego/acme"
|
||||||
)
|
)
|
||||||
|
@ -70,8 +71,8 @@ func TestDomainsSetAppend(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCertificatesRenew(t *testing.T) {
|
func TestCertificatesRenew(t *testing.T) {
|
||||||
foo1Cert, foo1Key, _ := generateKeyPair("foo1.com", time.Now())
|
foo1Cert, foo1Key, _ := generate.KeyPair("foo1.com", time.Now())
|
||||||
foo2Cert, foo2Key, _ := generateKeyPair("foo2.com", time.Now())
|
foo2Cert, foo2Key, _ := generate.KeyPair("foo2.com", time.Now())
|
||||||
domainsCertificates := DomainsCertificates{
|
domainsCertificates := DomainsCertificates{
|
||||||
lock: sync.RWMutex{},
|
lock: sync.RWMutex{},
|
||||||
Certs: []*DomainsCertificate{
|
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{
|
newCertificate := &Certificate{
|
||||||
Domain: "foo1.com",
|
Domain: "foo1.com",
|
||||||
CertURL: "url",
|
CertURL: "url",
|
||||||
|
@ -128,10 +129,10 @@ func TestCertificatesRenew(t *testing.T) {
|
||||||
|
|
||||||
func TestRemoveDuplicates(t *testing.T) {
|
func TestRemoveDuplicates(t *testing.T) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
fooCert, fooKey, _ := generateKeyPair("foo.com", now)
|
fooCert, fooKey, _ := generate.KeyPair("foo.com", now)
|
||||||
foo24Cert, foo24Key, _ := generateKeyPair("foo.com", now.Add(24*time.Hour))
|
foo24Cert, foo24Key, _ := generate.KeyPair("foo.com", now.Add(24*time.Hour))
|
||||||
foo48Cert, foo48Key, _ := generateKeyPair("foo.com", now.Add(48*time.Hour))
|
foo48Cert, foo48Key, _ := generate.KeyPair("foo.com", now.Add(48*time.Hour))
|
||||||
barCert, barKey, _ := generateKeyPair("bar.com", now)
|
barCert, barKey, _ := generate.KeyPair("bar.com", now)
|
||||||
domainsCertificates := DomainsCertificates{
|
domainsCertificates := DomainsCertificates{
|
||||||
lock: sync.RWMutex{},
|
lock: sync.RWMutex{},
|
||||||
Certs: []*DomainsCertificate{
|
Certs: []*DomainsCertificate{
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -11,6 +19,7 @@ import (
|
||||||
"github.com/containous/traefik/cluster"
|
"github.com/containous/traefik/cluster"
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/log"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
|
"github.com/containous/traefik/tls/generate"
|
||||||
"github.com/xenolf/lego/acme"
|
"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 {
|
func (c *challengeProvider) Present(domain, token, keyAuth string) error {
|
||||||
log.Debugf("Challenge Present %s", domain)
|
log.Debugf("Challenge Present %s", domain)
|
||||||
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
|
cert, _, err := tlsSNI01ChallengeCert(keyAuth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -95,3 +104,47 @@ func (c *challengeProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
func (c *challengeProvider) Timeout() (timeout, interval time.Duration) {
|
func (c *challengeProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
return 60 * time.Second, 5 * time.Second
|
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)
|
||||||
|
}
|
||||||
|
|
133
acme/crypto.go
133
acme/crypto.go
|
@ -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)
|
|
||||||
}
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/containous/traefik/provider/web"
|
"github.com/containous/traefik/provider/web"
|
||||||
"github.com/containous/traefik/provider/zk"
|
"github.com/containous/traefik/provider/zk"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
|
traefikTls "github.com/containous/traefik/tls"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
thoas_stats "github.com/thoas/stats"
|
thoas_stats "github.com/thoas/stats"
|
||||||
)
|
)
|
||||||
|
@ -48,10 +49,10 @@ func TestDo_globalConfiguration(t *testing.T) {
|
||||||
"foo": {
|
"foo": {
|
||||||
Network: "foo Network",
|
Network: "foo Network",
|
||||||
Address: "foo Address",
|
Address: "foo Address",
|
||||||
TLS: &configuration.TLS{
|
TLS: &traefikTls.TLS{
|
||||||
MinVersion: "foo MinVersion",
|
MinVersion: "foo MinVersion",
|
||||||
CipherSuites: []string{"foo CipherSuites 1", "foo CipherSuites 2", "foo CipherSuites 3"},
|
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 1", KeyFile: "KeyFile 1"},
|
||||||
{CertFile: "CertFile 2", KeyFile: "KeyFile 2"},
|
{CertFile: "CertFile 2", KeyFile: "KeyFile 2"},
|
||||||
},
|
},
|
||||||
|
@ -91,10 +92,10 @@ func TestDo_globalConfiguration(t *testing.T) {
|
||||||
"fii": {
|
"fii": {
|
||||||
Network: "fii Network",
|
Network: "fii Network",
|
||||||
Address: "fii Address",
|
Address: "fii Address",
|
||||||
TLS: &configuration.TLS{
|
TLS: &traefikTls.TLS{
|
||||||
MinVersion: "fii MinVersion",
|
MinVersion: "fii MinVersion",
|
||||||
CipherSuites: []string{"fii CipherSuites 1", "fii CipherSuites 2", "fii CipherSuites 3"},
|
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 1", KeyFile: "KeyFile 1"},
|
||||||
{CertFile: "CertFile 2", KeyFile: "KeyFile 2"},
|
{CertFile: "CertFile 2", KeyFile: "KeyFile 2"},
|
||||||
},
|
},
|
||||||
|
@ -178,7 +179,7 @@ func TestDo_globalConfiguration(t *testing.T) {
|
||||||
config.MaxIdleConnsPerHost = 666
|
config.MaxIdleConnsPerHost = 666
|
||||||
config.IdleTimeout = flaeg.Duration(666 * time.Second)
|
config.IdleTimeout = flaeg.Duration(666 * time.Second)
|
||||||
config.InsecureSkipVerify = true
|
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{
|
config.Retry = &configuration.Retry{
|
||||||
Attempts: 666,
|
Attempts: 666,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/containous/traefik/cmd/traefik/anonymize"
|
"github.com/containous/traefik/cmd/traefik/anonymize"
|
||||||
"github.com/containous/traefik/configuration"
|
"github.com/containous/traefik/configuration"
|
||||||
"github.com/containous/traefik/provider/file"
|
"github.com/containous/traefik/provider/file"
|
||||||
|
"github.com/containous/traefik/tls"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ func Test_createBugReport(t *testing.T) {
|
||||||
File: &file.Provider{
|
File: &file.Provider{
|
||||||
Directory: "BAR",
|
Directory: "BAR",
|
||||||
},
|
},
|
||||||
RootCAs: configuration.RootCAs{"fllf"},
|
RootCAs: tls.RootCAs{"fllf"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/server"
|
"github.com/containous/traefik/server"
|
||||||
"github.com/containous/traefik/server/uuid"
|
"github.com/containous/traefik/server/uuid"
|
||||||
|
traefikTls "github.com/containous/traefik/tls"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/containous/traefik/version"
|
"github.com/containous/traefik/version"
|
||||||
"github.com/coreos/go-systemd/daemon"
|
"github.com/coreos/go-systemd/daemon"
|
||||||
|
@ -144,7 +145,7 @@ Complete documentation is available at https://traefik.io`,
|
||||||
//add custom parsers
|
//add custom parsers
|
||||||
f.AddParser(reflect.TypeOf(configuration.EntryPoints{}), &configuration.EntryPoints{})
|
f.AddParser(reflect.TypeOf(configuration.EntryPoints{}), &configuration.EntryPoints{})
|
||||||
f.AddParser(reflect.TypeOf(configuration.DefaultEntryPoints{}), &configuration.DefaultEntryPoints{})
|
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(types.Constraints{}), &types.Constraints{})
|
||||||
f.AddParser(reflect.TypeOf(kubernetes.Namespaces{}), &kubernetes.Namespaces{})
|
f.AddParser(reflect.TypeOf(kubernetes.Namespaces{}), &kubernetes.Namespaces{})
|
||||||
f.AddParser(reflect.TypeOf(ecs.Clusters{}), &ecs.Clusters{})
|
f.AddParser(reflect.TypeOf(ecs.Clusters{}), &ecs.Clusters{})
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
package configuration
|
package configuration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,6 +22,7 @@ import (
|
||||||
"github.com/containous/traefik/provider/rancher"
|
"github.com/containous/traefik/provider/rancher"
|
||||||
"github.com/containous/traefik/provider/web"
|
"github.com/containous/traefik/provider/web"
|
||||||
"github.com/containous/traefik/provider/zk"
|
"github.com/containous/traefik/provider/zk"
|
||||||
|
"github.com/containous/traefik/tls"
|
||||||
"github.com/containous/traefik/types"
|
"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"`
|
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
|
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"`
|
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"`
|
Retry *Retry `description:"Enable retry sending request if network error" export:"true"`
|
||||||
HealthCheck *HealthCheckConfig `description:"Health check parameters" export:"true"`
|
HealthCheck *HealthCheckConfig `description:"Health check parameters" export:"true"`
|
||||||
RespondingTimeouts *RespondingTimeouts `description:"Timeouts for incoming requests to the Traefik instance" 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"
|
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...)
|
// EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...)
|
||||||
type EntryPoints map[string]*EntryPoint
|
type EntryPoints map[string]*EntryPoint
|
||||||
|
|
||||||
|
@ -268,18 +204,18 @@ func (ep *EntryPoints) String() string {
|
||||||
func (ep *EntryPoints) Set(value string) error {
|
func (ep *EntryPoints) Set(value string) error {
|
||||||
result := parseEntryPointsConfiguration(value)
|
result := parseEntryPointsConfiguration(value)
|
||||||
|
|
||||||
var configTLS *TLS
|
var configTLS *tls.TLS
|
||||||
if len(result["tls"]) > 0 {
|
if len(result["tls"]) > 0 {
|
||||||
certs := Certificates{}
|
certs := tls.Certificates{}
|
||||||
if err := certs.Set(result["tls"]); err != nil {
|
if err := certs.Set(result["tls"]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configTLS = &TLS{
|
configTLS = &tls.TLS{
|
||||||
Certificates: certs,
|
Certificates: certs,
|
||||||
}
|
}
|
||||||
} else if len(result["tls_acme"]) > 0 {
|
} else if len(result["tls_acme"]) > 0 {
|
||||||
configTLS = &TLS{
|
configTLS = &tls.TLS{
|
||||||
Certificates: Certificates{},
|
Certificates: tls.Certificates{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(result["ca"]) > 0 {
|
if len(result["ca"]) > 0 {
|
||||||
|
@ -391,7 +327,7 @@ func (ep *EntryPoints) Type() string {
|
||||||
type EntryPoint struct {
|
type EntryPoint struct {
|
||||||
Network string
|
Network string
|
||||||
Address string
|
Address string
|
||||||
TLS *TLS `export:"true"`
|
TLS *tls.TLS `export:"true"`
|
||||||
Redirect *Redirect `export:"true"`
|
Redirect *Redirect `export:"true"`
|
||||||
Auth *types.Auth `export:"true"`
|
Auth *types.Auth `export:"true"`
|
||||||
WhitelistSourceRange []string
|
WhitelistSourceRange []string
|
||||||
|
@ -407,123 +343,6 @@ type Redirect struct {
|
||||||
Replacement string
|
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
|
// Retry contains request retry config
|
||||||
type Retry struct {
|
type Retry struct {
|
||||||
Attempts int `description:"Number of attempts" export:"true"`
|
Attempts int `description:"Number of attempts" export:"true"`
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/containous/flaeg"
|
"github.com/containous/flaeg"
|
||||||
"github.com/containous/traefik/provider"
|
"github.com/containous/traefik/provider"
|
||||||
"github.com/containous/traefik/provider/file"
|
"github.com/containous/traefik/provider/file"
|
||||||
|
"github.com/containous/traefik/tls"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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"},
|
TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"},
|
||||||
},
|
},
|
||||||
WhitelistSourceRange: []string{"Range"},
|
WhitelistSourceRange: []string{"Range"},
|
||||||
TLS: &TLS{
|
TLS: &tls.TLS{
|
||||||
ClientCAFiles: []string{"car"},
|
ClientCAFiles: []string{"car"},
|
||||||
Certificates: Certificates{
|
Certificates: tls.Certificates{
|
||||||
{
|
{
|
||||||
CertFile: FileOrContent("goo"),
|
CertFile: tls.FileOrContent("goo"),
|
||||||
KeyFile: FileOrContent("gii"),
|
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"},
|
TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"},
|
||||||
},
|
},
|
||||||
WhitelistSourceRange: []string{"Range"},
|
WhitelistSourceRange: []string{"Range"},
|
||||||
TLS: &TLS{
|
TLS: &tls.TLS{
|
||||||
ClientCAFiles: []string{"car"},
|
ClientCAFiles: []string{"car"},
|
||||||
Certificates: Certificates{
|
Certificates: tls.Certificates{
|
||||||
{
|
{
|
||||||
CertFile: FileOrContent("goo"),
|
CertFile: tls.FileOrContent("goo"),
|
||||||
KeyFile: FileOrContent("gii"),
|
KeyFile: tls.FileOrContent("gii"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,8 @@ You have three choices:
|
||||||
- [Rules in a Separate File](/configuration/backends/file/#rules-in-a-separate-file)
|
- [Rules in a Separate File](/configuration/backends/file/#rules-in-a-separate-file)
|
||||||
- [Multiple `.toml` Files](/configuration/backends/file/#multiple-toml-files)
|
- [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
|
## Simple
|
||||||
|
|
||||||
Add your configuration at the end of the global configuration file `traefik.toml`:
|
Add your configuration at the end of the global configuration file `traefik.toml`:
|
||||||
|
@ -24,11 +26,8 @@ defaultEntryPoints = ["http", "https"]
|
||||||
address = ":443"
|
address = ":443"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "integration/fixtures/https/snitest.com.cert"
|
certFile = "integration/fixtures/https/snitest.org.cert"
|
||||||
KeyFile = "integration/fixtures/https/snitest.com.key"
|
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]
|
[file]
|
||||||
|
|
||||||
|
@ -81,8 +80,19 @@ defaultEntryPoints = ["http", "https"]
|
||||||
entrypoints = ["http", "https"] # overrides defaultEntryPoints
|
entrypoints = ["http", "https"] # overrides defaultEntryPoints
|
||||||
backend = "backend2"
|
backend = "backend2"
|
||||||
rule = "Path:/test"
|
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
|
## Rules in a Separate File
|
||||||
|
|
||||||
Put your rules in a separate file, for example `rules.toml`:
|
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]
|
[entryPoints.https]
|
||||||
address = ":443"
|
address = ":443"
|
||||||
[entryPoints.https.tls]
|
[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]
|
[file]
|
||||||
filename = "rules.toml"
|
filename = "rules.toml"
|
||||||
|
@ -149,11 +153,23 @@ filename = "rules.toml"
|
||||||
entrypoints = ["http", "https"] # overrides defaultEntryPoints
|
entrypoints = ["http", "https"] # overrides defaultEntryPoints
|
||||||
backend = "backend2"
|
backend = "backend2"
|
||||||
rule = "Path:/test"
|
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
|
## 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
|
```toml
|
||||||
[file]
|
[file]
|
||||||
|
|
|
@ -27,11 +27,11 @@ To redirect an http entrypoint to an https entrypoint (with SNI support).
|
||||||
address = ":443"
|
address = ":443"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "integration/fixtures/https/snitest.com.cert"
|
certFile = "integration/fixtures/https/snitest.com.cert"
|
||||||
KeyFile = "integration/fixtures/https/snitest.com.key"
|
keyFile = "integration/fixtures/https/snitest.com.key"
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "integration/fixtures/https/snitest.org.cert"
|
certFile = "integration/fixtures/https/snitest.org.cert"
|
||||||
KeyFile = "integration/fixtures/https/snitest.org.key"
|
keyFile = "integration/fixtures/https/snitest.org.key"
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
@ -53,6 +53,23 @@ To redirect an entrypoint rewriting the URL.
|
||||||
!!! note
|
!!! 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).
|
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
|
## TLS Mutual Authentication
|
||||||
|
|
||||||
Only accept clients that present a certificate signed by a specified Certificate Authority (CA).
|
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]
|
[entryPoints.https.tls]
|
||||||
ClientCAFiles = ["tests/clientca1.crt", "tests/clientca2.crt"]
|
ClientCAFiles = ["tests/clientca1.crt", "tests/clientca2.crt"]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "integration/fixtures/https/snitest.com.cert"
|
certFile = "integration/fixtures/https/snitest.com.cert"
|
||||||
KeyFile = "integration/fixtures/https/snitest.com.key"
|
keyFile = "integration/fixtures/https/snitest.com.key"
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "integration/fixtures/https/snitest.org.cert"
|
certFile = "integration/fixtures/https/snitest.org.cert"
|
||||||
KeyFile = "integration/fixtures/https/snitest.org.key"
|
keyFile = "integration/fixtures/https/snitest.org.key"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
|
@ -78,7 +78,7 @@ Let's take a look at a simple `traefik.toml` configuration as well before we'll
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
debug = false
|
debug = false
|
||||||
checkNewVersion = true
|
|
||||||
logLevel = "ERROR"
|
logLevel = "ERROR"
|
||||||
defaultEntryPoints = ["https","http"]
|
defaultEntryPoints = ["https","http"]
|
||||||
|
|
||||||
|
|
|
@ -76,13 +76,13 @@ defaultEntryPoints = ["http", "https"]
|
||||||
address = ":443"
|
address = ":443"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "integration/fixtures/https/snitest.com.cert"
|
certFile = "integration/fixtures/https/snitest.com.cert"
|
||||||
KeyFile = "integration/fixtures/https/snitest.com.key"
|
keyFile = "integration/fixtures/https/snitest.com.key"
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = """-----BEGIN CERTIFICATE-----
|
certFile = """-----BEGIN CERTIFICATE-----
|
||||||
<cert file content>
|
<cert file content>
|
||||||
-----END CERTIFICATE-----"""
|
-----END CERTIFICATE-----"""
|
||||||
KeyFile = """-----BEGIN CERTIFICATE-----
|
keyFile = """-----BEGIN CERTIFICATE-----
|
||||||
<key file content>
|
<key file content>
|
||||||
-----END CERTIFICATE-----"""
|
-----END CERTIFICATE-----"""
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,26 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C
|
||||||
s.retrieveAcmeCertificate(c, testCase)
|
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
|
// Doing an HTTPS request and test the response certificate
|
||||||
func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) {
|
func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) {
|
||||||
file := s.adaptFile(c, testCase.traefikConfFilePath, struct {
|
file := s.adaptFile(c, testCase.traefikConfFilePath, struct {
|
||||||
|
|
|
@ -9,8 +9,8 @@ defaultEntryPoints = ["http", "https"]
|
||||||
address = ":5001"
|
address = ":5001"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/acme/ssl/wildcard.crt"
|
certFile = "fixtures/acme/ssl/wildcard.crt"
|
||||||
KeyFile = "fixtures/acme/ssl/wildcard.key"
|
keyFile = "fixtures/acme/ssl/wildcard.key"
|
||||||
|
|
||||||
[acme]
|
[acme]
|
||||||
email = "test@traefik.io"
|
email = "test@traefik.io"
|
||||||
|
|
23
integration/fixtures/acme/acme_provided_dynamic.toml
Normal file
23
integration/fixtures/acme/acme_provided_dynamic.toml
Normal file
|
@ -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
|
16
integration/fixtures/acme/certificates.toml
Normal file
16
integration/fixtures/acme/certificates.toml
Normal file
|
@ -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"
|
|
@ -7,8 +7,8 @@ RootCAs = [ """{{ .CertContent }}""" ]
|
||||||
address = ":4443"
|
address = ":4443"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = """{{ .CertContent }}"""
|
certFile = """{{ .CertContent }}"""
|
||||||
KeyFile = """{{ .KeyContent }}"""
|
keyFile = """{{ .KeyContent }}"""
|
||||||
|
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
|
|
|
@ -7,8 +7,8 @@ InsecureSkipVerify = true
|
||||||
address = ":4443"
|
address = ":4443"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = """{{ .CertContent }}"""
|
certFile = """{{ .CertContent }}"""
|
||||||
KeyFile = """{{ .KeyContent }}"""
|
keyFile = """{{ .KeyContent }}"""
|
||||||
|
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
|
|
|
@ -8,11 +8,11 @@ defaultEntryPoints = ["https"]
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
ClientCAFiles = ["fixtures/https/clientca/ca1.crt"]
|
ClientCAFiles = ["fixtures/https/clientca/ca1.crt"]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.com.cert"
|
certFile = "fixtures/https/snitest.com.cert"
|
||||||
KeyFile = "fixtures/https/snitest.com.key"
|
keyFile = "fixtures/https/snitest.com.key"
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.org.cert"
|
certFile = "fixtures/https/snitest.org.cert"
|
||||||
KeyFile = "fixtures/https/snitest.org.key"
|
keyFile = "fixtures/https/snitest.org.key"
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
address = ":8080"
|
address = ":8080"
|
||||||
|
|
|
@ -8,11 +8,11 @@ defaultEntryPoints = ["https"]
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
ClientCAFiles = ["fixtures/https/clientca/ca1and2.crt"]
|
ClientCAFiles = ["fixtures/https/clientca/ca1and2.crt"]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.com.cert"
|
certFile = "fixtures/https/snitest.com.cert"
|
||||||
KeyFile = "fixtures/https/snitest.com.key"
|
keyFile = "fixtures/https/snitest.com.key"
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.org.cert"
|
certFile = "fixtures/https/snitest.org.cert"
|
||||||
KeyFile = "fixtures/https/snitest.org.key"
|
keyFile = "fixtures/https/snitest.org.key"
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
address = ":8080"
|
address = ":8080"
|
||||||
|
|
|
@ -8,11 +8,11 @@ defaultEntryPoints = ["https"]
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
ClientCAFiles = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"]
|
ClientCAFiles = ["fixtures/https/clientca/ca1.crt", "fixtures/https/clientca/ca2.crt"]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.com.cert"
|
certFile = "fixtures/https/snitest.com.cert"
|
||||||
KeyFile = "fixtures/https/snitest.com.key"
|
keyFile = "fixtures/https/snitest.com.key"
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.org.cert"
|
certFile = "fixtures/https/snitest.org.cert"
|
||||||
KeyFile = "fixtures/https/snitest.org.key"
|
keyFile = "fixtures/https/snitest.org.key"
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
address = ":8080"
|
address = ":8080"
|
||||||
|
|
67
integration/fixtures/https/dynamic_https.toml
Normal file
67
integration/fixtures/https/dynamic_https.toml
Normal file
|
@ -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-----"""
|
16
integration/fixtures/https/dynamic_https_sni.toml
Normal file
16
integration/fixtures/https/dynamic_https_sni.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
logLevel = "DEBUG"
|
||||||
|
|
||||||
|
defaultEntryPoints = ["https"]
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.https]
|
||||||
|
address = ":4443"
|
||||||
|
[entryPoints.https.tls]
|
||||||
|
|
||||||
|
[web]
|
||||||
|
address = ":8080"
|
||||||
|
|
||||||
|
[file]
|
||||||
|
|
||||||
|
fileName = "{{.DynamicConfFileName}}"
|
||||||
|
watch = true
|
|
@ -7,11 +7,11 @@ defaultEntryPoints = ["https"]
|
||||||
address = ":4443"
|
address = ":4443"
|
||||||
[entryPoints.https.tls]
|
[entryPoints.https.tls]
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.com.cert"
|
certFile = "fixtures/https/snitest.com.cert"
|
||||||
KeyFile = "fixtures/https/snitest.com.key"
|
keyFile = "fixtures/https/snitest.com.key"
|
||||||
[[entryPoints.https.tls.certificates]]
|
[[entryPoints.https.tls.certificates]]
|
||||||
CertFile = "fixtures/https/snitest.org.cert"
|
certFile = "fixtures/https/snitest.org.cert"
|
||||||
KeyFile = "fixtures/https/snitest.org.key"
|
keyFile = "fixtures/https/snitest.org.key"
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
address = ":8080"
|
address = ":8080"
|
||||||
|
|
|
@ -8,8 +8,8 @@ InsecureSkipVerify=true
|
||||||
address = ":8000"
|
address = ":8000"
|
||||||
[entryPoints.wss.tls]
|
[entryPoints.wss.tls]
|
||||||
[[entryPoints.wss.tls.certificates]]
|
[[entryPoints.wss.tls.certificates]]
|
||||||
CertFile = "resources/tls/local.cert"
|
certFile = "resources/tls/local.cert"
|
||||||
KeyFile = "resources/tls/local.key"
|
keyFile = "resources/tls/local.key"
|
||||||
|
|
||||||
[web]
|
[web]
|
||||||
address = ":8080"
|
address = ":8080"
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
"github.com/containous/traefik/integration/try"
|
"github.com/containous/traefik/integration/try"
|
||||||
|
traefikTls "github.com/containous/traefik/tls"
|
||||||
|
"github.com/containous/traefik/types"
|
||||||
"github.com/go-check/check"
|
"github.com/go-check/check"
|
||||||
checker "github.com/vdemeester/shakers"
|
checker "github.com/vdemeester/shakers"
|
||||||
)
|
)
|
||||||
|
@ -293,10 +298,10 @@ func (s *HTTPSSuite) TestWithRootCAsContentForHTTPSOnBackend(c *check.C) {
|
||||||
defer cmd.Process.Kill()
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
// wait for Traefik
|
// 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)
|
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)
|
c.Assert(err, checker.IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,10 +320,10 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) {
|
||||||
defer cmd.Process.Kill()
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
// wait for Traefik
|
// 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)
|
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)
|
c.Assert(err, checker.IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,3 +343,203 @@ func startTestServer(port string, statusCode int) (ts *httptest.Server) {
|
||||||
ts.Start()
|
ts.Start()
|
||||||
return ts
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -523,6 +523,11 @@ func (p *Provider) getMaxConnExtractorFunc(container dockerData) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) containerFilter(container dockerData) bool {
|
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
|
var err error
|
||||||
portLabel := "traefik.port label"
|
portLabel := "traefik.port label"
|
||||||
if p.hasServices(container) {
|
if p.hasServices(container) {
|
||||||
|
@ -536,11 +541,6 @@ func (p *Provider) containerFilter(container dockerData) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isContainerEnabled(container, p.ExposedByDefault) {
|
|
||||||
log.Debugf("Filtering disabled container %s", container.Name)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
constraintTags := strings.Split(container.Labels[types.LabelTags], ",")
|
constraintTags := strings.Split(container.Labels[types.LabelTags], ",")
|
||||||
if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok {
|
if ok, failingConstraint := p.MatchConstraints(constraintTags); !ok {
|
||||||
if failingConstraint != nil {
|
if failingConstraint != nil {
|
||||||
|
|
|
@ -3,13 +3,16 @@ package file
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/log"
|
||||||
"github.com/containous/traefik/provider"
|
"github.com/containous/traefik/provider"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
|
"github.com/containous/traefik/tls"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"gopkg.in/fsnotify.v1"
|
"gopkg.in/fsnotify.v1"
|
||||||
)
|
)
|
||||||
|
@ -37,7 +40,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
|
||||||
if p.Directory != "" {
|
if p.Directory != "" {
|
||||||
watchItem = p.Directory
|
watchItem = p.Directory
|
||||||
} else {
|
} else {
|
||||||
watchItem = p.Filename
|
watchItem = filepath.Dir(p.Filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.addWatcher(pool, watchItem, configurationChan, p.watcherCallback); err != nil {
|
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:
|
case <-stop:
|
||||||
return
|
return
|
||||||
case evt := <-watcher.Events:
|
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:
|
case err := <-watcher.Errors:
|
||||||
log.Errorf("Watcher event error: %s", err)
|
log.Errorf("Watcher event error: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -92,28 +103,39 @@ func loadFileConfig(filename string) (*types.Configuration, error) {
|
||||||
return configuration, nil
|
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)
|
fileList, err := ioutil.ReadDir(directory)
|
||||||
|
|
||||||
if err != nil {
|
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{
|
if configuration == nil {
|
||||||
Frontends: make(map[string]*types.Frontend),
|
configuration = &types.Configuration{
|
||||||
Backends: make(map[string]*types.Backend),
|
Frontends: make(map[string]*types.Frontend),
|
||||||
|
Backends: make(map[string]*types.Backend),
|
||||||
|
TLSConfiguration: make([]*tls.Configuration, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range fileList {
|
configTLSMaps := make(map[*tls.Configuration]struct{})
|
||||||
if !strings.HasSuffix(file.Name(), ".toml") {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var c *types.Configuration
|
var c *types.Configuration
|
||||||
c, err = loadFileConfig(path.Join(directory, file.Name()))
|
c, err = loadFileConfig(path.Join(directory, item.Name()))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return configuration, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for backendName, backend := range c.Backends {
|
for backendName, backend := range c.Backends {
|
||||||
|
@ -131,12 +153,33 @@ func loadFileConfigFromDirectory(directory string) (*types.Configuration, error)
|
||||||
configuration.Frontends[frontendName] = frontend
|
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
|
return configuration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) watcherCallback(configurationChan chan<- types.ConfigMessage, event fsnotify.Event) {
|
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()
|
configuration, err := p.loadConfig()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -149,7 +192,7 @@ func (p *Provider) watcherCallback(configurationChan chan<- types.ConfigMessage,
|
||||||
|
|
||||||
func (p *Provider) loadConfig() (*types.Configuration, error) {
|
func (p *Provider) loadConfig() (*types.Configuration, error) {
|
||||||
if p.Directory != "" {
|
if p.Directory != "" {
|
||||||
return loadFileConfigFromDirectory(p.Directory)
|
return loadFileConfigFromDirectory(p.Directory, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadFileConfig(p.Filename)
|
return loadFileConfig(p.Filename)
|
||||||
|
|
|
@ -20,13 +20,15 @@ func TestProvideSingleFileAndWatch(t *testing.T) {
|
||||||
|
|
||||||
expectedNumFrontends := 2
|
expectedNumFrontends := 2
|
||||||
expectedNumBackends := 2
|
expectedNumBackends := 2
|
||||||
|
expectedNumTLSConf := 2
|
||||||
|
|
||||||
tempFile := createFile(t,
|
tempFile := createFile(t,
|
||||||
tempDir, "simple.toml",
|
tempDir, "simple.toml",
|
||||||
createFrontendConfiguration(expectedNumFrontends),
|
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))
|
provide(configurationChan, watch, withFile(tempFile))
|
||||||
|
|
||||||
|
@ -37,14 +39,15 @@ func TestProvideSingleFileAndWatch(t *testing.T) {
|
||||||
// Now test again with single frontend and backend
|
// Now test again with single frontend and backend
|
||||||
expectedNumFrontends = 1
|
expectedNumFrontends = 1
|
||||||
expectedNumBackends = 1
|
expectedNumBackends = 1
|
||||||
|
expectedNumTLSConf = 1
|
||||||
|
|
||||||
createFile(t,
|
createFile(t,
|
||||||
tempDir, "simple.toml",
|
tempDir, "simple.toml",
|
||||||
createFrontendConfiguration(expectedNumFrontends),
|
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, backend, TLS configuration")
|
||||||
err = waitForSignal(signal, 2*time.Second, "single frontend and backend")
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,13 +57,15 @@ func TestProvideSingleFileAndNotWatch(t *testing.T) {
|
||||||
|
|
||||||
expectedNumFrontends := 2
|
expectedNumFrontends := 2
|
||||||
expectedNumBackends := 2
|
expectedNumBackends := 2
|
||||||
|
expectedNumTLSConf := 2
|
||||||
|
|
||||||
tempFile := createFile(t,
|
tempFile := createFile(t,
|
||||||
tempDir, "simple.toml",
|
tempDir, "simple.toml",
|
||||||
createFrontendConfiguration(expectedNumFrontends),
|
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))
|
provide(configurationChan, withFile(tempFile))
|
||||||
|
|
||||||
|
@ -71,14 +76,16 @@ func TestProvideSingleFileAndNotWatch(t *testing.T) {
|
||||||
// Now test again with single frontend and backend
|
// Now test again with single frontend and backend
|
||||||
expectedNumFrontends = 1
|
expectedNumFrontends = 1
|
||||||
expectedNumBackends = 1
|
expectedNumBackends = 1
|
||||||
|
expectedNumTLSConf = 1
|
||||||
|
|
||||||
createFile(t,
|
createFile(t,
|
||||||
tempDir, "simple.toml",
|
tempDir, "simple.toml",
|
||||||
createFrontendConfiguration(expectedNumFrontends),
|
createFrontendConfiguration(expectedNumFrontends),
|
||||||
createBackendConfiguration(expectedNumBackends))
|
createBackendConfiguration(expectedNumBackends),
|
||||||
|
createTLSConfiguration(expectedNumTLSConf))
|
||||||
|
|
||||||
// Must fail because we don't watch the changes
|
// 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)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,11 +95,13 @@ func TestProvideDirectoryAndWatch(t *testing.T) {
|
||||||
|
|
||||||
expectedNumFrontends := 2
|
expectedNumFrontends := 2
|
||||||
expectedNumBackends := 2
|
expectedNumBackends := 2
|
||||||
|
expectedNumTLSConf := 2
|
||||||
|
|
||||||
tempFile1 := createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends))
|
tempFile1 := createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends))
|
||||||
tempFile2 := createRandomFile(t, tempDir, createBackendConfiguration(expectedNumBackends))
|
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))
|
provide(configurationChan, watch, withDirectory(tempDir))
|
||||||
|
|
||||||
|
@ -103,6 +112,7 @@ func TestProvideDirectoryAndWatch(t *testing.T) {
|
||||||
// Now remove the backends file
|
// Now remove the backends file
|
||||||
expectedNumFrontends = 2
|
expectedNumFrontends = 2
|
||||||
expectedNumBackends = 0
|
expectedNumBackends = 0
|
||||||
|
expectedNumTLSConf = 2
|
||||||
os.Remove(tempFile2.Name())
|
os.Remove(tempFile2.Name())
|
||||||
err = waitForSignal(signal, 2*time.Second, "remove the backends file")
|
err = waitForSignal(signal, 2*time.Second, "remove the backends file")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -110,22 +120,34 @@ func TestProvideDirectoryAndWatch(t *testing.T) {
|
||||||
// Now remove the frontends file
|
// Now remove the frontends file
|
||||||
expectedNumFrontends = 0
|
expectedNumFrontends = 0
|
||||||
expectedNumBackends = 0
|
expectedNumBackends = 0
|
||||||
|
expectedNumTLSConf = 2
|
||||||
os.Remove(tempFile1.Name())
|
os.Remove(tempFile1.Name())
|
||||||
err = waitForSignal(signal, 2*time.Second, "remove the frontends file")
|
err = waitForSignal(signal, 2*time.Second, "remove the frontends file")
|
||||||
assert.NoError(t, err)
|
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) {
|
func TestProvideDirectoryAndNotWatch(t *testing.T) {
|
||||||
tempDir := createTempDir(t, "testdir")
|
tempDir := createTempDir(t, "testdir")
|
||||||
|
tempTLSDir := createSubDir(t, tempDir, "tls")
|
||||||
defer os.RemoveAll(tempDir)
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
expectedNumFrontends := 2
|
expectedNumFrontends := 2
|
||||||
expectedNumBackends := 2
|
expectedNumBackends := 2
|
||||||
|
expectedNumTLSConf := 2
|
||||||
|
|
||||||
createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends))
|
createRandomFile(t, tempDir, createFrontendConfiguration(expectedNumFrontends))
|
||||||
tempFile2 := createRandomFile(t, tempDir, createBackendConfiguration(expectedNumBackends))
|
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))
|
provide(configurationChan, withDirectory(tempDir))
|
||||||
|
|
||||||
|
@ -136,6 +158,7 @@ func TestProvideDirectoryAndNotWatch(t *testing.T) {
|
||||||
// Now remove the backends file
|
// Now remove the backends file
|
||||||
expectedNumFrontends = 2
|
expectedNumFrontends = 2
|
||||||
expectedNumBackends = 0
|
expectedNumBackends = 0
|
||||||
|
expectedNumTLSConf = 2
|
||||||
os.Remove(tempFile2.Name())
|
os.Remove(tempFile2.Name())
|
||||||
|
|
||||||
// Must fail because we don't watch the changes
|
// 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)
|
configurationChan := make(chan types.ConfigMessage)
|
||||||
signal := make(chan interface{})
|
signal := make(chan interface{})
|
||||||
|
|
||||||
|
@ -154,6 +177,7 @@ func createConfigurationRoutine(t *testing.T, expectedNumFrontends *int, expecte
|
||||||
assert.Equal(t, "file", data.ProviderName)
|
assert.Equal(t, "file", data.ProviderName)
|
||||||
assert.Len(t, data.Configuration.Frontends, *expectedNumFrontends)
|
assert.Len(t, data.Configuration.Frontends, *expectedNumFrontends)
|
||||||
assert.Len(t, data.Configuration.Backends, *expectedNumBackends)
|
assert.Len(t, data.Configuration.Backends, *expectedNumBackends)
|
||||||
|
assert.Len(t, data.Configuration.TLSConfiguration, *expectedNumTLSConfigurations)
|
||||||
signal <- nil
|
signal <- nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -207,6 +231,7 @@ func createRandomFile(t *testing.T, tempDir string, contents ...string) *os.File
|
||||||
|
|
||||||
// createFile Helper
|
// createFile Helper
|
||||||
func createFile(t *testing.T, tempDir string, name string, contents ...string) *os.File {
|
func createFile(t *testing.T, tempDir string, name string, contents ...string) *os.File {
|
||||||
|
t.Helper()
|
||||||
fileName := path.Join(tempDir, name)
|
fileName := path.Join(tempDir, name)
|
||||||
|
|
||||||
tempFile, err := os.Create(fileName)
|
tempFile, err := os.Create(fileName)
|
||||||
|
@ -231,6 +256,7 @@ func createFile(t *testing.T, tempDir string, name string, contents ...string) *
|
||||||
|
|
||||||
// createTempDir Helper
|
// createTempDir Helper
|
||||||
func createTempDir(t *testing.T, dir string) string {
|
func createTempDir(t *testing.T, dir string) string {
|
||||||
|
t.Helper()
|
||||||
d, err := ioutil.TempDir("", dir)
|
d, err := ioutil.TempDir("", dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -238,6 +264,16 @@ func createTempDir(t *testing.T, dir string) string {
|
||||||
return d
|
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
|
// createFrontendConfiguration Helper
|
||||||
func createFrontendConfiguration(n int) string {
|
func createFrontendConfiguration(n int) string {
|
||||||
conf := "[frontends]\n"
|
conf := "[frontends]\n"
|
||||||
|
@ -260,3 +296,17 @@ func createBackendConfiguration(n int) string {
|
||||||
}
|
}
|
||||||
return conf
|
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
|
||||||
|
}
|
||||||
|
|
213
server/server.go
213
server/server.go
|
@ -33,6 +33,7 @@ import (
|
||||||
"github.com/containous/traefik/provider"
|
"github.com/containous/traefik/provider"
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/server/cookie"
|
"github.com/containous/traefik/server/cookie"
|
||||||
|
traefikTls "github.com/containous/traefik/tls"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/containous/traefik/whitelist"
|
"github.com/containous/traefik/whitelist"
|
||||||
"github.com/streamrail/concurrent-map"
|
"github.com/streamrail/concurrent-map"
|
||||||
|
@ -66,6 +67,8 @@ type Server struct {
|
||||||
leadership *cluster.Leadership
|
leadership *cluster.Leadership
|
||||||
defaultForwardingRoundTripper http.RoundTripper
|
defaultForwardingRoundTripper http.RoundTripper
|
||||||
metricsRegistry metrics.Registry
|
metricsRegistry metrics.Registry
|
||||||
|
lastReceivedConfiguration *safe.Safe
|
||||||
|
lastConfigs cmap.ConcurrentMap
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverEntryPoints map[string]*serverEntryPoint
|
type serverEntryPoints map[string]*serverEntryPoint
|
||||||
|
@ -74,6 +77,7 @@ type serverEntryPoint struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
httpRouter *middlewares.HandlerSwitcher
|
httpRouter *middlewares.HandlerSwitcher
|
||||||
|
certs safe.Safe
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverRoute struct {
|
type serverRoute struct {
|
||||||
|
@ -101,6 +105,8 @@ func NewServer(globalConfiguration configuration.GlobalConfiguration) *Server {
|
||||||
server.globalConfiguration = globalConfiguration
|
server.globalConfiguration = globalConfiguration
|
||||||
server.routinesPool = safe.NewPool(context.Background())
|
server.routinesPool = safe.NewPool(context.Background())
|
||||||
server.defaultForwardingRoundTripper = createHTTPTransport(globalConfiguration)
|
server.defaultForwardingRoundTripper = createHTTPTransport(globalConfiguration)
|
||||||
|
server.lastReceivedConfiguration = safe.New(time.Unix(0, 0))
|
||||||
|
server.lastConfigs = cmap.New()
|
||||||
|
|
||||||
server.metricsRegistry = metrics.NewVoidRegistry()
|
server.metricsRegistry = metrics.NewVoidRegistry()
|
||||||
if globalConfiguration.Web != nil && globalConfiguration.Web.Metrics != nil {
|
if globalConfiguration.Web != nil && globalConfiguration.Web.Metrics != nil {
|
||||||
|
@ -165,7 +171,7 @@ func createHTTPTransport(globalConfiguration configuration.GlobalConfiguration)
|
||||||
return transport
|
return transport
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRootCACertPool(rootCAs configuration.RootCAs) *x509.CertPool {
|
func createRootCACertPool(rootCAs traefikTls.RootCAs) *x509.CertPool {
|
||||||
roots := x509.NewCertPool()
|
roots := x509.NewCertPool()
|
||||||
|
|
||||||
for _, cert := range rootCAs {
|
for _, cert := range rootCAs {
|
||||||
|
@ -317,51 +323,54 @@ func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) listenProviders(stop chan bool) {
|
func (server *Server) listenProviders(stop chan bool) {
|
||||||
lastReceivedConfiguration := safe.New(time.Unix(0, 0))
|
|
||||||
lastConfigs := cmap.New()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-stop:
|
case <-stop:
|
||||||
return
|
return
|
||||||
case configMsg, ok := <-server.configurationChan:
|
case configMsg, ok := <-server.configurationChan:
|
||||||
if !ok {
|
if !ok || configMsg.Configuration == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
server.defaultConfigurationValues(configMsg.Configuration)
|
server.preLoadConfiguration(configMsg)
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (server *Server) defaultConfigurationValues(configuration *types.Configuration) {
|
||||||
if configuration == nil || configuration.Frontends == nil {
|
if configuration == nil || configuration.Frontends == nil {
|
||||||
return
|
return
|
||||||
|
@ -376,34 +385,73 @@ func (server *Server) listenConfigurations(stop chan bool) {
|
||||||
case <-stop:
|
case <-stop:
|
||||||
return
|
return
|
||||||
case configMsg, ok := <-server.configurationValidatedChan:
|
case configMsg, ok := <-server.configurationValidatedChan:
|
||||||
if !ok {
|
if !ok || configMsg.Configuration == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentConfigurations := server.currentConfigurations.Get().(types.Configurations)
|
server.loadConfiguration(configMsg)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if server.globalConfiguration.ACME == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -418,8 +466,8 @@ func (server *Server) postLoadConfig() {
|
||||||
// check if one of the frontend entrypoints is configured with TLS
|
// check if one of the frontend entrypoints is configured with TLS
|
||||||
// and is configured with ACME
|
// and is configured with ACME
|
||||||
ACMEEnabled := false
|
ACMEEnabled := false
|
||||||
for _, entrypoint := range frontend.EntryPoints {
|
for _, entryPoint := range frontend.EntryPoints {
|
||||||
if server.globalConfiguration.ACME.EntryPoint == entrypoint && server.globalConfiguration.EntryPoints[entrypoint].TLS != nil {
|
if server.globalConfiguration.ACME.EntryPoint == entryPoint && server.globalConfiguration.EntryPoints[entryPoint].TLS != nil {
|
||||||
ACMEEnabled = true
|
ACMEEnabled = true
|
||||||
break
|
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 {
|
if tlsOption == nil {
|
||||||
return nil, errors.New("no TLS provided")
|
return nil, errors.New("no TLS provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := tlsOption.Certificates.CreateTLSConfig()
|
config, _, err := tlsOption.Certificates.CreateTLSConfig(entryPointName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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 {
|
if tlsOption == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := tlsOption.Certificates.CreateTLSConfig()
|
config, epDomainsCertificates, err := tlsOption.Certificates.CreateTLSConfig(entryPointName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// ensure http2 enabled
|
||||||
config.NextProtos = []string{"h2", "http/1.1"}
|
config.NextProtos = []string{"h2", "http/1.1"}
|
||||||
|
|
||||||
|
@ -578,12 +632,12 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if server.leadership == nil {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -592,6 +646,8 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *configur
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + " for ACME configuration")
|
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 {
|
if len(config.Certificates) == 0 {
|
||||||
return nil, errors.New("No certificates found for TLS entrypoint " + entryPointName)
|
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.
|
// in each certificate and populates the config.NameToCertificate map.
|
||||||
config.BuildNameToCertificate()
|
config.BuildNameToCertificate()
|
||||||
//Set the minimum TLS version if set in the config TOML
|
//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.PreferServerCipherSuites = true
|
||||||
config.MinVersion = minConst
|
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
|
//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)
|
config.CipherSuites = make([]uint16, 0)
|
||||||
for _, cipher := range server.globalConfiguration.EntryPoints[entryPointName].TLS.CipherSuites {
|
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)
|
config.CipherSuites = append(config.CipherSuites, cipherConst)
|
||||||
} else {
|
} else {
|
||||||
//CipherSuite listed in the toml does not exist in our listed
|
//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
|
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
|
// 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.
|
// 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 {
|
if passTLSCert {
|
||||||
tlsConfig, err := createClientTLSConfig(tls)
|
tlsConfig, err := createClientTLSConfig(entryPointName, tls)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to create TLSClientConfig: %s", err)
|
log.Errorf("Failed to create TLSClientConfig: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -802,7 +857,7 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf
|
||||||
if backends[entryPointName+frontend.Backend] == nil {
|
if backends[entryPointName+frontend.Backend] == nil {
|
||||||
log.Debugf("Creating backend %s", frontend.Backend)
|
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 {
|
if err != nil {
|
||||||
log.Errorf("Failed to create RoundTripper for frontend %s: %v", frontendName, err)
|
log.Errorf("Failed to create RoundTripper for frontend %s: %v", frontendName, err)
|
||||||
log.Errorf("Skipping frontend %s...", frontendName)
|
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)
|
healthcheck.GetHealthCheck().SetBackendsConfiguration(server.routinesPool.Ctx(), backendsHealthCheck)
|
||||||
//sort routes
|
// Get new certificates list sorted per entrypoints
|
||||||
for _, serverEntryPoint := range serverEntryPoints {
|
// Update certificates
|
||||||
|
entryPointsCertificates, err := server.loadHTTPSConfiguration(configurations)
|
||||||
|
//sort routes and update certificates
|
||||||
|
for serverEntryPointName, serverEntryPoint := range serverEntryPoints {
|
||||||
serverEntryPoint.httpRouter.GetHandler().SortRoutes()
|
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 {
|
func configureLBServers(lb healthcheck.LoadBalancer, config *types.Configuration, frontend *types.Frontend) error {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/containous/traefik/metrics"
|
"github.com/containous/traefik/metrics"
|
||||||
"github.com/containous/traefik/middlewares"
|
"github.com/containous/traefik/middlewares"
|
||||||
"github.com/containous/traefik/testhelpers"
|
"github.com/containous/traefik/testhelpers"
|
||||||
|
"github.com/containous/traefik/tls"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -24,6 +25,44 @@ import (
|
||||||
"github.com/vulcand/oxy/roundrobin"
|
"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{}
|
type testLoadBalancer struct{}
|
||||||
|
|
||||||
func (lb *testLoadBalancer) RemoveServer(u *url.URL) error {
|
func (lb *testLoadBalancer) RemoveServer(u *url.URL) error {
|
||||||
|
@ -241,6 +280,15 @@ func TestServerLoadConfigHealthCheckOptions(t *testing.T) {
|
||||||
HealthCheck: healthCheck,
|
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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
206
tls/certificate.go
Normal file
206
tls/certificate.go
Normal file
|
@ -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"
|
||||||
|
}
|
91
tls/generate/generate.go
Normal file
91
tls/generate/generate.go
Normal file
|
@ -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)
|
||||||
|
}
|
94
tls/tls.go
Normal file
94
tls/tls.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/containous/flaeg"
|
"github.com/containous/flaeg"
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/log"
|
||||||
|
traefikTls "github.com/containous/traefik/tls"
|
||||||
"github.com/docker/libkv/store"
|
"github.com/docker/libkv/store"
|
||||||
"github.com/ryanuber/go-glob"
|
"github.com/ryanuber/go-glob"
|
||||||
)
|
)
|
||||||
|
@ -188,8 +189,9 @@ type Configurations map[string]*Configuration
|
||||||
|
|
||||||
// Configuration of a provider.
|
// Configuration of a provider.
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Backends map[string]*Backend `json:"backends,omitempty"`
|
Backends map[string]*Backend `json:"backends,omitempty"`
|
||||||
Frontends map[string]*Frontend `json:"frontends,omitempty"`
|
Frontends map[string]*Frontend `json:"frontends,omitempty"`
|
||||||
|
TLSConfiguration []*traefikTls.Configuration `json:"tlsConfiguration,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigMessage hold configuration information exchanged between parts of traefik.
|
// ConfigMessage hold configuration information exchanged between parts of traefik.
|
||||||
|
|
Loading…
Reference in a new issue