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= 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 ## TLS Options

View file

@ -201,3 +201,13 @@
- "traefik.udp.routers.udprouter1.entrypoints=foobar, foobar" - "traefik.udp.routers.udprouter1.entrypoints=foobar, foobar"
- "traefik.udp.routers.udprouter1.service=foobar" - "traefik.udp.routers.udprouter1.service=foobar"
- "traefik.udp.services.udpservice01.loadbalancer.server.port=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] [tls.stores.Store0.defaultCertificate]
certFile = "foobar" certFile = "foobar"
keyFile = "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]
[tls.stores.Store1.defaultCertificate] [tls.stores.Store1.defaultCertificate]
certFile = "foobar" certFile = "foobar"
keyFile = "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: defaultCertificate:
certFile: foobar certFile: foobar
keyFile: foobar keyFile: foobar
defaultGeneratedCert:
resolver: foobar
domain:
main: foobar
sans:
- foobar
- foobar
Store1: Store1:
defaultCertificate: defaultCertificate:
certFile: foobar certFile: foobar
keyFile: foobar keyFile: foobar
defaultGeneratedCert:
resolver: foobar
domain:
main: foobar
sans:
- foobar
- foobar

View file

@ -1870,6 +1870,27 @@ spec:
required: required:
- secretName - secretName
type: object 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 type: object
required: required:
- metadata - metadata

View file

@ -319,8 +319,16 @@
| `traefik/tls/options/Options1/sniStrict` | `true` | | `traefik/tls/options/Options1/sniStrict` | `true` |
| `traefik/tls/stores/Store0/defaultCertificate/certFile` | `foobar` | | `traefik/tls/stores/Store0/defaultCertificate/certFile` | `foobar` |
| `traefik/tls/stores/Store0/defaultCertificate/keyFile` | `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/certFile` | `foobar` |
| `traefik/tls/stores/Store1/defaultCertificate/keyFile` | `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/0` | `foobar` |
| `traefik/udp/routers/UDPRouter0/entryPoints/1` | `foobar` | | `traefik/udp/routers/UDPRouter0/entryPoints/1` | `foobar` |
| `traefik/udp/routers/UDPRouter0/service` | `foobar` | | `traefik/udp/routers/UDPRouter0/service` | `foobar` |

View file

@ -201,3 +201,13 @@
"traefik.udp.routers.udprouter1.entrypoints": "foobar, foobar", "traefik.udp.routers.udprouter1.entrypoints": "foobar, foobar",
"traefik.udp.routers.udprouter1.service": "foobar", "traefik.udp.routers.udprouter1.service": "foobar",
"traefik.udp.services.udpservice01.loadbalancer.server.port": "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: required:
- secretName - secretName
type: object 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 type: object
required: required:
- metadata - metadata

View file

