216 lines
6 KiB
Go
216 lines
6 KiB
Go
|
package tls
|
||
|
|
||
|
import (
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"fmt"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/containous/traefik/log"
|
||
|
"github.com/containous/traefik/tls/generate"
|
||
|
"github.com/containous/traefik/types"
|
||
|
"github.com/sirupsen/logrus"
|
||
|
"github.com/xenolf/lego/challenge/tlsalpn01"
|
||
|
)
|
||
|
|
||
|
// Manager is the TLS option/store/configuration factory
|
||
|
type Manager struct {
|
||
|
storesConfig map[string]Store
|
||
|
stores map[string]*CertificateStore
|
||
|
configs map[string]TLS
|
||
|
certs []*Configuration
|
||
|
TLSAlpnGetter func(string) (*tls.Certificate, error)
|
||
|
lock sync.RWMutex
|
||
|
}
|
||
|
|
||
|
// NewManager creates a new Manager
|
||
|
func NewManager() *Manager {
|
||
|
return &Manager{}
|
||
|
}
|
||
|
|
||
|
// UpdateConfigs updates the TLS* configuration options
|
||
|
func (m *Manager) UpdateConfigs(stores map[string]Store, configs map[string]TLS, certs []*Configuration) {
|
||
|
m.lock.Lock()
|
||
|
defer m.lock.Unlock()
|
||
|
|
||
|
m.configs = configs
|
||
|
m.storesConfig = stores
|
||
|
m.certs = certs
|
||
|
|
||
|
m.stores = make(map[string]*CertificateStore)
|
||
|
for storeName, storeConfig := range m.storesConfig {
|
||
|
var err error
|
||
|
m.stores[storeName], err = buildCertificateStore(storeConfig)
|
||
|
if err != nil {
|
||
|
log.Errorf("Error while creating certificate store %s", storeName)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
storesCertificates := make(map[string]map[string]*tls.Certificate)
|
||
|
for _, conf := range certs {
|
||
|
if len(conf.Stores) == 0 {
|
||
|
if log.GetLevel() >= logrus.DebugLevel {
|
||
|
log.Debugf("No store is defined to add the certificate %s, it will be added to the default store.",
|
||
|
conf.Certificate.GetTruncatedCertificateName())
|
||
|
}
|
||
|
conf.Stores = []string{"default"}
|
||
|
}
|
||
|
for _, store := range conf.Stores {
|
||
|
if err := conf.Certificate.AppendCertificates(storesCertificates, store); err != nil {
|
||
|
log.Errorf("Unable to append certificate %s to store %s: %v", conf.Certificate.GetTruncatedCertificateName(), store, err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for storeName, certs := range storesCertificates {
|
||
|
m.getStore(storeName).DynamicCerts.Set(certs)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Get gets the tls configuration to use for a given store / configuration
|
||
|
func (m *Manager) Get(storeName string, configName string) *tls.Config {
|
||
|
m.lock.RLock()
|
||
|
defer m.lock.RUnlock()
|
||
|
|
||
|
store := m.getStore(storeName)
|
||
|
|
||
|
tlsConfig, err := buildTLSConfig(m.configs[configName])
|
||
|
if err != nil {
|
||
|
log.Error(err)
|
||
|
tlsConfig = &tls.Config{}
|
||
|
}
|
||
|
|
||
|
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||
|
domainToCheck := types.CanonicalDomain(clientHello.ServerName)
|
||
|
|
||
|
if m.TLSAlpnGetter != nil {
|
||
|
cert, err := m.TLSAlpnGetter(domainToCheck)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if cert != nil {
|
||
|
return cert, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
bestCertificate := store.GetBestCertificate(clientHello)
|
||
|
if bestCertificate != nil {
|
||
|
return bestCertificate, nil
|
||
|
}
|
||
|
|
||
|
if m.configs[configName].SniStrict {
|
||
|
return nil, fmt.Errorf("strict SNI enabled - No certificate found for domain: %q, closing connection", domainToCheck)
|
||
|
}
|
||
|
|
||
|
log.WithoutContext().Debugf("Serving default certificate for request: %q", domainToCheck)
|
||
|
return store.DefaultCertificate, nil
|
||
|
}
|
||
|
return tlsConfig
|
||
|
}
|
||
|
|
||
|
func (m *Manager) getStore(storeName string) *CertificateStore {
|
||
|
_, ok := m.stores[storeName]
|
||
|
if !ok {
|
||
|
m.stores[storeName], _ = buildCertificateStore(Store{})
|
||
|
}
|
||
|
return m.stores[storeName]
|
||
|
}
|
||
|
|
||
|
// GetStore gets the certificate store of a given name
|
||
|
func (m *Manager) GetStore(storeName string) *CertificateStore {
|
||
|
m.lock.RLock()
|
||
|
defer m.lock.RUnlock()
|
||
|
|
||
|
return m.getStore(storeName)
|
||
|
}
|
||
|
|
||
|
func buildCertificateStore(tlsStore Store) (*CertificateStore, error) {
|
||
|
certificateStore := NewCertificateStore()
|
||
|
certificateStore.DynamicCerts.Set(make(map[string]*tls.Certificate))
|
||
|
|
||
|
if tlsStore.DefaultCertificate != nil {
|
||
|
cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
certificateStore.DefaultCertificate = cert
|
||
|
} else {
|
||
|
log.Debug("No default certificate, generate one")
|
||
|
cert, err := generate.DefaultCertificate()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
certificateStore.DefaultCertificate = cert
|
||
|
}
|
||
|
return certificateStore, nil
|
||
|
}
|
||
|
|
||
|
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI
|
||
|
func buildTLSConfig(tlsOption TLS) (*tls.Config, error) {
|
||
|
conf := &tls.Config{}
|
||
|
|
||
|
// ensure http2 enabled
|
||
|
conf.NextProtos = []string{"h2", "http/1.1", tlsalpn01.ACMETLS1Protocol}
|
||
|
|
||
|
if len(tlsOption.ClientCA.Files) > 0 {
|
||
|
pool := x509.NewCertPool()
|
||
|
for _, caFile := range tlsOption.ClientCA.Files {
|
||
|
data, err := caFile.Read()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
ok := pool.AppendCertsFromPEM(data)
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("invalid certificate(s) in %s", caFile)
|
||
|
}
|
||
|
}
|
||
|
conf.ClientCAs = pool
|
||
|
if tlsOption.ClientCA.Optional {
|
||
|
conf.ClientAuth = tls.VerifyClientCertIfGiven
|
||
|
} else {
|
||
|
conf.ClientAuth = tls.RequireAndVerifyClientCert
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Set the minimum TLS version if set in the config TOML
|
||
|
if minConst, exists := MinVersion[tlsOption.MinVersion]; exists {
|
||
|
conf.PreferServerCipherSuites = true
|
||
|
conf.MinVersion = minConst
|
||
|
}
|
||
|
|
||
|
// Set the list of CipherSuites if set in the config TOML
|
||
|
if tlsOption.CipherSuites != nil {
|
||
|
// if our list of CipherSuites is defined in the entryPoint config, we can re-initialize the suites list as empty
|
||
|
conf.CipherSuites = make([]uint16, 0)
|
||
|
for _, cipher := range tlsOption.CipherSuites {
|
||
|
if cipherConst, exists := CipherSuites[cipher]; exists {
|
||
|
conf.CipherSuites = append(conf.CipherSuites, cipherConst)
|
||
|
} else {
|
||
|
// CipherSuite listed in the toml does not exist in our listed
|
||
|
return nil, fmt.Errorf("invalid CipherSuite: %s", cipher)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return conf, nil
|
||
|
}
|
||
|
|
||
|
func buildDefaultCertificate(defaultCertificate *Certificate) (*tls.Certificate, error) {
|
||
|
certFile, err := defaultCertificate.CertFile.Read()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to get cert file content: %v", err)
|
||
|
}
|
||
|
|
||
|
keyFile, err := defaultCertificate.KeyFile.Read()
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to get key file content: %v", err)
|
||
|
}
|
||
|
|
||
|
cert, err := tls.X509KeyPair(certFile, keyFile)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to load X509 key pair: %v", err)
|
||
|
}
|
||
|
return &cert, nil
|
||
|
}
|