Fix ACME certificate for wildcard and root domains
This commit is contained in:
parent
838dd8c19f
commit
402f7011d4
2 changed files with 136 additions and 4 deletions
|
@ -13,6 +13,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/BurntSushi/ty/fun"
|
"github.com/BurntSushi/ty/fun"
|
||||||
|
"github.com/cenk/backoff"
|
||||||
"github.com/containous/flaeg"
|
"github.com/containous/flaeg"
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/log"
|
||||||
"github.com/containous/traefik/rules"
|
"github.com/containous/traefik/rules"
|
||||||
|
@ -74,6 +75,8 @@ type Certificate struct {
|
||||||
type DNSChallenge struct {
|
type DNSChallenge struct {
|
||||||
Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."`
|
Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."`
|
||||||
DelayBeforeCheck flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
|
DelayBeforeCheck flaeg.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
|
||||||
|
preCheckTimeout time.Duration
|
||||||
|
preCheckInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPChallenge contains HTTP challenge Configuration
|
// HTTPChallenge contains HTTP challenge Configuration
|
||||||
|
@ -262,6 +265,16 @@ func (p *Provider) getClient() (*acme.Client, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same default values than LEGO
|
||||||
|
p.DNSChallenge.preCheckTimeout = 60 * time.Second
|
||||||
|
p.DNSChallenge.preCheckInterval = 2 * time.Second
|
||||||
|
|
||||||
|
// Set the precheck timeout into the DNSChallenge provider
|
||||||
|
if challengeProviderTimeout, ok := provider.(acme.ChallengeProviderTimeout); ok {
|
||||||
|
p.DNSChallenge.preCheckTimeout, p.DNSChallenge.preCheckInterval = challengeProviderTimeout.Timeout()
|
||||||
|
}
|
||||||
|
|
||||||
} else if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 {
|
} else if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 {
|
||||||
log.Debug("Using HTTP Challenge provider.")
|
log.Debug("Using HTTP Challenge provider.")
|
||||||
|
|
||||||
|
@ -361,13 +374,20 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
|
||||||
return nil, fmt.Errorf("cannot get ACME client %v", err)
|
return nil, fmt.Errorf("cannot get ACME client %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var certificate *acme.CertificateResource
|
||||||
bundle := true
|
bundle := true
|
||||||
|
if p.useCertificateWithRetry(uncheckedDomains) {
|
||||||
certificate, err := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple)
|
certificate, err = obtainCertificateWithRetry(domains, client, p.DNSChallenge.preCheckTimeout, p.DNSChallenge.preCheckInterval, bundle)
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, fmt.Errorf("cannot obtain certificates: %+v", err)
|
certificate, err = client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to generate a certificate for the domains %v: %v", uncheckedDomains, err)
|
||||||
|
}
|
||||||
|
if certificate == nil {
|
||||||
|
return nil, fmt.Errorf("domains %v do not generate a certificate", uncheckedDomains)
|
||||||
|
}
|
||||||
if len(certificate.Certificate) == 0 || len(certificate.PrivateKey) == 0 {
|
if len(certificate.Certificate) == 0 || len(certificate.PrivateKey) == 0 {
|
||||||
return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, certificate)
|
return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, certificate)
|
||||||
}
|
}
|
||||||
|
@ -384,6 +404,60 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
|
||||||
return certificate, nil
|
return certificate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) useCertificateWithRetry(domains []string) bool {
|
||||||
|
// Check if we can use the retry mechanism only if we use the DNS Challenge and if is there are at least 2 domains to check
|
||||||
|
if p.DNSChallenge != nil && len(domains) > 1 {
|
||||||
|
rootDomain := ""
|
||||||
|
for _, searchWildcardDomain := range domains {
|
||||||
|
// Search a wildcard domain if not already found
|
||||||
|
if len(rootDomain) == 0 && strings.HasPrefix(searchWildcardDomain, "*.") {
|
||||||
|
rootDomain = strings.TrimPrefix(searchWildcardDomain, "*.")
|
||||||
|
if len(rootDomain) > 0 {
|
||||||
|
// Look for a root domain which matches the wildcard domain
|
||||||
|
for _, searchRootDomain := range domains {
|
||||||
|
if rootDomain == searchRootDomain {
|
||||||
|
// If the domains list contains a wildcard domain and its root domain, we can use the retry mechanism to obtain the certificate
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// There is only one wildcard domain in the slice, if its root domain has not been found, the retry mechanism does not have to be used
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func obtainCertificateWithRetry(domains []string, client *acme.Client, timeout, interval time.Duration, bundle bool) (*acme.CertificateResource, error) {
|
||||||
|
var certificate *acme.CertificateResource
|
||||||
|
var err error
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
certificate, err = client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
log.Errorf("Error obtaining certificate retrying in %s", time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a retry backOff to let LEGO tries twice to obtain a certificate for both wildcard and root domain
|
||||||
|
ebo := backoff.NewExponentialBackOff()
|
||||||
|
ebo.MaxElapsedTime = 2 * timeout
|
||||||
|
ebo.MaxInterval = interval
|
||||||
|
rbo := backoff.WithMaxRetries(ebo, 2)
|
||||||
|
|
||||||
|
err = backoff.RetryNotify(safe.OperationWithRecover(operation), rbo, notify)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error obtaining certificate: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
func dnsOverrideDelay(delay flaeg.Duration) error {
|
func dnsOverrideDelay(delay flaeg.Duration) error {
|
||||||
if delay == 0 {
|
if delay == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -504,3 +504,61 @@ func TestIsAccountMatchingCaServer(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUseBackOffToObtainCertificate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
domains []string
|
||||||
|
dnsChallenge *DNSChallenge
|
||||||
|
expectedResponse bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "only one single domain",
|
||||||
|
domains: []string{"acme.wtf"},
|
||||||
|
dnsChallenge: &DNSChallenge{},
|
||||||
|
expectedResponse: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "only one wildcard domain",
|
||||||
|
domains: []string{"*.acme.wtf"},
|
||||||
|
dnsChallenge: &DNSChallenge{},
|
||||||
|
expectedResponse: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard domain with no root domain",
|
||||||
|
domains: []string{"*.acme.wtf", "foo.acme.wtf", "bar.acme.wtf", "foo.bar"},
|
||||||
|
dnsChallenge: &DNSChallenge{},
|
||||||
|
expectedResponse: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard and root domain",
|
||||||
|
domains: []string{"*.acme.wtf", "foo.acme.wtf", "bar.acme.wtf", "acme.wtf"},
|
||||||
|
dnsChallenge: &DNSChallenge{},
|
||||||
|
expectedResponse: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "wildcard and root domain but no DNS challenge",
|
||||||
|
domains: []string{"*.acme.wtf", "acme.wtf"},
|
||||||
|
dnsChallenge: nil,
|
||||||
|
expectedResponse: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "two wildcard domains (must never happen)",
|
||||||
|
domains: []string{"*.acme.wtf", "*.bar.foo"},
|
||||||
|
dnsChallenge: nil,
|
||||||
|
expectedResponse: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}}
|
||||||
|
|
||||||
|
actualResponse := acmeProvider.useCertificateWithRetry(test.domains)
|
||||||
|
assert.Equal(t, test.expectedResponse, actualResponse, "unexpected response to use backOff")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue