Add a new protocol
Co-authored-by: Gérald Croës <gerald@containo.us>
This commit is contained in:
parent
0ca2149408
commit
4a68d29ce2
231 changed files with 6895 additions and 4395 deletions
|
@ -201,7 +201,8 @@ func (c *Certificate) AppendCertificates(certs map[string]map[string]*tls.Certif
|
|||
return err
|
||||
}
|
||||
|
||||
func (c *Certificate) getTruncatedCertificateName() string {
|
||||
// GetTruncatedCertificateName truncates the certificate name
|
||||
func (c *Certificate) GetTruncatedCertificateName() string {
|
||||
certName := c.CertFile.String()
|
||||
|
||||
// Truncate certificate information only if it's a well formed certificate content with more than 50 characters
|
||||
|
|
|
@ -18,7 +18,6 @@ type CertificateStore struct {
|
|||
DynamicCerts *safe.Safe
|
||||
DefaultCertificate *tls.Certificate
|
||||
CertCache *cache.Cache
|
||||
SniStrict bool
|
||||
}
|
||||
|
||||
// NewCertificateStore create a store for dynamic and static certificates
|
||||
|
|
44
tls/tls.go
44
tls/tls.go
|
@ -1,17 +1,11 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
certificateHeader = "-----BEGIN CERTIFICATE-----\n"
|
||||
)
|
||||
const certificateHeader = "-----BEGIN CERTIFICATE-----\n"
|
||||
|
||||
// ClientCA defines traefik CA files for a entryPoint
|
||||
// and it indicates if they are mandatory or have just to be analyzed if provided
|
||||
|
@ -22,11 +16,15 @@ type ClientCA struct {
|
|||
|
||||
// TLS configures TLS for an entry point
|
||||
type TLS struct {
|
||||
MinVersion string `export:"true"`
|
||||
CipherSuites []string
|
||||
ClientCA ClientCA
|
||||
MinVersion string `export:"true"`
|
||||
CipherSuites []string
|
||||
ClientCA ClientCA
|
||||
SniStrict bool `export:"true"`
|
||||
}
|
||||
|
||||
// Store holds the options for a given Store
|
||||
type Store struct {
|
||||
DefaultCertificate *Certificate
|
||||
SniStrict bool `export:"true"`
|
||||
}
|
||||
|
||||
// FilesOrContents hold the CA we want to have in root
|
||||
|
@ -34,7 +32,7 @@ type FilesOrContents []FileOrContent
|
|||
|
||||
// Configuration allows mapping a TLS certificate to a list of entrypoints
|
||||
type Configuration struct {
|
||||
EntryPoints []string
|
||||
Stores []string
|
||||
Certificate *Certificate
|
||||
}
|
||||
|
||||
|
@ -76,25 +74,3 @@ func (r *FilesOrContents) SetValue(val interface{}) {
|
|||
func (r *FilesOrContents) Type() string {
|
||||
return "filesorcontents"
|
||||
}
|
||||
|
||||
// SortTLSPerEntryPoints converts TLS configuration sorted by Certificates into TLS configuration sorted by EntryPoints
|
||||
func SortTLSPerEntryPoints(configurations []*Configuration, epConfiguration map[string]map[string]*tls.Certificate, defaultEntryPoints []string) {
|
||||
if epConfiguration == nil {
|
||||
epConfiguration = make(map[string]map[string]*tls.Certificate)
|
||||
}
|
||||
for _, conf := range configurations {
|
||||
if conf.EntryPoints == nil || len(conf.EntryPoints) == 0 {
|
||||
if log.GetLevel() >= logrus.DebugLevel {
|
||||
log.Debugf("No entryPoint is defined to add the certificate %s, it will be added to the default entryPoints: %s",
|
||||
conf.Certificate.getTruncatedCertificateName(),
|
||||
strings.Join(defaultEntryPoints, ", "))
|
||||
}
|
||||
conf.EntryPoints = append(conf.EntryPoints, defaultEntryPoints...)
|
||||
}
|
||||
for _, ep := range conf.EntryPoints {
|
||||
if err := conf.Certificate.AppendCertificates(epConfiguration, ep); err != nil {
|
||||
log.Errorf("Unable to append certificate %s to entrypoint %s: %v", conf.Certificate.getTruncatedCertificateName(), ep, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
215
tls/tlsmanager.go
Normal file
215
tls/tlsmanager.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
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
|
||||
}
|
64
tls/tlsmanager_test.go
Normal file
64
tls/tlsmanager_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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 = 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 = 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-----`)
|
||||
)
|
||||
|
||||
func TestTLSInStore(t *testing.T) {
|
||||
dynamicConfigs :=
|
||||
[]*Configuration{
|
||||
{
|
||||
Certificate: &Certificate{
|
||||
CertFile: localhostCert,
|
||||
KeyFile: localhostKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tlsManager := NewManager()
|
||||
tlsManager.UpdateConfigs(nil, nil, dynamicConfigs)
|
||||
|
||||
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*tls.Certificate)
|
||||
if len(certs) == 0 {
|
||||
t.Fatal("got error: default store must have TLS certificates.")
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue