From 929d0e502ad895737d7b0bafab18c63fb1d44cc1 Mon Sep 17 00:00:00 2001 From: mehrdadbn9 <80095851+mehrdadbn9@users.noreply.github.com> Date: Sat, 14 Feb 2026 01:17:02 +0330 Subject: [PATCH] caddyfile: Add `renewal_window_ratio` global option and `tls` subdirective (#7473) * caddyfile: Add renewal_window_ratio global option Adds support for configuring the TLS certificate renewal window ratio directly in the Caddyfile global options block. This allows users to customize when certificates should be renewed without needing to use JSON configuration. Example usage: { renewal_window_ratio 0.1666 } Fixes #7467 * caddyfile: Add renewal_window_ratio to tls directive and tests Adds support for renewal_window_ratio in the tls directive (not just global options) and adds caddyfile adapt tests for both the global option and tls directive. * fix: inherit global renewal_window_ratio in site policies * fix: correct test expected output for policy consolidation * fix: properly inherit global renewal_window_ratio without removing other code --- caddyconfig/httpcaddyfile/builtins.go | 24 +++++++ caddyconfig/httpcaddyfile/options.go | 20 ++++++ caddyconfig/httpcaddyfile/tlsapp.go | 13 +++- .../renewal_window_ratio_global.caddyfiletest | 41 ++++++++++++ ...l_window_ratio_tls_directive.caddyfiletest | 63 +++++++++++++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 caddytest/integration/caddyfile_adapt/renewal_window_ratio_global.caddyfiletest create mode 100644 caddytest/integration/caddyfile_adapt/renewal_window_ratio_tls_directive.caddyfiletest 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 + } + ] + } + } + } +}