From 633ede5af6e0402f8cdf4156bdc56839cc0fb8bd Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 12 Jan 2026 02:44:08 +0100 Subject: [PATCH] nixos/acme: default to dynamic renewal days With Lego since 4.25.0 instead of --days we can pass --dynamic to pick the renewal date based on a fraction of its total validity duration. This provides a reasonable default that accomodates varying certificate validy durations we're going to be seeing through the profile option and LE's plans to reduce the default validity duration in multiple steps down to 45 days in 2028. This changes changes the default valid duration to null to enable dynamic renewal calculation. To that end the expiration skip function gained the ability to calculate the total and remaining duration and to apply the correct remainder based on the certificates total duration. --- .../manual/release-notes/rl-2605.section.md | 9 +++ nixos/modules/security/acme/default.nix | 64 ++++++++++++++----- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index a708c285091b..c2fdaaafc95e 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -191,6 +191,15 @@ See . - Wine has been updated to the 11.0 branch. Please check the [upstream announcement](https://gitlab.winehq.org/wine/wine/-/releases/wine-11.0) for more details. +- `security.acme` now defaults to a dynamic renewal duration, if + [security.acme.defaults.validMinDays](#opt-security.acme.defaults.validMinDays) + remains unset. This accomodates certificates with different ACME profile: + - For certificates with a total validity at or above 10 days renewal will happen + after two thirds of the lifetime has passed (e.g. a certificate valid for 90 + days renews once the validity falls below 30 days) + - For shortlived certificates with a total validty below 10 days renewal + will happen after half of the total lifetime has passed + - Cinnamon has been updated to 6.6, please check the [upstream announcement](https://www.linuxmint.com/rel_zena_whatsnew.php) for more details. - Budgie has been updated to 10.10, please check the [upstream announcement](https://buddiesofbudgie.org/blog/budgie-10-10-released) for more details. diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix index 1b262e285e19..d70bb5ace394 100644 --- a/nixos/modules/security/acme/default.nix +++ b/nixos/modules/security/acme/default.nix @@ -523,20 +523,42 @@ let [[ -e $pem ]] - expiration_line="$( - set -euxo pipefail - openssl x509 -noout -enddate <"$pem" \ - | grep notAfter \ - | sed -e 's/^notAfter=//' - )" - [[ -n "$expiration_line" ]] + # Read certificate valid duration + not_before=$(openssl x509 -in "$pem" -noout -startdate | cut -d= -f2) + not_after=$(openssl x509 -in "$pem" -noout -enddate | cut -d= -f2) - expiration_date="$(date -d "$expiration_line" +%s)" - now="$(date +%s)" - expiration_s=$((expiration_date - now)) - expiration_days=$((expiration_s / (3600 * 24))) # rounds down + # Convert timestamp to seconds since epoch + not_before_epoch=$(date -d "$not_before" +%s) + not_after_epoch=$(date -d "$not_after" +%s) + now_epoch=$(date +%s) - [[ $expiration_days -gt ${toString data.validMinDays} ]] + # Determine total and remaining duration + total_duration=$((not_after_epoch - not_before_epoch)) + remaining_duration=$((not_after_epoch - now_epoch)) + + ${ + if (data.validMinDays != null) then + '' + # Convert to days and round down + total_days=$((total_duration / 86400)) + remaining_days=$((remaining_duration / 86400)) + + [[ $remaining_days -gt ${toString data.validMinDays} ]] + '' + else + '' + # Recreate --dynamic logic from lego + if (( total_duration < 864000 )); then + # 1/2 for shortlived certificates with total lifetime < 10 days + threshold=$(( total_duration / 2)) + else + # 1/3 for longer validity durations with total lifetime >= 10 days + threshold=$(( total_duration / 3)) + fi + + [[ $remaining_duration -gt $threshold ]] + '' + } } echo '${domainHash}' > domainhash.txt @@ -548,9 +570,11 @@ let # Even if a cert is not expired, it may be revoked by the CA. # Try to renew, and silently fail if the cert is not expired. # Avoids #85794 and resolves #129838 - if ! lego ${renewOpts} --days ${toString data.validMinDays}; then + if ! lego ${renewOpts} ${ + if data.validMinDays != null then "--days ${toString data.validMinDays}" else "--dynamic" + }; then if is_expiration_skippable out/full.pem; then - echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days" + echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't due yet" else # High number to avoid Systemd reserved codes. exit 11 @@ -626,9 +650,15 @@ let options = { validMinDays = lib.mkOption { - type = lib.types.int; - inherit (defaultAndText "validMinDays" 30) default defaultText; - description = "Minimum remaining validity before renewal in days."; + type = lib.types.nullOr lib.types.int; + default = null; + description = '' + Minimum remaining validity before renewal in days. + + If unset, the renewal time is calculated dynamically: + - for regular certificates, renewal occurs when less than one-third of the lifetime remains + - for short-lived certificates, renewal occurs when less than half of the lifetime remains + ''; }; renewInterval = lib.mkOption {