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:
Romain 2022-09-13 20:34:08 +02:00 committed by GitHub
parent 693d5da1b9
commit a002ccfce3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 767 additions and 253 deletions

View file

@ -157,7 +157,75 @@ data:
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
```
If no default certificate is provided, Traefik generates and uses a self-signed certificate.
If no `defaultCertificate` is provided, Traefik will use the generated one.
### ACME Default Certificate
You can configure Traefik to use an ACME provider (like Let's Encrypt) to generate the default certificate.
The configuration to resolve the default certificate should be defined in a TLS store:
!!! important "Precedence with the `defaultGeneratedCert` option"
The `defaultGeneratedCert` definition takes precedence over the ACME default certificate configuration.
```yaml tab="File (YAML)"
# Dynamic configuration
tls:
stores:
default:
defaultGeneratedCert:
resolver: myresolver
domain:
main: example.org
sans:
- foo.example.org
- bar.example.org
```
```toml tab="File (TOML)"
# Dynamic configuration
[tls.stores]
[tls.stores.default.defaultGeneratedCert]
resolver = "myresolver"
[tls.stores.default.defaultGeneratedCert.domain]
main = "example.org"
sans = ["foo.example.org", "bar.example.org"]
```
```yaml tab="Kubernetes"
apiVersion: traefik.containo.us/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: default
spec:
defaultGeneratedCert:
resolver: myresolver
domain:
main: example.org
sans:
- foo.example.org
- bar.example.org
```
```yaml tab="Docker"
## Dynamic configuration
labels:
- "traefik.tls.stores.default.defaultgeneratedcert.resolver=myresolver"
- "traefik.tls.stores.default.defaultgeneratedcert.domain.main=example.org"
- "traefik.tls.stores.default.defaultgeneratedcert.domain.sans=foo.example.org, bar.example.org"
```
```json tab="Marathon"
labels: {
"traefik.tls.stores.default.defaultgeneratedcert.resolver": "myresolver",
"traefik.tls.stores.default.defaultgeneratedcert.domain.main": "example.org",
"traefik.tls.stores.default.defaultgeneratedcert.domain.sans": "foo.example.org, bar.example.org",
}
```
## TLS Options

View file

@ -201,3 +201,13 @@
- "traefik.udp.routers.udprouter1.entrypoints=foobar, foobar"
- "traefik.udp.routers.udprouter1.service=foobar"
- "traefik.udp.services.udpservice01.loadbalancer.server.port=foobar"
- "traefik.tls.stores.Store0.defaultcertificate.certfile=foobar"
- "traefik.tls.stores.Store0.defaultcertificate.keyfile=foobar"
- "traefik.tls.stores.Store0.defaultgeneratedcert.domain.main=foobar"
- "traefik.tls.stores.Store0.defaultgeneratedcert.domain.sans=foobar, foobar"
- "traefik.tls.stores.Store0.defaultgeneratedcert.resolver=foobar"
- "traefik.tls.stores.Store1.defaultcertificate.certfile=foobar"
- "traefik.tls.stores.Store1.defaultcertificate.keyfile=foobar"
- "traefik.tls.stores.Store1.defaultgeneratedcert.domain.main=foobar"
- "traefik.tls.stores.Store1.defaultgeneratedcert.domain.sans=foobar, foobar"
- "traefik.tls.stores.Store1.defaultgeneratedcert.resolver=foobar"

View file

@ -463,7 +463,17 @@
[tls.stores.Store0.defaultCertificate]
certFile = "foobar"
keyFile = "foobar"
[tls.stores.Store0.defaultGeneratedCert]
resolver = "foobar"
[tls.stores.Store0.defaultGeneratedCert.domain]
main = "foobar"
sans = ["foobar", "foobar"]
[tls.stores.Store1]
[tls.stores.Store1.defaultCertificate]
certFile = "foobar"
keyFile = "foobar"
[tls.stores.Store1.defaultGeneratedCert]
resolver = "foobar"
[tls.stores.Store1.defaultGeneratedCert.domain]
main = "foobar"
sans = ["foobar", "foobar"]

View file

@ -518,7 +518,21 @@ tls:
defaultCertificate:
certFile: foobar
keyFile: foobar
defaultGeneratedCert:
resolver: foobar
domain:
main: foobar
sans:
- foobar
- foobar
Store1:
defaultCertificate:
certFile: foobar
keyFile: foobar
defaultGeneratedCert:
resolver: foobar
domain:
main: foobar
sans:
- foobar
- foobar

View file

@ -1870,6 +1870,27 @@ spec:
required:
- secretName
type: object
defaultGeneratedCert:
description: DefaultGeneratedCert defines the default generated certificate
configuration.
properties:
domain:
description: Domain is the domain definition for the DefaultCertificate.
properties:
main:
description: Main defines the main domain name.
type: string
sans:
description: SANs defines the subject alternative domain names.
items:
type: string
type: array
type: object
resolver:
description: Resolver is the name of the resolver that will be
used to issue the DefaultCertificate.
type: string
type: object
type: object
required:
- metadata

View file

@ -319,8 +319,16 @@
| `traefik/tls/options/Options1/sniStrict` | `true` |
| `traefik/tls/stores/Store0/defaultCertificate/certFile` | `foobar` |
| `traefik/tls/stores/Store0/defaultCertificate/keyFile` | `foobar` |
| `traefik/tls/stores/Store0/defaultGeneratedCert/domain/main` | `foobar` |
| `traefik/tls/stores/Store0/defaultGeneratedCert/domain/sans/0` | `foobar` |
| `traefik/tls/stores/Store0/defaultGeneratedCert/domain/sans/1` | `foobar` |
| `traefik/tls/stores/Store0/defaultGeneratedCert/resolver` | `foobar` |
| `traefik/tls/stores/Store1/defaultCertificate/certFile` | `foobar` |
| `traefik/tls/stores/Store1/defaultCertificate/keyFile` | `foobar` |
| `traefik/tls/stores/Store1/defaultGeneratedCert/domain/main` | `foobar` |
| `traefik/tls/stores/Store1/defaultGeneratedCert/domain/sans/0` | `foobar` |
| `traefik/tls/stores/Store1/defaultGeneratedCert/domain/sans/1` | `foobar` |
| `traefik/tls/stores/Store1/defaultGeneratedCert/resolver` | `foobar` |
| `traefik/udp/routers/UDPRouter0/entryPoints/0` | `foobar` |
| `traefik/udp/routers/UDPRouter0/entryPoints/1` | `foobar` |
| `traefik/udp/routers/UDPRouter0/service` | `foobar` |

View file

@ -201,3 +201,13 @@
"traefik.udp.routers.udprouter1.entrypoints": "foobar, foobar",
"traefik.udp.routers.udprouter1.service": "foobar",
"traefik.udp.services.udpservice01.loadbalancer.server.port": "foobar",
"traefik.tls.stores.Store0.defaultcertificate.certfile": "foobar",
"traefik.tls.stores.Store0.defaultcertificate.keyfile": "foobar",
"traefik.tls.stores.Store0.defaultgeneratedcert.domain.main": "foobar",
"traefik.tls.stores.Store0.defaultgeneratedcert.domain.sans": "foobar, foobar",
"traefik.tls.stores.Store0.defaultgeneratedcert.resolver": "foobar",
"traefik.tls.stores.Store1.defaultcertificate.certfile": "foobar",
"traefik.tls.stores.Store1.defaultcertificate.keyfile": "foobar",
"traefik.tls.stores.Store1.defaultgeneratedcert.domain.main": "foobar",
"traefik.tls.stores.Store1.defaultgeneratedcert.domain.sans": "foobar, foobar",
"traefik.tls.stores.Store1.defaultgeneratedcert.resolver": "foobar",

View file

@ -63,6 +63,27 @@ spec:
required:
- secretName
type: object
defaultGeneratedCert:
description: DefaultGeneratedCert defines the default generated certificate
configuration.
properties:
domain:
description: Domain is the domain definition for the DefaultCertificate.
properties:
main:
description: Main defines the main domain name.
type: string
sans:
description: SANs defines the subject alternative domain names.
items:
type: string
type: array
type: object
resolver:
description: Resolver is the name of the resolver that will be
used to issue the DefaultCertificate.
type: string
type: object
type: object
required:
- metadata

View file

@ -40,6 +40,7 @@ type acmeTestCase struct {
}
type templateModel struct {
Domain types.Domain
Domains []types.Domain
PortHTTP string
PortHTTPS string
@ -149,6 +150,29 @@ func (s *AcmeSuite) TestHTTP01Domains(c *check.C) {
s.retrieveAcmeCertificate(c, testCase)
}
func (s *AcmeSuite) TestHTTP01StoreDomains(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_store_domains.toml",
subCases: []subCases{{
host: acmeDomain,
expectedCommonName: acmeDomain,
expectedAlgorithm: x509.RSA,
}},
template: templateModel{
Domain: types.Domain{
Main: "traefik.acme.wtf",
},
Acme: map[string]static.CertificateResolver{
"default": {ACME: &acme.Configuration{
HTTPChallenge: &acme.HTTPChallenge{EntryPoint: "web"},
}},
},
},
}
s.retrieveAcmeCertificate(c, testCase)
}
func (s *AcmeSuite) TestHTTP01DomainsInSAN(c *check.C) {
testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_domains.toml",

View file

@ -0,0 +1,60 @@
[global]
checkNewVersion = false
sendAnonymousUsage = false
[log]
level = "DEBUG"
[entryPoints]
[entryPoints.web]
address = "{{ .PortHTTP }}"
[entryPoints.websecure]
address = "{{ .PortHTTPS }}"
{{range $name, $resolvers := .Acme }}
[certificatesResolvers.{{ $name }}.acme]
email = "test@traefik.io"
storage = "/tmp/acme.json"
keyType = "{{ $resolvers.ACME.KeyType }}"
caServer = "{{ $resolvers.ACME.CAServer }}"
{{if $resolvers.ACME.HTTPChallenge }}
[certificatesResolvers.{{ $name }}.acme.httpChallenge]
entryPoint = "{{ $resolvers.ACME.HTTPChallenge.EntryPoint }}"
{{end}}
{{if $resolvers.ACME.TLSChallenge }}
[certificatesResolvers.{{ $name }}.acme.tlsChallenge]
{{end}}
{{end}}
[api]
insecure = true
[providers.file]
filename = "{{ .SelfFilename }}"
## dynamic configuration ##
[http.services]
[http.services.test.loadBalancer]
[[http.services.test.loadBalancer.servers]]
url = "http://127.0.0.1:9010"
[http.routers]
[http.routers.test]
entryPoints = ["websecure"]
rule = "PathPrefix(`/`)"
service = "test"
[http.routers.test.tls]
[tls.stores]
[tls.stores.default.defaultGeneratedCert]
resolver = "default"
[tls.stores.default.defaultGeneratedCert.domain]
main = "{{ .Domain.Main }}"
sans = [{{range .Domain.SANs }}
"{{.}}",
{{end}}]

View file

@ -1870,6 +1870,27 @@ spec:
required:
- secretName
type: object
defaultGeneratedCert:
description: DefaultGeneratedCert defines the default generated certificate
configuration.
properties:
domain:
description: Domain is the domain definition for the DefaultCertificate.
properties:
main:
description: Main defines the main domain name.
type: string
sans:
description: SANs defines the subject alternative domain names.
items:
type: string
type: array
type: object
resolver:
description: Resolver is the name of the resolver that will be
used to issue the DefaultCertificate.
type: string
type: object
type: object
required:
- metadata

View file

@ -325,7 +325,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificate(c *check.C) {
cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("snitest.com")
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2")
@ -360,7 +360,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI(c *check.C) {
cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("snitest.com")
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2")
@ -397,7 +397,7 @@ func (s *HTTPSSuite) TestWithOverlappingStaticCertificate(c *check.C) {
cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com")
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2")
@ -434,7 +434,7 @@ func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate(c *check.C) {
cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com")
c.Assert(err, checker.IsNil, check.Commentf("certificate did not serve correct default certificate"))
c.Assert(err, checker.IsNil, check.Commentf("server did not serve correct default certificate"))
proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2")

View file

@ -8,6 +8,7 @@ import (
"fmt"
"net/url"
"reflect"
"sort"
"strings"
"sync"
"time"
@ -29,8 +30,8 @@ import (
"github.com/traefik/traefik/v2/pkg/version"
)
// oscpMustStaple enables OSCP stapling as from https://github.com/go-acme/lego/issues/270.
var oscpMustStaple = false
// ocspMustStaple enables OCSP stapling as from https://github.com/go-acme/lego/issues/270.
var ocspMustStaple = false
// Configuration holds ACME configuration provided by users.
type Configuration struct {
@ -99,10 +100,11 @@ type Provider struct {
TLSChallengeProvider challenge.Provider
HTTPChallengeProvider challenge.Provider
certificates []*CertAndStore
certificates []*CertAndStore
certificatesMu sync.RWMutex
account *Account
client *lego.Client
certsChan chan *CertAndStore
configurationChan chan<- dynamic.Message
tlsManager *traefiktls.Manager
clientMutex sync.Mutex
@ -152,7 +154,10 @@ func (p *Provider) Init() error {
p.account = nil
}
p.certificatesMu.Lock()
p.certificates, err = p.Store.GetCertificates(p.ResolverName)
p.certificatesMu.Unlock()
if err != nil {
return fmt.Errorf("unable to get ACME certificates : %w", err)
}
@ -195,11 +200,15 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
p.pool = pool
p.watchCertificate(ctx)
p.watchNewDomains(ctx)
p.configurationChan = configurationChan
p.refreshCertificates()
p.certificatesMu.RLock()
msg := p.buildMessage()
p.certificatesMu.RUnlock()
p.configurationChan <- msg
renewPeriod, renewInterval := getCertificateRenewDurations(p.CertificatesDuration)
log.FromContext(ctx).Debugf("Attempt to renew certificates %q before expiry and check every %q",
@ -365,12 +374,14 @@ func (p *Provider) register(ctx context.Context, client *lego.Client) (*registra
}
func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStore string) {
logger := log.FromContext(ctx)
if len(domains) == 0 {
log.FromContext(ctx).Debug("No domain parsed in provider ACME")
logger.Debug("No domain parsed in provider ACME")
return
}
log.FromContext(ctx).Debugf("Try to challenge certificate for domain %v found in HostSNI rule", domains)
logger.Debugf("Trying to challenge certificate for domain %v found in HostSNI rule", domains)
var domain types.Domain
if len(domains) > 0 {
@ -380,14 +391,22 @@ func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStor
}
safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil {
log.FromContext(ctx).Errorf("Unable to obtain ACME certificate for domains %q: %v", strings.Join(domains, ","), err)
dom, cert, err := p.resolveCertificate(ctx, domain, tlsStore)
if err != nil {
logger.Errorf("Unable to obtain ACME certificate for domains %q: %v", strings.Join(domains, ","), err)
return
}
err = p.addCertificateForDomain(dom, cert.Certificate, cert.PrivateKey, tlsStore)
if err != nil {
logger.WithError(err).Error("Error adding certificate for domain")
}
})
}
}
func (p *Provider) watchNewDomains(ctx context.Context) {
ctx = log.With(ctx, log.Str(log.ProviderName, p.ResolverName+".acme"))
p.pool.GoCtx(func(ctxPool context.Context) {
for {
select {
@ -402,31 +421,26 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
logger := log.FromContext(ctxRouter)
if len(route.TLS.Domains) > 0 {
for _, domain := range route.TLS.Domains {
if domain.Main != dns01.UnFqdn(domain.Main) {
logger.Warnf("FQDN detected, please remove the trailing dot: %s", domain.Main)
}
for _, san := range domain.SANs {
if san != dns01.UnFqdn(san) {
logger.Warnf("FQDN detected, please remove the trailing dot: %s", san)
}
}
}
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := 0; i < len(domains); i++ {
domain := domains[i]
safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName); err != nil {
log.WithoutContext().WithField(log.ProviderName, p.ResolverName+".acme").
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
if err != nil {
logger.WithError(err).Errorf("Unable to obtain ACME certificate for domains %q", strings.Join(domain.ToStrArray(), ","))
return
}
err = p.addCertificateForDomain(dom, cert.Certificate, cert.PrivateKey, traefiktls.DefaultTLSStoreName)
if err != nil {
logger.WithError(err).Error("Error adding certificate for domain")
}
})
}
} else {
domains, err := tcpmuxer.ParseHostSNI(route.Rule)
if err != nil {
logger.Errorf("Error parsing domains in provider ACME: %v", err)
logger.WithError(err).Errorf("Error parsing domains in provider ACME")
continue
}
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName)
@ -434,32 +448,98 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
}
}
for routerName, route := range config.HTTP.Routers {
if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
if config.HTTP != nil {
for routerName, route := range config.HTTP.Routers {
if route.TLS == nil || route.TLS.CertResolver != p.ResolverName {
continue
}
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
logger := log.FromContext(ctxRouter)
if len(route.TLS.Domains) > 0 {
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := 0; i < len(domains); i++ {
domain := domains[i]
safe.Go(func() {
dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
if err != nil {
logger.WithError(err).Errorf("Unable to obtain ACME certificate for domains %q", strings.Join(domain.ToStrArray(), ","))
return
}
err = p.addCertificateForDomain(dom, cert.Certificate, cert.PrivateKey, traefiktls.DefaultTLSStoreName)
if err != nil {
logger.WithError(err).Error("Error adding certificate for domain")
}
})
}
} else {
domains, err := httpmuxer.ParseDomains(route.Rule)
if err != nil {
logger.WithError(err).Errorf("Error parsing domains in provider ACME")
continue
}
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName)
}
}
}
if config.TLS == nil {
continue
}
for tlsStoreName, tlsStore := range config.TLS.Stores {
ctxTLSStore := log.With(ctx, log.Str(log.TLSStoreName, tlsStoreName))
logger := log.FromContext(ctxTLSStore)
if tlsStore.DefaultCertificate != nil && tlsStore.DefaultGeneratedCert != nil {
logger.Warn("defaultCertificate and defaultGeneratedCert cannot be defined at the same time.")
}
// Gives precedence to the user defined default certificate.
if tlsStore.DefaultCertificate != nil || tlsStore.DefaultGeneratedCert == nil {
continue
}
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule))
if len(route.TLS.Domains) > 0 {
domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := 0; i < len(domains); i++ {
domain := domains[i]
safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName); err != nil {
log.WithoutContext().WithField(log.ProviderName, p.ResolverName+".acme").
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err)
}
})
}
} else {
domains, err := httpmuxer.ParseDomains(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue
}
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName)
if tlsStore.DefaultGeneratedCert.Domain == nil || tlsStore.DefaultGeneratedCert.Resolver == "" {
logger.Warn("default generated certificate domain or resolver is missing.")
continue
}
if tlsStore.DefaultGeneratedCert.Resolver != p.ResolverName {
continue
}
validDomains, err := p.sanitizeDomains(ctx, *tlsStore.DefaultGeneratedCert.Domain)
if err != nil {
logger.WithError(err).Errorf("domains validation: %s", strings.Join(tlsStore.DefaultGeneratedCert.Domain.ToStrArray(), ","))
}
if p.certExists(validDomains) {
logger.Debug("Default ACME certificate generation is not required.")
continue
}
safe.Go(func() {
cert, err := p.resolveDefaultCertificate(ctx, validDomains)
if err != nil {
logger.WithError(err).Errorf("Unable to obtain ACME certificate for domain %q", strings.Join(validDomains, ","))
return
}
domain := types.Domain{
Main: validDomains[0],
}
if len(validDomains) > 0 {
domain.SANs = validDomains[1:]
}
err = p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey, traefiktls.DefaultTLSStoreName)
if err != nil {
logger.WithError(err).Error("Error adding certificate for domain")
}
})
}
case <-ctxPool.Done():
return
@ -468,22 +548,30 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, tlsStore string) (*certificate.Resource, error) {
domains, err := p.getValidDomains(ctx, domain)
if err != nil {
return nil, err
}
func (p *Provider) resolveDefaultCertificate(ctx context.Context, domains []string) (*certificate.Resource, error) {
logger := log.FromContext(ctx)
// Check if provided certificates are not already in progress and lock them if needed
uncheckedDomains := p.getUncheckedDomains(ctx, domains, tlsStore)
if len(uncheckedDomains) == 0 {
p.resolvingDomainsMutex.Lock()
sort.Strings(domains)
domainKey := strings.Join(domains, ",")
if _, ok := p.resolvingDomains[domainKey]; ok {
p.resolvingDomainsMutex.Unlock()
return nil, nil
}
defer p.removeResolvingDomains(uncheckedDomains)
p.resolvingDomains[domainKey] = struct{}{}
logger := log.FromContext(ctx)
logger.Debugf("Loading ACME certificates %+v...", uncheckedDomains)
for _, certDomain := range domains {
p.resolvingDomains[certDomain] = struct{}{}
}
p.resolvingDomainsMutex.Unlock()
defer p.removeResolvingDomains(append(domains, domainKey))
logger.Debugf("Loading ACME certificates %+v...", domains)
client, err := p.getClient()
if err != nil {
@ -493,31 +581,74 @@ func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain,
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
MustStaple: oscpMustStaple,
MustStaple: ocspMustStaple,
PreferredChain: p.PreferredChain,
}
cert, err := client.Certificate.Obtain(request)
if err != nil {
return nil, fmt.Errorf("unable to generate a certificate for the domains %v: %w", uncheckedDomains, err)
return nil, fmt.Errorf("unable to generate a certificate for the domains %v: %w", domains, err)
}
if cert == nil {
return nil, fmt.Errorf("domains %v do not generate a certificate", uncheckedDomains)
return nil, fmt.Errorf("unable to generate a certificate for the domains %v", domains)
}
if len(cert.Certificate) == 0 || len(cert.PrivateKey) == 0 {
return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, cert)
return nil, fmt.Errorf("certificate for domains %v is empty: %v", domains, cert)
}
logger.Debugf("Default certificate obtained for domains %+v", domains)
return cert, nil
}
func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain, tlsStore string) (types.Domain, *certificate.Resource, error) {
domains, err := p.sanitizeDomains(ctx, domain)
if err != nil {
return types.Domain{}, nil, err
}
// Check if provided certificates are not already in progress and lock them if needed
uncheckedDomains := p.getUncheckedDomains(ctx, domains, tlsStore)
if len(uncheckedDomains) == 0 {
return types.Domain{}, nil, nil
}
defer p.removeResolvingDomains(uncheckedDomains)
logger := log.FromContext(ctx)
logger.Debugf("Loading ACME certificates %+v...", uncheckedDomains)
client, err := p.getClient()
if err != nil {
return types.Domain{}, nil, fmt.Errorf("cannot get ACME client %w", err)
}
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
MustStaple: ocspMustStaple,
PreferredChain: p.PreferredChain,
}
cert, err := client.Certificate.Obtain(request)
if err != nil {
return types.Domain{}, nil, fmt.Errorf("unable to generate a certificate for the domains %v: %w", uncheckedDomains, err)
}
if cert == nil {
return types.Domain{}, nil, fmt.Errorf("unable to generate a certificate for the domains %v", uncheckedDomains)
}
if len(cert.Certificate) == 0 || len(cert.PrivateKey) == 0 {
return types.Domain{}, nil, fmt.Errorf("certificate for domains %v is empty: %v", uncheckedDomains, cert)
}
logger.Debugf("Certificates obtained for domains %+v", uncheckedDomains)
domain = types.Domain{Main: uncheckedDomains[0]}
if len(uncheckedDomains) > 1 {
domain = types.Domain{Main: uncheckedDomains[0], SANs: uncheckedDomains[1:]}
} else {
domain = types.Domain{Main: uncheckedDomains[0]}
domain.SANs = uncheckedDomains[1:]
}
p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey, tlsStore)
return cert, nil
return domain, cert, nil
}
func (p *Provider) removeResolvingDomains(resolvingDomains []string) {
@ -529,8 +660,28 @@ func (p *Provider) removeResolvingDomains(resolvingDomains []string) {
}
}
func (p *Provider) addCertificateForDomain(domain types.Domain, certificate, key []byte, tlsStore string) {
p.certsChan <- &CertAndStore{Certificate: Certificate{Certificate: certificate, Key: key, Domain: domain}, Store: tlsStore}
func (p *Provider) addCertificateForDomain(domain types.Domain, certificate, key []byte, tlsStore string) error {
p.certificatesMu.Lock()
defer p.certificatesMu.Unlock()
cert := Certificate{Certificate: certificate, Key: key, Domain: domain}
certUpdated := false
for _, domainsCertificate := range p.certificates {
if reflect.DeepEqual(domain, domainsCertificate.Certificate.Domain) {
domainsCertificate.Certificate = cert
certUpdated = true
break
}
}
if !certUpdated {
p.certificates = append(p.certificates, &CertAndStore{Certificate: cert, Store: tlsStore})
}
p.configurationChan <- p.buildMessage()
return p.Store.SaveCertificates(p.ResolverName, p.certificates)
}
// getCertificateRenewDurations returns renew durations calculated from the given certificatesDuration in hours.
@ -608,45 +759,7 @@ func deleteUnnecessaryDomains(ctx context.Context, domains []types.Domain) []typ
return newDomains
}
func (p *Provider) watchCertificate(ctx context.Context) {
p.certsChan = make(chan *CertAndStore)
p.pool.GoCtx(func(ctxPool context.Context) {
for {
select {
case cert := <-p.certsChan:
certUpdated := false
for _, domainsCertificate := range p.certificates {
if reflect.DeepEqual(cert.Domain, domainsCertificate.Certificate.Domain) {
domainsCertificate.Certificate = cert.Certificate
certUpdated = true
break
}
}
if !certUpdated {
p.certificates = append(p.certificates, cert)
}
err := p.saveCertificates()
if err != nil {
log.FromContext(ctx).Error(err)
}
case <-ctxPool.Done():
return
}
}
})
}
func (p *Provider) saveCertificates() error {
err := p.Store.SaveCertificates(p.ResolverName, p.certificates)
p.refreshCertificates()
return err
}
func (p *Provider) refreshCertificates() {
func (p *Provider) buildMessage() dynamic.Message {
conf := dynamic.Message{
ProviderName: p.ResolverName + ".acme",
Configuration: &dynamic.Configuration{
@ -670,41 +783,54 @@ func (p *Provider) refreshCertificates() {
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
}
p.configurationChan <- conf
return conf
}
func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Duration) {
logger := log.FromContext(ctx)
logger.Info("Testing certificate renew...")
p.certificatesMu.RLock()
var certificates []*CertAndStore
for _, cert := range p.certificates {
crt, err := getX509Certificate(ctx, &cert.Certificate)
// If there's an error, we assume the cert is broken, and needs update
if err != nil || crt == nil || crt.NotAfter.Before(time.Now().Add(renewPeriod)) {
client, err := p.getClient()
if err != nil {
logger.Infof("Error renewing certificate from LE : %+v, %v", cert.Domain, err)
continue
}
certificates = append(certificates, cert)
}
}
logger.Infof("Renewing certificate from LE : %+v", cert.Domain)
p.certificatesMu.RUnlock()
renewedCert, err := client.Certificate.Renew(certificate.Resource{
Domain: cert.Domain.Main,
PrivateKey: cert.Key,
Certificate: cert.Certificate.Certificate,
}, true, oscpMustStaple, p.PreferredChain)
if err != nil {
logger.Errorf("Error renewing certificate from LE: %v, %v", cert.Domain, err)
continue
}
for _, cert := range certificates {
client, err := p.getClient()
if err != nil {
logger.WithError(err).Infof("Error renewing certificate from LE : %+v", cert.Domain)
continue
}
if len(renewedCert.Certificate) == 0 || len(renewedCert.PrivateKey) == 0 {
logger.Errorf("domains %v renew certificate with no value: %v", cert.Domain.ToStrArray(), cert)
continue
}
logger.Infof("Renewing certificate from LE : %+v", cert.Domain)
p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey, cert.Store)
renewedCert, err := client.Certificate.Renew(certificate.Resource{
Domain: cert.Domain.Main,
PrivateKey: cert.Key,
Certificate: cert.Certificate.Certificate,
}, true, ocspMustStaple, p.PreferredChain)
if err != nil {
logger.WithError(err).Errorf("Error renewing certificate from LE: %v", cert.Domain)
continue
}
if len(renewedCert.Certificate) == 0 || len(renewedCert.PrivateKey) == 0 {
logger.Errorf("domains %v renew certificate with no value: %v", cert.Domain.ToStrArray(), cert)
continue
}
err = p.addCertificateForDomain(cert.Domain, renewedCert.Certificate, renewedCert.PrivateKey, cert.Store)
if err != nil {
logger.WithError(err).Error("Error adding certificate for domain")
}
}
}
@ -712,17 +838,24 @@ func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Durat
// Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates.
func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, tlsStore string) []string {
p.resolvingDomainsMutex.Lock()
defer p.resolvingDomainsMutex.Unlock()
log.FromContext(ctx).Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck)
allDomains := p.tlsManager.GetStore(tlsStore).GetAllDomains()
var allDomains []string
store := p.tlsManager.GetStore(tlsStore)
if store != nil {
allDomains = append(allDomains, store.GetAllDomains()...)
}
// Get ACME certificates
p.certificatesMu.RLock()
for _, cert := range p.certificates {
allDomains = append(allDomains, strings.Join(cert.Domain.ToStrArray(), ","))
}
p.certificatesMu.RUnlock()
p.resolvingDomainsMutex.Lock()
defer p.resolvingDomainsMutex.Unlock()
// Get currently resolved domains
for domain := range p.resolvingDomains {
@ -761,7 +894,7 @@ func getX509Certificate(ctx context.Context, cert *Certificate) (*x509.Certifica
tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.Key)
if err != nil {
logger.Errorf("Failed to load TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed : %v", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","), err)
logger.WithError(err).Errorf("Failed to load TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","))
return nil, err
}
@ -769,43 +902,62 @@ func getX509Certificate(ctx context.Context, cert *Certificate) (*x509.Certifica
if crt == nil {
crt, err = x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
logger.Errorf("Failed to parse TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed : %v", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","), err)
logger.WithError(err).Errorf("Failed to parse TLS key pair from ACME certificate for domain %q (SAN : %q), certificate will be renewed", cert.Domain.Main, strings.Join(cert.Domain.SANs, ","))
}
}
return crt, err
}
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it.
func (p *Provider) getValidDomains(ctx context.Context, domain types.Domain) ([]string, error) {
// sanitizeDomains checks if given domain is allowed to generate a ACME certificate and return it.
func (p *Provider) sanitizeDomains(ctx context.Context, domain types.Domain) ([]string, error) {
domains := domain.ToStrArray()
if len(domains) == 0 {
return nil, errors.New("unable to generate a certificate in ACME provider when no domain is given")
}
if strings.HasPrefix(domain.Main, "*") {
if p.DNSChallenge == nil {
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
}
if strings.HasPrefix(domain.Main, "*.*") {
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ","))
}
return nil, errors.New("no domain was given")
}
var cleanDomains []string
for _, domain := range domains {
canonicalDomain := types.CanonicalDomain(domain)
for _, dom := range domains {
if strings.HasPrefix(dom, "*") {
if p.DNSChallenge == nil {
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME needs a DNSChallenge", strings.Join(domains, ","))
}
if strings.HasPrefix(dom, "*.*") {
return nil, fmt.Errorf("unable to generate a wildcard certificate in ACME provider for domain %q : ACME does not allow '*.*' wildcard domain", strings.Join(domains, ","))
}
}
canonicalDomain := types.CanonicalDomain(dom)
cleanDomain := dns01.UnFqdn(canonicalDomain)
if canonicalDomain != cleanDomain {
log.FromContext(ctx).Warnf("FQDN detected, please remove the trailing dot: %s", canonicalDomain)
}
cleanDomains = append(cleanDomains, cleanDomain)
}
return cleanDomains, nil
}
// certExists returns whether a certificate already exists for given domains.
func (p *Provider) certExists(validDomains []string) bool {
p.certificatesMu.RLock()
defer p.certificatesMu.RUnlock()
sort.Strings(validDomains)
for _, cert := range p.certificates {
domains := cert.Certificate.Domain.ToStrArray()
sort.Strings(domains)
if reflect.DeepEqual(domains, validDomains) {
return true
}
}
return false
}
func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool {
for _, certDomains := range existentDomains {
for _, certDomain := range strings.Split(certDomains, ",") {

View file

@ -188,7 +188,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
}
}
func TestGetValidDomain(t *testing.T) {
func TestProvider_sanitizeDomains(t *testing.T) {
testCases := []struct {
desc string
domains types.Domain
@ -214,7 +214,7 @@ func TestGetValidDomain(t *testing.T) {
desc: "no domain",
domains: types.Domain{},
dnsChallenge: nil,
expectedErr: "unable to generate a certificate in ACME provider when no domain is given",
expectedErr: "no domain was given",
expectedDomains: nil,
},
{
@ -254,7 +254,7 @@ func TestGetValidDomain(t *testing.T) {
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}}
domains, err := acmeProvider.getValidDomains(context.Background(), test.domains)
domains, err := acmeProvider.sanitizeDomains(context.Background(), test.domains)
if len(test.expectedErr) > 0 {
assert.EqualError(t, err, test.expectedErr, "Unexpected error.")

View file

@ -942,6 +942,13 @@ func buildTLSStores(ctx context.Context, client Client) (map[string]tls.Store, m
}
}
if t.Spec.DefaultGeneratedCert != nil {
tlsStore.DefaultGeneratedCert = &tls.GeneratedCert{
Resolver: t.Spec.DefaultGeneratedCert.Resolver,
Domain: t.Spec.DefaultGeneratedCert.Domain,
}
}
if err := buildCertificates(client, id, t.Namespace, t.Spec.Certificates, tlsConfigs); err != nil {
logger.Errorf("Failed to load certificates: %v", err)
continue

View file

@ -1,6 +1,7 @@
package v1alpha1
import (
"github.com/traefik/traefik/v2/pkg/tls"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -27,6 +28,10 @@ type TLSStore struct {
type TLSStoreSpec struct {
// DefaultCertificate defines the default certificate configuration.
DefaultCertificate *Certificate `json:"defaultCertificate,omitempty"`
// DefaultGeneratedCert defines the default generated certificate configuration.
DefaultGeneratedCert *tls.GeneratedCert `json:"defaultGeneratedCert,omitempty"`
// Certificates is a list of secret names, each secret holding a key/certificate pair to add to the store.
Certificates []Certificate `json:"certificates,omitempty"`
}

View file

@ -31,6 +31,7 @@ package v1alpha1
import (
dynamic "github.com/traefik/traefik/v2/pkg/config/dynamic"
tls "github.com/traefik/traefik/v2/pkg/tls"
types "github.com/traefik/traefik/v2/pkg/types"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
@ -1450,6 +1451,11 @@ func (in *TLSStoreSpec) DeepCopyInto(out *TLSStoreSpec) {
*out = new(Certificate)
**out = **in
}
if in.DefaultGeneratedCert != nil {
in, out := &in.DefaultGeneratedCert, &out.DefaultGeneratedCert
*out = new(tls.GeneratedCert)
(*in).DeepCopyInto(*out)
}
if in.Certificates != nil {
in, out := &in.Certificates, &out.Certificates
*out = make([]Certificate, len(*in))

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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.

View file

@ -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
}