feat: allow configuration of ACME certificates duration
This commit is contained in:
parent
1f17731369
commit
0a5c9095ac
12 changed files with 183 additions and 19 deletions
|
@ -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=""_
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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```)
|
||||
|
||||
|
|
|
@ -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```)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -914,11 +914,12 @@ func TestDo_staticConfiguration(t *testing.T) {
|
|||
config.CertificatesResolvers = map[string]static.CertificateResolver{
|
||||
"CertificateResolver0": {
|
||||
ACME: &acme.Configuration{
|
||||
Email: "acme Email",
|
||||
CAServer: "CAServer",
|
||||
PreferredChain: "foobar",
|
||||
Storage: "Storage",
|
||||
KeyType: "MyKeyType",
|
||||
Email: "acme Email",
|
||||
CAServer: "CAServer",
|
||||
CertificatesDuration: 42,
|
||||
PreferredChain: "foobar",
|
||||
Storage: "Storage",
|
||||
KeyType: "MyKeyType",
|
||||
DNSChallenge: &acme.DNSChallenge{
|
||||
Provider: "DNSProvider",
|
||||
DelayBeforeCheck: 42,
|
||||
|
|
|
@ -426,6 +426,7 @@
|
|||
"preferredChain": "foobar",
|
||||
"storage": "Storage",
|
||||
"keyType": "MyKeyType",
|
||||
"certificatesDuration": 42,
|
||||
"dnsChallenge": {
|
||||
"provider": "DNSProvider",
|
||||
"delayBeforeCheck": "42ns",
|
||||
|
|
|
@ -33,12 +33,13 @@ var oscpMustStaple = false
|
|||
|
||||
// Configuration holds ACME configuration provided by users.
|
||||
type Configuration struct {
|
||||
Email string `description:"Email address used for registration." json:"email,omitempty" toml:"email,omitempty" yaml:"email,omitempty"`
|
||||
CAServer string `description:"CA server to use." json:"caServer,omitempty" toml:"caServer,omitempty" yaml:"caServer,omitempty"`
|
||||
PreferredChain string `description:"Preferred chain to use." json:"preferredChain,omitempty" toml:"preferredChain,omitempty" yaml:"preferredChain,omitempty" export:"true"`
|
||||
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"`
|
||||
Email string `description:"Email address used for registration." json:"email,omitempty" toml:"email,omitempty" yaml:"email,omitempty"`
|
||||
CAServer string `description:"CA server to use." json:"caServer,omitempty" toml:"caServer,omitempty" yaml:"caServer,omitempty"`
|
||||
PreferredChain string `description:"Preferred chain to use." json:"preferredChain,omitempty" toml:"preferredChain,omitempty" yaml:"preferredChain,omitempty" export:"true"`
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue