ACME Default Certificate
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com> Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
This commit is contained in:
parent
693d5da1b9
commit
a002ccfce3
22 changed files with 767 additions and 253 deletions
|
@ -11,7 +11,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/traefik/traefik/v2/pkg/log"
|
||||
"github.com/traefik/traefik/v2/pkg/tls/generate"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -101,55 +100,8 @@ func (f FileOrContent) Read() ([]byte, error) {
|
|||
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.AppendCertificate(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)
|
||||
}
|
||||
|
||||
// AppendCertificate appends a Certificate to a certificates map keyed by entrypoint.
|
||||
func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certificate, ep string) error {
|
||||
// AppendCertificate appends a Certificate to a certificates map keyed by store name.
|
||||
func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certificate, storeName string) error {
|
||||
certContent, err := c.CertFile.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read CertFile : %w", err)
|
||||
|
@ -171,7 +123,6 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi
|
|||
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))
|
||||
|
@ -185,13 +136,16 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guarantees the order to produce a unique cert key.
|
||||
sort.Strings(SANs)
|
||||
certKey := strings.Join(SANs, ",")
|
||||
|
||||
certExists := false
|
||||
if certs[ep] == nil {
|
||||
certs[ep] = make(map[string]*tls.Certificate)
|
||||
if certs[storeName] == nil {
|
||||
certs[storeName] = make(map[string]*tls.Certificate)
|
||||
} else {
|
||||
for domains := range certs[ep] {
|
||||
for domains := range certs[storeName] {
|
||||
if domains == certKey {
|
||||
certExists = true
|
||||
break
|
||||
|
@ -199,10 +153,10 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi
|
|||
}
|
||||
}
|
||||
if certExists {
|
||||
log.Debugf("Skipping addition of certificate for domain(s) %q, to EntryPoint %s, as it already exists for this Entrypoint.", certKey, ep)
|
||||
log.Debugf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName)
|
||||
} else {
|
||||
log.Debugf("Adding certificate for domain(s) %s", certKey)
|
||||
certs[ep][certKey] = &tlsCert
|
||||
certs[storeName][certKey] = &tlsCert
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
|
@ -22,8 +22,11 @@ type CertificateStore struct {
|
|||
|
||||
// NewCertificateStore create a store for dynamic certificates.
|
||||
func NewCertificateStore() *CertificateStore {
|
||||
s := &safe.Safe{}
|
||||
s.Set(make(map[string]*tls.Certificate))
|
||||
|
||||
return &CertificateStore{
|
||||
DynamicCerts: &safe.Safe{},
|
||||
DynamicCerts: s,
|
||||
CertCache: cache.New(1*time.Hour, 10*time.Minute),
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +117,45 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo)
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetCertificate returns the first certificate matching all the given domains.
|
||||
func (c *CertificateStore) GetCertificate(domains []string) *tls.Certificate {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Strings(domains)
|
||||
domainsKey := strings.Join(domains, ",")
|
||||
|
||||
if cert, ok := c.CertCache.Get(domainsKey); ok {
|
||||
return cert.(*tls.Certificate)
|
||||
}
|
||||
|
||||
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
|
||||
for certDomains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
|
||||
if domainsKey == certDomains {
|
||||
c.CertCache.SetDefault(domainsKey, cert)
|
||||
return cert
|
||||
}
|
||||
|
||||
var matchedDomains []string
|
||||
for _, certDomain := range strings.Split(certDomains, ",") {
|
||||
for _, checkDomain := range domains {
|
||||
if certDomain == checkDomain {
|
||||
matchedDomains = append(matchedDomains, certDomain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchedDomains) == len(domains) {
|
||||
c.CertCache.SetDefault(domainsKey, cert)
|
||||
return cert
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetCache clears the cache in the store.
|
||||
func (c CertificateStore) ResetCache() {
|
||||
if c.CertCache != nil {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package tls
|
||||
|
||||
import "github.com/traefik/traefik/v2/pkg/types"
|
||||
|
||||
const certificateHeader = "-----BEGIN CERTIFICATE-----\n"
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
@ -36,7 +38,18 @@ func (o *Options) SetDefaults() {
|
|||
|
||||
// Store holds the options for a given Store.
|
||||
type Store struct {
|
||||
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty" toml:"defaultCertificate,omitempty" yaml:"defaultCertificate,omitempty" export:"true"`
|
||||
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty" toml:"defaultCertificate,omitempty" yaml:"defaultCertificate,omitempty" export:"true"`
|
||||
DefaultGeneratedCert *GeneratedCert `json:"defaultGeneratedCert,omitempty" toml:"defaultGeneratedCert,omitempty" yaml:"defaultGeneratedCert,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// GeneratedCert defines the default generated certificate configuration.
|
||||
type GeneratedCert struct {
|
||||
// Resolver is the name of the resolver that will be used to issue the DefaultCertificate.
|
||||
Resolver string `json:"resolver,omitempty" toml:"resolver,omitempty" yaml:"resolver,omitempty" export:"true"`
|
||||
// Domain is the domain definition for the DefaultCertificate.
|
||||
Domain *types.Domain `json:"domain,omitempty" toml:"domain,omitempty" yaml:"domain,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/traefik/traefik/v2/pkg/log"
|
||||
|
@ -81,17 +83,6 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
|
|||
m.storesConfig[tlsalpn01.ACMETLS1Protocol] = Store{}
|
||||
}
|
||||
|
||||
m.stores = make(map[string]*CertificateStore)
|
||||
for storeName, storeConfig := range m.storesConfig {
|
||||
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, storeName))
|
||||
store, err := buildCertificateStore(ctxStore, storeConfig, storeName)
|
||||
if err != nil {
|
||||
log.FromContext(ctxStore).Errorf("Error while creating certificate store: %v", err)
|
||||
continue
|
||||
}
|
||||
m.stores[storeName] = store
|
||||
}
|
||||
|
||||
storesCertificates := make(map[string]map[string]*tls.Certificate)
|
||||
for _, conf := range certs {
|
||||
if len(conf.Stores) == 0 {
|
||||
|
@ -99,26 +90,68 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
|
|||
log.FromContext(ctx).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"}
|
||||
conf.Stores = []string{DefaultTLSStoreName}
|
||||
}
|
||||
|
||||
for _, store := range conf.Stores {
|
||||
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, store))
|
||||
if err := conf.Certificate.AppendCertificate(storesCertificates, store); err != nil {
|
||||
|
||||
if _, ok := m.storesConfig[store]; !ok {
|
||||
m.storesConfig[store] = Store{}
|
||||
}
|
||||
|
||||
err := conf.Certificate.AppendCertificate(storesCertificates, store)
|
||||
if err != nil {
|
||||
log.FromContext(ctxStore).Errorf("Unable to append certificate %s to store: %v", conf.Certificate.GetTruncatedCertificateName(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for storeName, certs := range storesCertificates {
|
||||
st, ok := m.stores[storeName]
|
||||
if !ok {
|
||||
st, _ = buildCertificateStore(context.Background(), Store{}, storeName)
|
||||
m.stores[storeName] = st
|
||||
m.stores = make(map[string]*CertificateStore)
|
||||
|
||||
for storeName, storeConfig := range m.storesConfig {
|
||||
st := NewCertificateStore()
|
||||
m.stores[storeName] = st
|
||||
|
||||
if certs, ok := storesCertificates[storeName]; ok {
|
||||
st.DynamicCerts.Set(certs)
|
||||
}
|
||||
st.DynamicCerts.Set(certs)
|
||||
|
||||
// a default cert for the ACME store does not make any sense, so generating one is a waste.
|
||||
if storeName == tlsalpn01.ACMETLS1Protocol {
|
||||
continue
|
||||
}
|
||||
|
||||
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, storeName))
|
||||
|
||||
certificate, err := getDefaultCertificate(ctxStore, storeConfig, st)
|
||||
if err != nil {
|
||||
log.FromContext(ctxStore).Errorf("Error while creating certificate store: %v", err)
|
||||
}
|
||||
|
||||
st.DefaultCertificate = certificate
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeDomains sanitizes the domain definition Main and SANS,
|
||||
// and returns them as a slice.
|
||||
// This func apply the same sanitization as the ACME provider do before resolving certificates.
|
||||
func sanitizeDomains(domain types.Domain) ([]string, error) {
|
||||
domains := domain.ToStrArray()
|
||||
if len(domains) == 0 {
|
||||
return nil, errors.New("no domain was given")
|
||||
}
|
||||
|
||||
var cleanDomains []string
|
||||
for _, domain := range domains {
|
||||
canonicalDomain := types.CanonicalDomain(domain)
|
||||
cleanDomain := dns01.UnFqdn(canonicalDomain)
|
||||
cleanDomains = append(cleanDomains, cleanDomain)
|
||||
}
|
||||
|
||||
return cleanDomains, nil
|
||||
}
|
||||
|
||||
// Get gets the TLS configuration to use for a given store / configuration.
|
||||
func (m *Manager) Get(storeName, configName string) (*tls.Config, error) {
|
||||
m.lock.RLock()
|
||||
|
@ -234,32 +267,37 @@ func (m *Manager) GetStore(storeName string) *CertificateStore {
|
|||
return m.getStore(storeName)
|
||||
}
|
||||
|
||||
func buildCertificateStore(ctx context.Context, tlsStore Store, storename string) (*CertificateStore, error) {
|
||||
certificateStore := NewCertificateStore()
|
||||
certificateStore.DynamicCerts.Set(make(map[string]*tls.Certificate))
|
||||
|
||||
func getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*tls.Certificate, error) {
|
||||
if tlsStore.DefaultCertificate != nil {
|
||||
cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate)
|
||||
if err != nil {
|
||||
return certificateStore, err
|
||||
return nil, err
|
||||
}
|
||||
certificateStore.DefaultCertificate = cert
|
||||
return certificateStore, nil
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// a default cert for the ACME store does not make any sense, so generating one
|
||||
// is a waste.
|
||||
if storename == tlsalpn01.ACMETLS1Protocol {
|
||||
return certificateStore, nil
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Debug("No default certificate, generating one")
|
||||
cert, err := generate.DefaultCertificate()
|
||||
defaultCert, err := generate.DefaultCertificate()
|
||||
if err != nil {
|
||||
return certificateStore, err
|
||||
return nil, err
|
||||
}
|
||||
certificateStore.DefaultCertificate = cert
|
||||
return certificateStore, nil
|
||||
|
||||
if tlsStore.DefaultGeneratedCert != nil && tlsStore.DefaultGeneratedCert.Domain != nil && tlsStore.DefaultGeneratedCert.Resolver != "" {
|
||||
domains, err := sanitizeDomains(*tlsStore.DefaultGeneratedCert.Domain)
|
||||
if err != nil {
|
||||
return defaultCert, fmt.Errorf("falling back to the internal generated certificate because invalid domains: %w", err)
|
||||
}
|
||||
|
||||
defaultACMECert := st.GetCertificate(domains)
|
||||
if defaultACMECert == nil {
|
||||
return defaultCert, fmt.Errorf("unable to find certificate for domains %q: falling back to the internal generated certificate", strings.Join(domains, ","))
|
||||
}
|
||||
|
||||
return defaultACMECert, nil
|
||||
}
|
||||
|
||||
log.FromContext(ctx).Debug("No default certificate, fallback to the internal generated certificate")
|
||||
return defaultCert, nil
|
||||
}
|
||||
|
||||
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI.
|
||||
|
|
|
@ -29,6 +29,10 @@ THE SOFTWARE.
|
|||
|
||||
package tls
|
||||
|
||||
import (
|
||||
types "github.com/traefik/traefik/v2/pkg/types"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CertAndStores) DeepCopyInto(out *CertAndStores) {
|
||||
*out = *in
|
||||
|
@ -72,6 +76,27 @@ func (in *ClientAuth) DeepCopy() *ClientAuth {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GeneratedCert) DeepCopyInto(out *GeneratedCert) {
|
||||
*out = *in
|
||||
if in.Domain != nil {
|
||||
in, out := &in.Domain, &out.Domain
|
||||
*out = new(types.Domain)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedCert.
|
||||
func (in *GeneratedCert) DeepCopy() *GeneratedCert {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GeneratedCert)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Options) DeepCopyInto(out *Options) {
|
||||
*out = *in
|
||||
|
@ -112,6 +137,11 @@ func (in *Store) DeepCopyInto(out *Store) {
|
|||
*out = new(Certificate)
|
||||
**out = **in
|
||||
}
|
||||
if in.DefaultGeneratedCert != nil {
|
||||
in, out := &in.DefaultGeneratedCert, &out.DefaultGeneratedCert
|
||||
*out = new(GeneratedCert)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue