feat: allow configuration of ACME certificates duration

This commit is contained in:
Pablo Montepagano 2021-11-10 08:06:09 -03:00 committed by GitHub
parent 1f17731369
commit 0a5c9095ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 183 additions and 19 deletions

View file

@ -140,7 +140,11 @@ Please check the [configuration examples below](#configuration-examples) for mor
Traefik automatically tracks the expiry date of ACME certificates it generates.
If there are less than 30 days remaining before the certificate expires, Traefik will attempt to renew it automatically.
By default, Traefik manages 90 days certificates,
and starts to renew certificates 30 days before their expiry.
When using a certificates resolver that issues certificates with custom durations,
one can configure the certificates' duration with the [`certificatesDuration`](#certificatesduration) option.
!!! info ""
Certificates that are no longer used may still be renewed, as Traefik does not currently check if the certificate is being used before renewing.
@ -533,6 +537,50 @@ docker run -v "/my/host/acme:/etc/traefik/acme" traefik
!!! warning
For concurrency reasons, this file cannot be shared across multiple instances of Traefik.
### `certificatesDuration`
_Optional, Default=2160_
The `certificatesDuration` option defines the certificates' duration in hours.
It defaults to `2160` (90 days) to follow Let's Encrypt certificates' duration.
!!! warning "Traefik cannot manage certificates with a duration lower than 1 hour."
```yaml tab="File (YAML)"
certificatesResolvers:
myresolver:
acme:
# ...
certificatesDuration: 72
# ...
```
```toml tab="File (TOML)"
[certificatesResolvers.myresolver.acme]
# ...
certificatesDuration=72
# ...
```
```bash tab="CLI"
# ...
--certificatesresolvers.myresolver.acme.certificatesduration=72
# ...
```
`certificatesDuration` is used to calculate two durations:
- `Renew Period`: the period before the end of the certificate duration, during which the certificate should be renewed.
- `Renew Interval`: the interval between renew attempts.
| Certificate Duration | Renew Period | Renew Interval |
|----------------------|-------------------|-------------------------|
| >= 1 year | 4 months | 1 week |
| >= 90 days | 30 days | 1 day |
| >= 7 days | 1 day | 1 hour |
| >= 24 hours | 6 hours | 10 min |
| < 24 hours | 20 min | 1 min |
### `preferredChain`
_Optional, Default=""_

View file

@ -22,6 +22,14 @@
#
# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
# The certificates' duration in hours.
# It defaults to 2160 (90 days) to follow Let's Encrypt certificates' duration.
#
# Optional
# Default: 2160
#
# certificatesDuration=2160
# Preferred chain to use.
#
# If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name.

View file

@ -21,6 +21,14 @@
#
--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
# The certificates' duration in hours.
# It defaults to 2160 (90 days) to follow Let's Encrypt certificates' duration.
#
# Optional
# Default: 2160
#
--certificatesresolvers.myresolver.acme.certificatesDuration=2160
# Preferred chain to use.
#
# If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name.

View file

@ -24,6 +24,14 @@ certificatesResolvers:
#
# caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
# The certificates' duration in hours.
# It defaults to 2160 (90 days) to follow Let's Encrypt certificates' duration.
#
# Optional
# Default: 2160
#
# certificatesDuration: 2160
# Preferred chain to use.
#
# If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name.

View file

@ -54,6 +54,9 @@ Certificates resolvers configuration. (Default: ```false```)
`--certificatesresolvers.<name>.acme.caserver`:
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
`--certificatesresolvers.<name>.acme.certificatesduration`:
Certificates' duration in hours. (Default: ```2160```)
`--certificatesresolvers.<name>.acme.dnschallenge`:
Activate DNS-01 Challenge. (Default: ```false```)

View file

@ -54,6 +54,9 @@ Certificates resolvers configuration. (Default: ```false```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CASERVER`:
CA server to use. (Default: ```https://acme-v02.api.letsencrypt.org/directory```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_CERTIFICATESDURATION`:
Certificates' duration in hours. (Default: ```2160```)
`TRAEFIK_CERTIFICATESRESOLVERS_<NAME>_ACME_DNSCHALLENGE`:
Activate DNS-01 Challenge. (Default: ```false```)

View file

@ -358,6 +358,7 @@
[certificatesResolvers.CertificateResolver0.acme]
email = "foobar"
caServer = "foobar"
certificatesDuration = 2160
preferredChain = "foobar"
storage = "foobar"
keyType = "foobar"
@ -376,6 +377,7 @@
[certificatesResolvers.CertificateResolver1.acme]
email = "foobar"
caServer = "foobar"
certificatesDuration = 2160
preferredChain = "foobar"
storage = "foobar"
keyType = "foobar"

View file

@ -376,6 +376,7 @@ certificatesResolvers:
acme:
email: foobar
caServer: foobar
certificatesDuration: 2160
preferredChain: foobar
storage: foobar
keyType: foobar
@ -396,6 +397,7 @@ certificatesResolvers:
acme:
email: foobar
caServer: foobar
certificatesDuration: 2160
preferredChain: foobar
storage: foobar
keyType: foobar

View file

@ -916,6 +916,7 @@ func TestDo_staticConfiguration(t *testing.T) {
ACME: &acme.Configuration{
Email: "acme Email",
CAServer: "CAServer",
CertificatesDuration: 42,
PreferredChain: "foobar",
Storage: "Storage",
KeyType: "MyKeyType",

View file

@ -426,6 +426,7 @@
"preferredChain": "foobar",
"storage": "Storage",
"keyType": "MyKeyType",
"certificatesDuration": 42,
"dnsChallenge": {
"provider": "DNSProvider",
"delayBeforeCheck": "42ns",

View file

@ -39,6 +39,7 @@ type Configuration struct {
Storage string `description:"Storage to use." json:"storage,omitempty" toml:"storage,omitempty" yaml:"storage,omitempty" export:"true"`
KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'." json:"keyType,omitempty" toml:"keyType,omitempty" yaml:"keyType,omitempty" export:"true"`
EAB *EAB `description:"External Account Binding to use." json:"eab,omitempty" toml:"eab,omitempty" yaml:"eab,omitempty"`
CertificatesDuration int `description:"Certificates' duration in hours." json:"certificatesDuration,omitempty" toml:"certificatesDuration,omitempty" yaml:"certificatesDuration,omitempty" export:"true"`
DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
@ -50,6 +51,7 @@ func (a *Configuration) SetDefaults() {
a.CAServer = lego.LEDirectoryProduction
a.Storage = "acme.json"
a.KeyType = "RSA4096"
a.CertificatesDuration = 3 * 30 * 24 // 90 Days
}
// CertAndStore allows mapping a TLS certificate to a TLS store.
@ -133,6 +135,10 @@ func (p *Provider) Init() error {
return errors.New("unable to initialize ACME provider with no storage location for the certificates")
}
if p.CertificatesDuration < 1 {
return errors.New("cannot manage certificates with duration lower than 1 hour")
}
var err error
p.account, err = p.Store.GetAccount(p.ResolverName)
if err != nil {
@ -177,7 +183,9 @@ func isAccountMatchingCaServer(ctx context.Context, accountURI, serverURI string
// Provide allows the file provider to provide configurations to traefik
// using the given Configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
ctx := log.With(context.Background(), log.Str(log.ProviderName, p.ResolverName+".acme"))
ctx := log.With(context.Background(),
log.Str(log.ProviderName, p.ResolverName+".acme"),
log.Str("ACME CA", p.Configuration.CAServer))
p.pool = pool
@ -187,14 +195,18 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
p.configurationChan = configurationChan
p.refreshCertificates()
p.renewCertificates(ctx)
renewPeriod, renewInterval := getCertificateRenewDurations(p.CertificatesDuration)
log.FromContext(ctx).Debugf("Attempt to renew certificates %q before expiry and check every %q",
renewPeriod, renewInterval)
ticker := time.NewTicker(24 * time.Hour)
p.renewCertificates(ctx, renewPeriod)
ticker := time.NewTicker(renewInterval)
pool.GoCtx(func(ctxPool context.Context) {
for {
select {
case <-ticker.C:
p.renewCertificates(ctx)
p.renewCertificates(ctx, renewPeriod)
case <-ctxPool.Done():
ticker.Stop()
return
@ -515,6 +527,24 @@ func (p *Provider) addCertificateForDomain(domain types.Domain, certificate, key
p.certsChan <- &CertAndStore{Certificate: Certificate{Certificate: certificate, Key: key, Domain: domain}, Store: tlsStore}
}
// getCertificateRenewDurations returns renew durations calculated from the given certificatesDuration in hours.
// The first (RenewPeriod) is the period before the end of the certificate duration, during which the certificate should be renewed.
// The second (RenewInterval) is the interval between renew attempts.
func getCertificateRenewDurations(certificatesDuration int) (time.Duration, time.Duration) {
switch {
case certificatesDuration >= 265*24: // >= 1 year
return 4 * 30 * 24 * time.Hour, 7 * 24 * time.Hour // 4 month, 1 week
case certificatesDuration >= 3*30*24: // >= 90 days
return 30 * 24 * time.Hour, 24 * time.Hour // 30 days, 1 day
case certificatesDuration >= 7*24: // >= 7 days
return 24 * time.Hour, time.Hour // 1 days, 1 hour
case certificatesDuration >= 24: // >= 1 days
return 6 * time.Hour, 10 * time.Minute // 6 hours, 10 minutes
default:
return 20 * time.Minute, time.Minute
}
}
// deleteUnnecessaryDomains deletes from the configuration :
// - Duplicated domains
// - Domains which are checked by wildcard domain.
@ -637,15 +667,14 @@ func (p *Provider) refreshCertificates() {
p.configurationChan <- conf
}
func (p *Provider) renewCertificates(ctx context.Context) {
func (p *Provider) renewCertificates(ctx context.Context, renewPeriod time.Duration) {
logger := log.FromContext(ctx)
logger.Info("Testing certificate renew...")
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
// <= 30 days left, renew certificate
if err != nil || crt == nil || crt.NotAfter.Before(time.Now().Add(24*30*time.Hour)) {
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)

View file

@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"testing"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/stretchr/testify/assert"
@ -592,3 +593,53 @@ func TestInitAccount(t *testing.T) {
})
}
}
func Test_getCertificateRenewDurations(t *testing.T) {
testCases := []struct {
desc string
certificatesDurations int
expectRenewPeriod time.Duration
expectRenewInterval time.Duration
}{
{
desc: "Less than 24 Hours certificates: 20 minutes renew period, 1 minutes renew interval",
certificatesDurations: 1,
expectRenewPeriod: time.Minute * 20,
expectRenewInterval: time.Minute,
},
{
desc: "1 Year certificates: 2 months renew period, 1 week renew interval",
certificatesDurations: 24 * 365,
expectRenewPeriod: time.Hour * 24 * 30 * 4,
expectRenewInterval: time.Hour * 24 * 7,
},
{
desc: "90 Days certificates: 30 days renew period, 1 day renew interval",
certificatesDurations: 24 * 90,
expectRenewPeriod: time.Hour * 24 * 30,
expectRenewInterval: time.Hour * 24,
},
{
desc: "7 Days certificates: 1 days renew period, 1 hour renew interval",
certificatesDurations: 24 * 7,
expectRenewPeriod: time.Hour * 24,
expectRenewInterval: time.Hour,
},
{
desc: "24 Hours certificates: 6 hours renew period, 10 minutes renew interval",
certificatesDurations: 24,
expectRenewPeriod: time.Hour * 6,
expectRenewInterval: time.Minute * 10,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
renewPeriod, renewInterval := getCertificateRenewDurations(test.certificatesDurations)
assert.Equal(t, test.expectRenewPeriod, renewPeriod)
assert.Equal(t, test.expectRenewInterval, renewInterval)
})
}
}