diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index cf8ad044f..a7bb3b1de 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -113,6 +113,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // issuer [...] // get_certificate [...] // insecure_secrets_log +// renewal_window_ratio // } func parseTLS(h Helper) ([]ConfigValue, error) { h.Next() // consume directive name @@ -129,6 +130,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var onDemand bool var reusePrivateKeys bool var forceAutomate bool + var renewalWindowRatio float64 // Track which DNS challenge options are set var dnsOptionsSet []string @@ -473,6 +475,20 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } cp.InsecureSecretsLog = h.Val() + case "renewal_window_ratio": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + ratio, err := strconv.ParseFloat(arg[0], 64) + if err != nil { + return nil, h.Errf("parsing renewal_window_ratio: %v", err) + } + if ratio <= 0 || ratio >= 1 { + return nil, h.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)") + } + renewalWindowRatio = ratio + default: return nil, h.Errf("unknown subdirective: %s", h.Val()) } @@ -597,6 +613,14 @@ func parseTLS(h Helper) ([]ConfigValue, error) { }) } + // renewal window ratio + if renewalWindowRatio > 0 { + configVals = append(configVals, ConfigValue{ + Class: "tls.renewal_window_ratio", + Value: renewalWindowRatio, + }) + } + // if enabled, the names in the site addresses will be // added to the automation policies if forceAutomate { diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 82d368db1..f985cff9e 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -65,6 +65,7 @@ func init() { RegisterGlobalOption("persist_config", parseOptPersistConfig) RegisterGlobalOption("dns", parseOptDNS) RegisterGlobalOption("ech", parseOptECH) + RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio) } func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } @@ -624,3 +625,22 @@ func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) { return ech, nil } + +func parseOptRenewalWindowRatio(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + if !d.Next() { + return 0, d.ArgErr() + } + val := d.Val() + ratio, err := strconv.ParseFloat(val, 64) + if err != nil { + return 0, d.Errf("parsing renewal_window_ratio: %v", err) + } + if ratio <= 0 || ratio >= 1 { + return 0, d.Errf("renewal_window_ratio must be between 0 and 1 (exclusive)") + } + if d.Next() { + return 0, d.ArgErr() + } + return ratio, nil +} diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 8b34cbc97..e1e37a84b 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -143,6 +143,12 @@ func (st ServerType) buildTLSApp( ap.KeyType = keyTypeVals[0].Value.(string) } + if renewalWindowRatioVals, ok := sblock.pile["tls.renewal_window_ratio"]; ok { + ap.RenewalWindowRatio = renewalWindowRatioVals[0].Value.(float64) + } else if globalRenewalWindowRatio, ok := options["renewal_window_ratio"]; ok { + ap.RenewalWindowRatio = globalRenewalWindowRatio.(float64) + } + // certificate issuers if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok { var issuers []certmagic.Issuer @@ -607,7 +613,8 @@ func newBaseAutomationPolicy( _, hasLocalCerts := options["local_certs"] keyType, hasKeyType := options["key_type"] ocspStapling, hasOCSPStapling := options["ocsp_stapling"] - hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling + renewalWindowRatio, hasRenewalWindowRatio := options["renewal_window_ratio"] + hasGlobalAutomationOpts := hasIssuers || hasLocalCerts || hasKeyType || hasOCSPStapling || hasRenewalWindowRatio globalACMECA := options["acme_ca"] globalACMECARoot := options["acme_ca_root"] @@ -654,6 +661,10 @@ func newBaseAutomationPolicy( ap.OCSPOverrides = ocspConfig.ResponderOverrides } + if hasRenewalWindowRatio { + ap.RenewalWindowRatio = renewalWindowRatio.(float64) + } + return ap, nil } diff --git a/caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest new file mode 100644 index 000000000..f6af0ce72 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest @@ -0,0 +1,41 @@ +{ + renewal_window_ratio 0.1666 +} + +example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "renewal_window_ratio": 0.1666 + } + ] + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest new file mode 100644 index 000000000..82c43f2a5 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest @@ -0,0 +1,63 @@ +{ + renewal_window_ratio 0.1666 +} + +a.example.com { + tls { + renewal_window_ratio 0.25 + } +} + +b.example.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "a.example.com" + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "b.example.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "a.example.com" + ], + "renewal_window_ratio": 0.25 + }, + { + "renewal_window_ratio": 0.1666 + } + ] + } + } + } +}