Move code to pkg
This commit is contained in:
parent
bd4c822670
commit
f1b085fa36
465 changed files with 656 additions and 680 deletions
250
pkg/tls/certificate.go
Normal file
250
pkg/tls/certificate.go
Normal file
|
@ -0,0 +1,250 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/traefik/pkg/log"
|
||||
"github.com/containous/traefik/pkg/tls/generate"
|
||||
)
|
||||
|
||||
var (
|
||||
// MinVersion Map of allowed TLS minimum versions
|
||||
MinVersion = map[string]uint16{
|
||||
`VersionTLS10`: tls.VersionTLS10,
|
||||
`VersionTLS11`: tls.VersionTLS11,
|
||||
`VersionTLS12`: tls.VersionTLS12,
|
||||
`VersionTLS13`: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
// 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,
|
||||
"TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
|
||||
"TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
|
||||
"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
"TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
|
||||
}
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// IsPath returns true if the FileOrContent is a file path, otherwise returns false
|
||||
func (f FileOrContent) IsPath() bool {
|
||||
_, err := os.Stat(f.String())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
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, error) {
|
||||
config := &tls.Config{}
|
||||
domainsCertificates := make(map[string]map[string]*tls.Certificate)
|
||||
|
||||
if c.isEmpty() {
|
||||
config.Certificates = []tls.Certificate{}
|
||||
|
||||
cert, err := generate.DefaultCertificate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.Certificates = append(config.Certificates, *cert)
|
||||
} else {
|
||||
for _, certificate := range *c {
|
||||
err := certificate.AppendCertificates(domainsCertificates, entryPointName)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to add a certificate to the entryPoint %q : %v", entryPointName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, certDom := range domainsCertificates {
|
||||
for _, cert := range certDom {
|
||||
config.Certificates = append(config.Certificates, *cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return config, 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]map[string]*tls.Certificate, ep string) error {
|
||||
|
||||
certContent, err := c.CertFile.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read CertFile : %v", err)
|
||||
}
|
||||
|
||||
keyContent, err := c.KeyFile.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read KeyFile : %v", err)
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(certContent, keyContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate TLS certificate : %v", err)
|
||||
}
|
||||
|
||||
parsedCert, _ := x509.ParseCertificate(tlsCert.Certificate[0])
|
||||
|
||||
var SANs []string
|
||||
if parsedCert.Subject.CommonName != "" {
|
||||
SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName))
|
||||
}
|
||||
if parsedCert.DNSNames != nil {
|
||||
sort.Strings(parsedCert.DNSNames)
|
||||
for _, dnsName := range parsedCert.DNSNames {
|
||||
if dnsName != parsedCert.Subject.CommonName {
|
||||
SANs = append(SANs, strings.ToLower(dnsName))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if parsedCert.IPAddresses != nil {
|
||||
for _, ip := range parsedCert.IPAddresses {
|
||||
if ip.String() != parsedCert.Subject.CommonName {
|
||||
SANs = append(SANs, strings.ToLower(ip.String()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
certKey := strings.Join(SANs, ",")
|
||||
|
||||
certExists := false
|
||||
if certs[ep] == nil {
|
||||
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 this certificate (%s). The new certificate will not be append to the EntryPoint.", ep, certKey)
|
||||
} else {
|
||||
log.Debugf("Add certificate for domains %s", certKey)
|
||||
certs[ep][certKey] = &tlsCert
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
if !c.CertFile.IsPath() && strings.HasPrefix(certName, certificateHeader) && len(certName) > len(certificateHeader)+50 {
|
||||
certName = strings.TrimPrefix(c.CertFile.String(), certificateHeader)[:50]
|
||||
}
|
||||
|
||||
return certName
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
140
pkg/tls/certificate_store.go
Normal file
140
pkg/tls/certificate_store.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/pkg/log"
|
||||
"github.com/containous/traefik/pkg/safe"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
// CertificateStore store for dynamic and static certificates
|
||||
type CertificateStore struct {
|
||||
DynamicCerts *safe.Safe
|
||||
DefaultCertificate *tls.Certificate
|
||||
CertCache *cache.Cache
|
||||
}
|
||||
|
||||
// NewCertificateStore create a store for dynamic and static certificates
|
||||
func NewCertificateStore() *CertificateStore {
|
||||
return &CertificateStore{
|
||||
DynamicCerts: &safe.Safe{},
|
||||
CertCache: cache.New(1*time.Hour, 10*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func (c CertificateStore) getDefaultCertificateDomains() []string {
|
||||
var allCerts []string
|
||||
|
||||
if c.DefaultCertificate == nil {
|
||||
return allCerts
|
||||
}
|
||||
|
||||
x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0])
|
||||
if err != nil {
|
||||
log.WithoutContext().Errorf("Could not parse default certicate: %v", err)
|
||||
return allCerts
|
||||
}
|
||||
|
||||
if len(x509Cert.Subject.CommonName) > 0 {
|
||||
allCerts = append(allCerts, x509Cert.Subject.CommonName)
|
||||
}
|
||||
|
||||
allCerts = append(allCerts, x509Cert.DNSNames...)
|
||||
|
||||
for _, ipSan := range x509Cert.IPAddresses {
|
||||
allCerts = append(allCerts, ipSan.String())
|
||||
}
|
||||
|
||||
return allCerts
|
||||
}
|
||||
|
||||
// GetAllDomains return a slice with all the certificate domain
|
||||
func (c CertificateStore) GetAllDomains() []string {
|
||||
|
||||
allCerts := c.getDefaultCertificateDomains()
|
||||
|
||||
// Get dynamic certificates
|
||||
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
|
||||
for domains := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||
allCerts = append(allCerts, domains)
|
||||
}
|
||||
}
|
||||
return allCerts
|
||||
}
|
||||
|
||||
// GetBestCertificate returns the best match certificate, and caches the response
|
||||
func (c CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *tls.Certificate {
|
||||
domainToCheck := strings.ToLower(strings.TrimSpace(clientHello.ServerName))
|
||||
if len(domainToCheck) == 0 {
|
||||
// If no ServerName is provided, Check for local IP address matches
|
||||
host, _, err := net.SplitHostPort(clientHello.Conn.LocalAddr().String())
|
||||
if err != nil {
|
||||
log.Debugf("Could not split host/port: %v", err)
|
||||
}
|
||||
domainToCheck = strings.TrimSpace(host)
|
||||
}
|
||||
|
||||
if cert, ok := c.CertCache.Get(domainToCheck); ok {
|
||||
return cert.(*tls.Certificate)
|
||||
}
|
||||
|
||||
matchedCerts := map[string]*tls.Certificate{}
|
||||
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
|
||||
for domains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||
for _, certDomain := range strings.Split(domains, ",") {
|
||||
if MatchDomain(domainToCheck, certDomain) {
|
||||
matchedCerts[certDomain] = cert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchedCerts) > 0 {
|
||||
// sort map by keys
|
||||
keys := make([]string, 0, len(matchedCerts))
|
||||
for k := range matchedCerts {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// cache best match
|
||||
c.CertCache.SetDefault(domainToCheck, matchedCerts[keys[len(keys)-1]])
|
||||
return matchedCerts[keys[len(keys)-1]]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetCache clears the cache in the store
|
||||
func (c CertificateStore) ResetCache() {
|
||||
if c.CertCache != nil {
|
||||
c.CertCache.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// MatchDomain return true if a domain match the cert domain
|
||||
func MatchDomain(domain string, certDomain string) bool {
|
||||
if domain == certDomain {
|
||||
return true
|
||||
}
|
||||
|
||||
for len(certDomain) > 0 && certDomain[len(certDomain)-1] == '.' {
|
||||
certDomain = certDomain[:len(certDomain)-1]
|
||||
}
|
||||
|
||||
labels := strings.Split(domain, ".")
|
||||
for i := range labels {
|
||||
labels[i] = "*"
|
||||
candidate := strings.Join(labels, ".")
|
||||
if certDomain == candidate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
107
pkg/tls/certificate_store_test.go
Normal file
107
pkg/tls/certificate_store_test.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/pkg/safe"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetBestCertificate(t *testing.T) {
|
||||
// FIXME Add tests for defaultCert
|
||||
testCases := []struct {
|
||||
desc string
|
||||
domainToCheck string
|
||||
dynamicCert string
|
||||
expectedCert string
|
||||
uppercase bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty Store, returns no certs",
|
||||
domainToCheck: "snitest.com",
|
||||
dynamicCert: "",
|
||||
expectedCert: "",
|
||||
},
|
||||
{
|
||||
desc: "Best Match with no corresponding",
|
||||
domainToCheck: "snitest.com",
|
||||
dynamicCert: "snitest.org",
|
||||
expectedCert: "",
|
||||
},
|
||||
{
|
||||
desc: "Best Match",
|
||||
domainToCheck: "snitest.com",
|
||||
dynamicCert: "snitest.com",
|
||||
expectedCert: "snitest.com",
|
||||
},
|
||||
{
|
||||
desc: "Best Match with dynamic wildcard",
|
||||
domainToCheck: "www.snitest.com",
|
||||
dynamicCert: "*.snitest.com",
|
||||
expectedCert: "*.snitest.com",
|
||||
},
|
||||
{
|
||||
desc: "Best Match with dynamic wildcard only, case insensitive",
|
||||
domainToCheck: "bar.www.snitest.com",
|
||||
dynamicCert: "*.www.snitest.com",
|
||||
expectedCert: "*.www.snitest.com",
|
||||
uppercase: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dynamicMap := map[string]*tls.Certificate{}
|
||||
|
||||
if test.dynamicCert != "" {
|
||||
cert, err := loadTestCert(test.dynamicCert, test.uppercase)
|
||||
require.NoError(t, err)
|
||||
dynamicMap[strings.ToLower(test.dynamicCert)] = cert
|
||||
}
|
||||
|
||||
store := &CertificateStore{
|
||||
DynamicCerts: safe.New(dynamicMap),
|
||||
CertCache: cache.New(1*time.Hour, 10*time.Minute),
|
||||
}
|
||||
|
||||
var expected *tls.Certificate
|
||||
if test.expectedCert != "" {
|
||||
cert, err := loadTestCert(test.expectedCert, test.uppercase)
|
||||
require.NoError(t, err)
|
||||
expected = cert
|
||||
}
|
||||
|
||||
clientHello := &tls.ClientHelloInfo{
|
||||
ServerName: test.domainToCheck,
|
||||
}
|
||||
|
||||
actual := store.GetBestCertificate(clientHello)
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadTestCert(certName string, uppercase bool) (*tls.Certificate, error) {
|
||||
replacement := "wildcard"
|
||||
if uppercase {
|
||||
replacement = "uppercase_wildcard"
|
||||
}
|
||||
|
||||
staticCert, err := tls.LoadX509KeyPair(
|
||||
fmt.Sprintf("../../integration/fixtures/https/%s.cert", strings.Replace(certName, "*", replacement, -1)),
|
||||
fmt.Sprintf("../../integration/fixtures/https/%s.key", strings.Replace(certName, "*", replacement, -1)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &staticCert, nil
|
||||
}
|
94
pkg/tls/generate/generate.go
Normal file
94
pkg/tls/generate/generate.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package generate
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultDomain Traefik domain for the default certificate
|
||||
const DefaultDomain = "TRAEFIK DEFAULT CERT"
|
||||
|
||||
// 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 * (24 * time.Hour))
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: DefaultDomain,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: expiration,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{domain},
|
||||
}
|
||||
|
||||
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||
}
|
76
pkg/tls/tls.go
Normal file
76
pkg/tls/tls.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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
|
||||
type ClientCA struct {
|
||||
Files FilesOrContents
|
||||
Optional bool
|
||||
}
|
||||
|
||||
// TLS configures TLS for an entry point
|
||||
type TLS struct {
|
||||
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
|
||||
}
|
||||
|
||||
// FilesOrContents hold the CA we want to have in root
|
||||
type FilesOrContents []FileOrContent
|
||||
|
||||
// Configuration allows mapping a TLS certificate to a list of entrypoints
|
||||
type Configuration struct {
|
||||
Stores []string
|
||||
Certificate *Certificate
|
||||
}
|
||||
|
||||
// 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 *FilesOrContents) 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 *FilesOrContents) Set(value string) error {
|
||||
filesOrContents := strings.Split(value, ",")
|
||||
if len(filesOrContents) == 0 {
|
||||
return fmt.Errorf("bad FilesOrContents format: %s", value)
|
||||
}
|
||||
for _, fileOrContent := range filesOrContents {
|
||||
*r = append(*r, FileOrContent(fileOrContent))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get return the FilesOrContents list
|
||||
func (r *FilesOrContents) Get() interface{} {
|
||||
return *r
|
||||
}
|
||||
|
||||
// SetValue sets the FilesOrContents with val
|
||||
func (r *FilesOrContents) SetValue(val interface{}) {
|
||||
*r = val.(FilesOrContents)
|
||||
}
|
||||
|
||||
// Type is type of the struct
|
||||
func (r *FilesOrContents) Type() string {
|
||||
return "filesorcontents"
|
||||
}
|
215
pkg/tls/tlsmanager.go
Normal file
215
pkg/tls/tlsmanager.go
Normal file
|
@ -0,0 +1,215 @@
|
|||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/containous/traefik/pkg/log"
|
||||
"github.com/containous/traefik/pkg/tls/generate"
|
||||
"github.com/containous/traefik/pkg/types"
|
||||
"github.com/go-acme/lego/challenge/tlsalpn01"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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
pkg/tls/tlsmanager_test.go
Normal file
64
pkg/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