@ -40,6 +40,7 @@ type acmeTestCase struct {
} }
type templateModel struct { type templateModel struct {
Domain types.Domain
Domains []types.Domain Domains []types.Domain
PortHTTP string PortHTTP string
PortHTTPS string PortHTTPS string
@ -149,6 +150,29 @@ func (s *AcmeSuite) TestHTTP01Domains(c *check.C) {
s.retrieveAcmeCertificate(c, testCase) 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) { func (s *AcmeSuite) TestHTTP01DomainsInSAN(c *check.C) {
testCase := acmeTestCase{ testCase := acmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_domains.toml", 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: required:
- secretName - secretName
type: object 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 type: object
required: required:
- metadata - metadata

View file

@ -325,7 +325,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificate(c *check.C) {
cs := conn.ConnectionState() cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("snitest.com") 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 proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2") c.Assert(proto, checker.Equals, "h2")
@ -360,7 +360,7 @@ func (s *HTTPSSuite) TestWithDefaultCertificateNoSNI(c *check.C) {
cs := conn.ConnectionState() cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("snitest.com") 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 proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2") c.Assert(proto, checker.Equals, "h2")
@ -397,7 +397,7 @@ func (s *HTTPSSuite) TestWithOverlappingStaticCertificate(c *check.C) {
cs := conn.ConnectionState() cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com") 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 proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2") c.Assert(proto, checker.Equals, "h2")
@ -434,7 +434,7 @@ func (s *HTTPSSuite) TestWithOverlappingDynamicCertificate(c *check.C) {
cs := conn.ConnectionState() cs := conn.ConnectionState()
err = cs.PeerCertificates[0].VerifyHostname("www.snitest.com") 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 proto := cs.NegotiatedProtocol
c.Assert(proto, checker.Equals, "h2") c.Assert(proto, checker.Equals, "h2")

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"reflect" "reflect"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -29,8 +30,8 @@ import (
"github.com/traefik/traefik/v2/pkg/version" "github.com/traefik/traefik/v2/pkg/version"
) )
// oscpMustStaple enables OSCP stapling as from https://github.com/go-acme/lego/issues/270. // ocspMustStaple enables OCSP stapling as from https://github.com/go-acme/lego/issues/270.
var oscpMustStaple = false var ocspMustStaple = false
// Configuration holds ACME configuration provided by users. // Configuration holds ACME configuration provided by users.
type Configuration struct { type Configuration struct {
@ -99,10 +100,11 @@ type Provider struct {
TLSChallengeProvider challenge.Provider TLSChallengeProvider challenge.Provider
HTTPChallengeProvider challenge.Provider HTTPChallengeProvider challenge.Provider
certificates []*CertAndStore certificates []*CertAndStore
certificatesMu sync.RWMutex
account *Account account *Account
client *lego.Client client *lego.Client
certsChan chan *CertAndStore
configurationChan chan<- dynamic.Message configurationChan chan<- dynamic.Message
tlsManager *traefiktls.Manager tlsManager *traefiktls.Manager
clientMutex sync.Mutex clientMutex sync.Mutex
@ -152,7 +154,10 @@ func (p *Provider) Init() error {
p.account = nil p.account = nil
} }
p.certificatesMu.Lock()
p.certificates, err = p.Store.GetCertificates(p.ResolverName) p.certificates, err = p.Store.GetCertificates(p.ResolverName)
p.certificatesMu.Unlock()
if err != nil { if err != nil {
return fmt.Errorf("unable to get ACME certificates : %w", err) 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.pool = pool
p.watchCertificate(ctx)
p.watchNewDomains(ctx) p.watchNewDomains(ctx)
p.configurationChan = configurationChan p.configurationChan = configurationChan
p.refreshCertificates()
p.certificatesMu.RLock()
msg := p.buildMessage()
p.certificatesMu.RUnlock()
p.configurationChan <- msg
renewPeriod, renewInterval := getCertificateRenewDurations(p.CertificatesDuration) renewPeriod, renewInterval := getCertificateRenewDurations(p.CertificatesDuration)
log.FromContext(ctx).Debugf("Attempt to renew certificates %q before expiry and check every %q", 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) { func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStore string) {
logger := log.FromContext(ctx)
if len(domains) == 0 { if len(domains) == 0 {
log.FromContext(ctx).Debug("No domain parsed in provider ACME") logger.Debug("No domain parsed in provider ACME")
return 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 var domain types.Domain
if len(domains) > 0 { if len(domains) > 0 {
@ -380,14 +391,22 @@ func (p *Provider) resolveDomains(ctx context.Context, domains []string, tlsStor
} }
safe.Go(func() { safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, tlsStore); err != nil { dom, cert, err := p.resolveCertificate(ctx, domain, tlsStore)
log.FromContext(ctx).Errorf("Unable to obtain ACME certificate for domains %q: %v", strings.Join(domains, ","), err) 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) { 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) { p.pool.GoCtx(func(ctxPool context.Context) {
for { for {
select { select {
@ -402,31 +421,26 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
logger := log.FromContext(ctxRouter) logger := log.FromContext(ctxRouter)
if len(route.TLS.Domains) > 0 { 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) domains := deleteUnnecessaryDomains(ctxRouter, route.TLS.Domains)
for i := 0; i < len(domains); i++ { for i := 0; i < len(domains); i++ {
domain := domains[i] domain := domains[i]
safe.Go(func() { safe.Go(func() {
if _, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName); err != nil { dom, cert, err := p.resolveCertificate(ctx, domain, traefiktls.DefaultTLSStoreName)
log.WithoutContext().WithField(log.ProviderName, p.ResolverName+".acme"). if err != nil {
Errorf("Unable to obtain ACME certificate for domains %q : %v", strings.Join(domain.ToStrArray(), ","), err) 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 { } else {
domains, err := tcpmuxer.ParseHostSNI(route.Rule) domains, err := tcpmuxer.ParseHostSNI(route.Rule)
if err != nil { if err != nil {
logger.Errorf("Error parsing domains in provider ACME: %v", err) logger.WithError(err).Errorf("Error parsing domains in provider ACME")
continue continue
} }
p.resolveDomains(ctxRouter, domains, traefiktls.DefaultTLSStoreName) 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 config.HTTP != nil {
if route.TLS == nil || route.TLS.CertResolver != p.ResolverName { 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 continue
} }
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName), log.Str(log.Rule, route.Rule)) if tlsStore.DefaultGeneratedCert.Domain == nil || tlsStore.DefaultGeneratedCert.Resolver == "" {
logger.Warn("default generated certificate domain or resolver is missing.")
if len(route.TLS.Domains) > 0 { continue
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.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(): case <-ctxPool.Done():
return 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) { func (p *Provider) resolveDefaultCertificate(ctx context.Context, domains []string) (*certificate.Resource, error) {
domains, err := p.getValidDomains(ctx, domain) logger := log.FromContext(ctx)
if err != nil {
return nil, err
}
// Check if provided certificates are not already in progress and lock them if needed p.resolvingDomainsMutex.Lock()
uncheckedDomains := p.getUncheckedDomains(ctx, domains, tlsStore)
if len(uncheckedDomains) == 0 { sort.Strings(domains)
domainKey := strings.Join(domains, ",")
if _, ok := p.resolvingDomains[domainKey]; ok {
p.resolvingDomainsMutex.Unlock()
return nil, nil return nil, nil
} }
defer p.removeResolvingDomains(uncheckedDomains) p.resolvingDomains[domainKey] = struct{}{}
logger := log.FromContext(ctx) for _, certDomain := range domains {
logger.Debugf("Loading ACME certificates %+v...", uncheckedDomains) p.resolvingDomains[certDomain] = struct{}{}
}
p.resolvingDomainsMutex.Unlock()
defer p.removeResolvingDomains(append(domains, domainKey))
logger.Debugf("Loading ACME certificates %+v...", domains)
client, err := p.getClient() client, err := p.getClient()
if err != nil { if err != nil {
@ -493,31 +581,74 @@ func (p *Provider) resolveCertificate(ctx context.Context, domain types.Domain,
request := certificate.ObtainRequest{ request := certificate.ObtainRequest{
Domains: domains, Domains: domains,
Bundle: true, Bundle: true,
MustStaple: oscpMustStaple, MustStaple: ocspMustStaple,
PreferredChain: p.PreferredChain, PreferredChain: p.PreferredChain,
} }
cert, err := client.Certificate.Obtain(request) cert, err := client.Certificate.Obtain(request)
if err != nil { 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 { 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 { 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) logger.Debugf("Certificates obtained for domains %+v", uncheckedDomains)
domain = types.Domain{Main: uncheckedDomains[0]}
if len(uncheckedDomains) > 1 { if len(uncheckedDomains) > 1 {
domain = types.Domain{Main: uncheckedDomains[0], SANs: uncheckedDomains[1:]} domain.SANs = uncheckedDomains[1:]
} else {
domain = types.Domain{Main: uncheckedDomains[0]}
} }
p.addCertificateForDomain(domain, cert.Certificate, cert.PrivateKey, tlsStore)
return cert, nil return domain, cert, nil
} }
func (p *Provider) removeResolvingDomains(resolvingDomains []string) { 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) { func (p *Provider) addCertificateForDomain(domain types.Domain, certificate, key []byte, tlsStore string) error {
p.certsChan <- &CertAndStore{Certificate: Certificate{Certificate: certificate, Key: key, Domain: domain}, Store: tlsStore} 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. // 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 return newDomains
} }
func (p *Provider) watchCertificate(ctx context.Context) { func (p *Provider) buildMessage() dynamic.Message {
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() {
conf := dynamic.Message{ conf := dynamic.Message{
ProviderName: p.ResolverName + ".acme", ProviderName: p.ResolverName + ".acme",
Configuration: &dynamic.Configuration{ Configuration: &dynamic.Configuration{
@ -670,41 +783,54 @@ func (p *Provider) refreshCertificates() {
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf) 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) { func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Duration) {
logger := log.FromContext(ctx) logger := log.FromContext(ctx)
logger.Info("Testing certificate renew...") logger.Info("Testing certificate renew...")
p.certificatesMu.RLock()
var certificates []*CertAndStore
for _, cert := range p.certificates { for _, cert := range p.certificates {
crt, err := getX509Certificate(ctx, &cert.Certificate) crt, err := getX509Certificate(ctx, &cert.Certificate)
// If there's an error, we assume the cert is broken, and needs update // 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)) { if err != nil || crt == nil || crt.NotAfter.Before(time.Now().Add(renewPeriod)) {
client, err := p.getClient() certificates = append(certificates, cert)
if err != nil { }
logger.Infof("Error renewing certificate from LE : %+v, %v", cert.Domain, err) }
continue
}
logger.Infof("Renewing certificate from LE : %+v", cert.Domain) p.certificatesMu.RUnlock()
renewedCert, err := client.Certificate.Renew(certificate.Resource{ for _, cert := range certificates {
Domain: cert.Domain.Main, client, err := p.getClient()
PrivateKey: cert.Key, if err != nil {
Certificate: cert.Certificate.Certificate, logger.WithError(err).Infof("Error renewing certificate from LE : %+v", cert.Domain)
}, true, oscpMustStaple, p.PreferredChain) continue
if err != nil { }
logger.Errorf("Error renewing certificate from LE: %v, %v", cert.Domain, err)
continue
}
if len(renewedCert.Certificate) == 0 || len(renewedCert.PrivateKey) == 0 { logger.Infof("Renewing certificate from LE : %+v", cert.Domain)
logger.Errorf("domains %v renew certificate with no value: %v", cert.Domain.ToStrArray(), cert)
continue
}
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) // Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates. // from static and dynamic provided certificates.
func (p *Provider) getUncheckedDomains(ctx context.Context, domainsToCheck []string, tlsStore string) []string { 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) 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 // Get ACME certificates
p.certificatesMu.RLock()
for _, cert := range p.certificates { for _, cert := range p.certificates {
allDomains = append(allDomains, strings.Join(cert.Domain.ToStrArray(), ",")) allDomains = append(allDomains, strings.Join(cert.Domain.ToStrArray(), ","))
} }
p.certificatesMu.RUnlock()
p.resolvingDomainsMutex.Lock()
defer p.resolvingDomainsMutex.Unlock()
// Get currently resolved domains // Get currently resolved domains
for domain := range p.resolvingDomains { 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) tlsCert, err := tls.X509KeyPair(cert.Certificate, cert.Key)
if err != nil { 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 return nil, err
} }
@ -769,43 +902,62 @@ func getX509Certificate(ctx context.Context, cert *Certificate) (*x509.Certifica
if crt == nil { if crt == nil {
crt, err = x509.ParseCertificate(tlsCert.Certificate[0]) crt, err = x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil { 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 return crt, err
} }
// getValidDomains checks if given domain is allowed to generate a ACME certificate and return it. // sanitizeDomains 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) { func (p *Provider) sanitizeDomains(ctx context.Context, domain types.Domain) ([]string, error) {
domains := domain.ToStrArray() domains := domain.ToStrArray()
if len(domains) == 0 { if len(domains) == 0 {
return nil, errors.New("unable to generate a certificate in ACME provider when no domain is given") return nil, errors.New("no domain was 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, ","))
}
} }
var cleanDomains []string var cleanDomains []string
for _, domain := range domains { for _, dom := range domains {
canonicalDomain := types.CanonicalDomain(domain) 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) cleanDomain := dns01.UnFqdn(canonicalDomain)
if canonicalDomain != cleanDomain { if canonicalDomain != cleanDomain {
log.FromContext(ctx).Warnf("FQDN detected, please remove the trailing dot: %s", canonicalDomain) log.FromContext(ctx).Warnf("FQDN detected, please remove the trailing dot: %s", canonicalDomain)
} }
cleanDomains = append(cleanDomains, cleanDomain) cleanDomains = append(cleanDomains, cleanDomain)
} }
return cleanDomains, nil 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 { func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool {
for _, certDomains := range existentDomains { for _, certDomains := range existentDomains {
for _, certDomain := range strings.Split(certDomains, ",") { 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 { testCases := []struct {
desc string desc string
domains types.Domain domains types.Domain
@ -214,7 +214,7 @@ func TestGetValidDomain(t *testing.T) {
desc: "no domain", desc: "no domain",
domains: types.Domain{}, domains: types.Domain{},
dnsChallenge: nil, dnsChallenge: nil,
expectedErr: "unable to generate a certificate in ACME provider when no domain is given", expectedErr: "no domain was given",
expectedDomains: nil, expectedDomains: nil,
}, },
{ {
@ -254,7 +254,7 @@ func TestGetValidDomain(t *testing.T) {
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}} 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 { if len(test.expectedErr) > 0 {
assert.EqualError(t, err, test.expectedErr, "Unexpected error.") 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 { if err := buildCertificates(client, id, t.Namespace, t.Spec.Certificates, tlsConfigs); err != nil {
logger.Errorf("Failed to load certificates: %v", err) logger.Errorf("Failed to load certificates: %v", err)
continue continue

View file

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

View file

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

View file

@ -11,7 +11,6 @@ import (
"strings" "strings"
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/tls/generate"
) )
var ( var (
@ -101,55 +100,8 @@ func (f FileOrContent) Read() ([]byte, error) {
return content, nil return content, nil
} }
// CreateTLSConfig creates a TLS config from Certificate structures. // AppendCertificate appends a Certificate to a certificates map keyed by store name.
func (c *Certificates) CreateTLSConfig(entryPointName string) (*tls.Config, error) { func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certificate, storeName string) 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 {
certContent, err := c.CertFile.Read() certContent, err := c.CertFile.Read()
if err != nil { if err != nil {
return fmt.Errorf("unable to read CertFile : %w", err) 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)) SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName))
} }
if parsedCert.DNSNames != nil { if parsedCert.DNSNames != nil {
sort.Strings(parsedCert.DNSNames)
for _, dnsName := range parsedCert.DNSNames { for _, dnsName := range parsedCert.DNSNames {
if dnsName != parsedCert.Subject.CommonName { if dnsName != parsedCert.Subject.CommonName {
SANs = append(SANs, strings.ToLower(dnsName)) 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, ",") certKey := strings.Join(SANs, ",")
certExists := false certExists := false
if certs[ep] == nil { if certs[storeName] == nil {
certs[ep] = make(map[string]*tls.Certificate) certs[storeName] = make(map[string]*tls.Certificate)
} else { } else {
for domains := range certs[ep] { for domains := range certs[storeName] {
if domains == certKey { if domains == certKey {
certExists = true certExists = true
break break
@ -199,10 +153,10 @@ func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certifi
} }
} }
if certExists { 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 { } else {
log.Debugf("Adding certificate for domain(s) %s", certKey) log.Debugf("Adding certificate for domain(s) %s", certKey)
certs[ep][certKey] = &tlsCert certs[storeName][certKey] = &tlsCert
} }
return err return err

View file

@ -22,8 +22,11 @@ type CertificateStore struct {
// NewCertificateStore create a store for dynamic certificates. // NewCertificateStore create a store for dynamic certificates.
func NewCertificateStore() *CertificateStore { func NewCertificateStore() *CertificateStore {
s := &safe.Safe{}
s.Set(make(map[string]*tls.Certificate))
return &CertificateStore{ return &CertificateStore{
DynamicCerts: &safe.Safe{}, DynamicCerts: s,
CertCache: cache.New(1*time.Hour, 10*time.Minute), CertCache: cache.New(1*time.Hour, 10*time.Minute),
} }
} }
@ -114,6 +117,45 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo)
return nil 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. // ResetCache clears the cache in the store.
func (c CertificateStore) ResetCache() { func (c CertificateStore) ResetCache() {
if c.CertCache != nil { if c.CertCache != nil {

View file

@ -1,5 +1,7 @@
package tls package tls
import "github.com/traefik/traefik/v2/pkg/types"
const certificateHeader = "-----BEGIN CERTIFICATE-----\n" const certificateHeader = "-----BEGIN CERTIFICATE-----\n"
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
@ -36,7 +38,18 @@ func (o *Options) SetDefaults() {
// Store holds the options for a given Store. // Store holds the options for a given Store.
type Store struct { 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 // +k8s:deepcopy-gen=true

View file

@ -6,8 +6,10 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"strings"
"sync" "sync"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/tlsalpn01" "github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/traefik/traefik/v2/pkg/log" "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.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) storesCertificates := make(map[string]map[string]*tls.Certificate)
for _, conf := range certs { for _, conf := range certs {
if len(conf.Stores) == 0 { 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.", 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.Certificate.GetTruncatedCertificateName())
} }
conf.Stores = []string{"default"} conf.Stores = []string{DefaultTLSStoreName}
} }
for _, store := range conf.Stores { for _, store := range conf.Stores {
ctxStore := log.With(ctx, log.Str(log.TLSStoreName, store)) 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) log.FromContext(ctxStore).Errorf("Unable to append certificate %s to store: %v", conf.Certificate.GetTruncatedCertificateName(), err)
} }
} }
} }
for storeName, certs := range storesCertificates { m.stores = make(map[string]*CertificateStore)
st, ok := m.stores[storeName]
if !ok { for storeName, storeConfig := range m.storesConfig {
st, _ = buildCertificateStore(context.Background(), Store{}, storeName) st := NewCertificateStore()
m.stores[storeName] = st 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. // Get gets the TLS configuration to use for a given store / configuration.
func (m *Manager) Get(storeName, configName string) (*tls.Config, error) { func (m *Manager) Get(storeName, configName string) (*tls.Config, error) {
m.lock.RLock() m.lock.RLock()
@ -234,32 +267,37 @@ func (m *Manager) GetStore(storeName string) *CertificateStore {
return m.getStore(storeName) return m.getStore(storeName)
} }
func buildCertificateStore(ctx context.Context, tlsStore Store, storename string) (*CertificateStore, error) { func getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*tls.Certificate, error) {
certificateStore := NewCertificateStore()
certificateStore.DynamicCerts.Set(make(map[string]*tls.Certificate))
if tlsStore.DefaultCertificate != nil { if tlsStore.DefaultCertificate != nil {
cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate) cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate)
if err != nil { 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 defaultCert, err := generate.DefaultCertificate()
// is a waste.
if storename == tlsalpn01.ACMETLS1Protocol {
return certificateStore, nil
}
log.FromContext(ctx).Debug("No default certificate, generating one")
cert, err := generate.DefaultCertificate()
if err != nil { 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. // creates a TLS config that allows terminating HTTPS for multiple domains using SNI.

View file

@ -29,6 +29,10 @@ THE SOFTWARE.
package tls 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CertAndStores) DeepCopyInto(out *CertAndStores) { func (in *CertAndStores) DeepCopyInto(out *CertAndStores) {
*out = *in *out = *in
@ -72,6 +76,27 @@ func (in *ClientAuth) DeepCopy() *ClientAuth {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Options) DeepCopyInto(out *Options) { func (in *Options) DeepCopyInto(out *Options) {
*out = *in *out = *in
@ -112,6 +137,11 @@ func (in *Store) DeepCopyInto(out *Store) {
*out = new(Certificate) *out = new(Certificate)
**out = **in **out = **in
} }
if in.DefaultGeneratedCert != nil {
in, out := &in.DefaultGeneratedCert, &out.DefaultGeneratedCert
*out = new(GeneratedCert)
(*in).DeepCopyInto(*out)
}
return return
} }