From a30084dd1ecccb3912ee63a292cd71ae146c6e4b Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Tue, 5 May 2026 17:34:14 +0200 Subject: [PATCH 01/11] added support for le shortlived profile --- .../s6-rc.d/init-certbot-config/run | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run index 5e34aaae..28473339 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run @@ -12,12 +12,13 @@ EXTRA_DOMAINS=${EXTRA_DOMAINS}\\n\ ONLY_SUBDOMAINS=${ONLY_SUBDOMAINS}\\n\ VALIDATION=${VALIDATION}\\n\ CERTPROVIDER=${CERTPROVIDER}\\n\ +CERT_PROFILE=${CERT_PROFILE}\\n\ DNSPLUGIN=${DNSPLUGIN}\\n\ EMAIL=${EMAIL}\\n\ STAGING=${STAGING}\\n" # Sanitize variables -SANED_VARS=(DNSPLUGIN EMAIL EXTRA_DOMAINS ONLY_SUBDOMAINS STAGING SUBDOMAINS URL VALIDATION CERTPROVIDER) +SANED_VARS=(DNSPLUGIN EMAIL EXTRA_DOMAINS ONLY_SUBDOMAINS STAGING SUBDOMAINS URL VALIDATION CERTPROVIDER CERT_PROFILE) for i in "${SANED_VARS[@]}"; do export echo "${i}"="${!i//\"/}" export echo "${i}"="$(echo "${!i}" | tr '[:upper:]' '[:lower:]')" @@ -80,7 +81,7 @@ if [[ -f "/config/donoteditthisfile.conf" ]]; then mv /config/donoteditthisfile.conf /config/.donoteditthisfile.conf fi if [[ ! -f "/config/.donoteditthisfile.conf" ]]; then - echo -e "ORIGURL=\"${URL}\" ORIGSUBDOMAINS=\"${SUBDOMAINS}\" ORIGONLY_SUBDOMAINS=\"${ONLY_SUBDOMAINS}\" ORIGEXTRA_DOMAINS=\"${EXTRA_DOMAINS}\" ORIGVALIDATION=\"${VALIDATION}\" ORIGDNSPLUGIN=\"${DNSPLUGIN}\" ORIGPROPAGATION=\"${PROPAGATION}\" ORIGSTAGING=\"${STAGING}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGEMAIL=\"${EMAIL}\"" >/config/.donoteditthisfile.conf + echo -e "ORIGURL=\"${URL}\" ORIGSUBDOMAINS=\"${SUBDOMAINS}\" ORIGONLY_SUBDOMAINS=\"${ONLY_SUBDOMAINS}\" ORIGEXTRA_DOMAINS=\"${EXTRA_DOMAINS}\" ORIGVALIDATION=\"${VALIDATION}\" ORIGDNSPLUGIN=\"${DNSPLUGIN}\" ORIGPROPAGATION=\"${PROPAGATION}\" ORIGSTAGING=\"${STAGING}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGEMAIL=\"${EMAIL}\" ORIGCERT_PROFILE=\"${CERT_PROFILE}\"" >/config/.donoteditthisfile.conf echo "Created .donoteditthisfile.conf" fi @@ -186,7 +187,8 @@ if [[ ! "${URL}" = "${ORIGURL}" ]] || [[ ! "${DNSPLUGIN}" = "${ORIGDNSPLUGIN}" ]] || [[ ! "${PROPAGATION}" = "${ORIGPROPAGATION}" ]] || [[ ! "${STAGING}" = "${ORIGSTAGING}" ]] || - [[ ! "${CERTPROVIDER}" = "${ORIGCERTPROVIDER}" ]]; then + [[ ! "${CERTPROVIDER}" = "${ORIGCERTPROVIDER}" ]] || + [[ ! "${CERT_PROFILE}" = "${ORIGCERT_PROFILE}" ]]; then echo "Different validation parameters entered than what was used before. Revoking and deleting existing certificate, and an updated one will be created" if [[ "${ORIGCERTPROVIDER}" = "zerossl" ]]; then REV_ACMESERVER=("https://acme.zerossl.com/v2/DV90") @@ -204,7 +206,7 @@ if [[ ! "${URL}" = "${ORIGURL}" ]] || fi # saving new variables -echo -e "ORIGURL=\"${URL}\" ORIGSUBDOMAINS=\"${SUBDOMAINS}\" ORIGONLY_SUBDOMAINS=\"${ONLY_SUBDOMAINS}\" ORIGEXTRA_DOMAINS=\"${EXTRA_DOMAINS}\" ORIGVALIDATION=\"${VALIDATION}\" ORIGDNSPLUGIN=\"${DNSPLUGIN}\" ORIGPROPAGATION=\"${PROPAGATION}\" ORIGSTAGING=\"${STAGING}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGEMAIL=\"${EMAIL}\"" >/config/.donoteditthisfile.conf +echo -e "ORIGURL=\"${URL}\" ORIGSUBDOMAINS=\"${SUBDOMAINS}\" ORIGONLY_SUBDOMAINS=\"${ONLY_SUBDOMAINS}\" ORIGEXTRA_DOMAINS=\"${EXTRA_DOMAINS}\" ORIGVALIDATION=\"${VALIDATION}\" ORIGDNSPLUGIN=\"${DNSPLUGIN}\" ORIGPROPAGATION=\"${PROPAGATION}\" ORIGSTAGING=\"${STAGING}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGEMAIL=\"${EMAIL}\" ORIGCERT_PROFILE=\"${CERT_PROFILE}\"" >/config/.donoteditthisfile.conf # Check if the cert is using the old LE root cert, revoke and regen if necessary if [[ -f "/config/keys/letsencrypt/chain.pem" ]] && { [[ "${CERTPROVIDER}" == "letsencrypt" ]] || [[ "${CERTPROVIDER}" == "" ]]; } && [[ "${STAGING}" != "true" ]] && ! openssl x509 -in /config/keys/letsencrypt/chain.pem -noout -issuer | grep -q "ISRG Root X"; then @@ -239,6 +241,21 @@ fi set_ini_value "server" "${ACMESERVER}" /config/etc/letsencrypt/cli.ini +# set certificate profile (e.g. "shortlived" for 6-day certs, "classic" for 90-day) +# Profiles are a Let's Encrypt ACME feature; ZeroSSL ignores it. +if [[ -n "${CERT_PROFILE}" ]]; then + if [[ "${CERTPROVIDER}" = "zerossl" ]]; then + echo "ZeroSSL does not support ACME profiles, ignoring CERT_PROFILE variable" + sed -i "/^preferred-profile\b/d" /config/etc/letsencrypt/cli.ini + else + echo "Requesting certificate with profile: ${CERT_PROFILE}" + set_ini_value "preferred-profile" "${CERT_PROFILE}" /config/etc/letsencrypt/cli.ini + fi +else + # remove if previously set so going back to default works + sed -i "/^preferred-profile\b/d" /config/etc/letsencrypt/cli.ini +fi + # figuring out domain only vs domain & subdomains vs subdomains only DOMAINS_ARRAY=() if [[ -z "${SUBDOMAINS}" ]] || [[ "${ONLY_SUBDOMAINS}" != true ]]; then From 828618f86bc669fe29b466d47b11a5ef736971f0 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Tue, 5 May 2026 18:30:22 +0200 Subject: [PATCH 02/11] Included the enviroment for cert_profile with a link to the Let's encrypt documentation for it. --- readme-vars.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/readme-vars.yml b/readme-vars.yml index 27bde094..84071236 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -32,6 +32,7 @@ opt_param_usage_include_env: true opt_param_env_vars: - {env_var: "SUBDOMAINS", env_value: "www,", desc: "Subdomains you'd like the cert to cover (comma separated, no spaces) ie. `www,ftp,cloud`. For a wildcard cert, set this *exactly* to `wildcard` (wildcard cert is available via `dns` validation only)"} - {env_var: "CERTPROVIDER", env_value: "", desc: "Optionally define the cert provider. Set to `zerossl` for ZeroSSL certs (requires existing [ZeroSSL account](https://app.zerossl.com/signup) and the e-mail address entered in `EMAIL` env var). Otherwise defaults to Let's Encrypt."} + - {env_var: "CERT_PROFILE", env_value: "", desc: "Optionally define a cert profile to use for cert generation. This is useful if you want to use a custom cert profile instead of the default one. Currently only supported for Let's Encrypt. See https://letsencrypt.org/docs/profiles/ "} - {env_var: "DNSPLUGIN", env_value: "cloudflare", desc: "Required if `VALIDATION` is set to `dns`. Options are `acmedns`, `aliyun`, `azure`, `bunny`, `cloudflare`, `cpanel`, `desec`, `digitalocean`, `directadmin`, `dnsimple`, `dnsmadeeasy`, `dnspod`, `do`, `domeneshop`, `dreamhost`, `duckdns`, `dynu`, `freedns`, `gandi`, `gehirn`, `glesys`, `godaddy`, `google`, `he`, `hetzner`, `hetzner-cloud`, `infomaniak`, `inwx`, `ionos`, `linode`, `loopia`, `luadns`, `namecheap`, `netcup`, `njalla`, `nsone`, `ovh`, `porkbun`, `rfc2136`, `route53`, `sakuracloud`, `standalone`, `transip`, and `vultr`. Also need to enter the credentials into the corresponding ini (or json for some plugins) file under `/config/dns-conf`."} - {env_var: "PROPAGATION", env_value: "", desc: "Optionally override (in seconds) the default propagation time for the dns plugins."} - {env_var: "EMAIL", env_value: "", desc: "Optional e-mail address used for cert expiration notifications (Required for ZeroSSL)."} From 295cd100dfc2b5c842067051b39d9a26a7759bc7 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Tue, 5 May 2026 18:34:22 +0200 Subject: [PATCH 03/11] Added change to changelog --- readme-vars.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/readme-vars.yml b/readme-vars.yml index 84071236..4713fc9f 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -220,6 +220,7 @@ init_diagram: | "swag:latest" <- Base Images # changelog changelogs: + - {date: "05.05.26:", desc: "Added support for Let's Encrypt cert profiles." } - {date: "23.01.26:", desc: "Reorder init to fix proxy conf version checks."} - {date: "21.12.25:", desc: "Add support for hetzner-cloud dns validation."} - {date: "04.11.25:", desc: "Switch default Gandi credentials from API Key to Token, allow DNS propagation time for Azure DNS plugin."} From 22bcba61509a8f4e18a32a010e8713b16448aa55 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Tue, 5 May 2026 19:57:20 +0200 Subject: [PATCH 04/11] Changes logic to check for old root ca. to stop constant certificate renewal. --- .../s6-overlay/s6-rc.d/init-certbot-config/run | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run index 28473339..8263b218 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run @@ -209,15 +209,12 @@ fi echo -e "ORIGURL=\"${URL}\" ORIGSUBDOMAINS=\"${SUBDOMAINS}\" ORIGONLY_SUBDOMAINS=\"${ONLY_SUBDOMAINS}\" ORIGEXTRA_DOMAINS=\"${EXTRA_DOMAINS}\" ORIGVALIDATION=\"${VALIDATION}\" ORIGDNSPLUGIN=\"${DNSPLUGIN}\" ORIGPROPAGATION=\"${PROPAGATION}\" ORIGSTAGING=\"${STAGING}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGEMAIL=\"${EMAIL}\" ORIGCERT_PROFILE=\"${CERT_PROFILE}\"" >/config/.donoteditthisfile.conf # Check if the cert is using the old LE root cert, revoke and regen if necessary -if [[ -f "/config/keys/letsencrypt/chain.pem" ]] && { [[ "${CERTPROVIDER}" == "letsencrypt" ]] || [[ "${CERTPROVIDER}" == "" ]]; } && [[ "${STAGING}" != "true" ]] && ! openssl x509 -in /config/keys/letsencrypt/chain.pem -noout -issuer | grep -q "ISRG Root X"; then - echo "The cert seems to be using the old LE root cert, which is no longer valid. Deleting and revoking." - REV_ACMESERVER=("https://acme-v02.api.letsencrypt.org/directory") - if [[ -f /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem ]]; then - certbot revoke --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini --non-interactive --cert-path /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem --server "${REV_ACMESERVER[@]}" || true - else - certbot revoke --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini --non-interactive --cert-name "${ORIGDOMAIN}" --server "${REV_ACMESERVER[@]}" || true - fi - rm -rf /config/etc/letsencrypt/{accounts,archive,live,renewal} +if [[ -f "/config/keys/letsencrypt/chain.pem" ]] && \ + { [[ "${CERTPROVIDER}" == "letsencrypt" ]] || [[ "${CERTPROVIDER}" == "" ]]; } && \ + [[ "${STAGING}" != "true" ]] && \ + openssl x509 -in /config/keys/letsencrypt/chain.pem -noout -issuer | grep -q "DST Root CA X3"; then + echo "The cert is chained through the expired DST Root CA X3. Deleting and revoking to force re-issuance." + ... fi # if zerossl is selected or staging is set to true, use the relevant server From bcca92b1fd8317436687d40dfa17097aa4533372 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Tue, 5 May 2026 20:14:39 +0200 Subject: [PATCH 05/11] Restore revocation body in DST Root CA X3 check --- root/etc/s6-overlay/s6-rc.d/init-certbot-config/run | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run index 8263b218..dd24ba1d 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run @@ -208,13 +208,19 @@ fi # saving new variables echo -e "ORIGURL=\"${URL}\" ORIGSUBDOMAINS=\"${SUBDOMAINS}\" ORIGONLY_SUBDOMAINS=\"${ONLY_SUBDOMAINS}\" ORIGEXTRA_DOMAINS=\"${EXTRA_DOMAINS}\" ORIGVALIDATION=\"${VALIDATION}\" ORIGDNSPLUGIN=\"${DNSPLUGIN}\" ORIGPROPAGATION=\"${PROPAGATION}\" ORIGSTAGING=\"${STAGING}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGEMAIL=\"${EMAIL}\" ORIGCERT_PROFILE=\"${CERT_PROFILE}\"" >/config/.donoteditthisfile.conf -# Check if the cert is using the old LE root cert, revoke and regen if necessary +# Check if the cert is chained through the expired DST Root CA X3, revoke and regen if necessary if [[ -f "/config/keys/letsencrypt/chain.pem" ]] && \ { [[ "${CERTPROVIDER}" == "letsencrypt" ]] || [[ "${CERTPROVIDER}" == "" ]]; } && \ [[ "${STAGING}" != "true" ]] && \ openssl x509 -in /config/keys/letsencrypt/chain.pem -noout -issuer | grep -q "DST Root CA X3"; then echo "The cert is chained through the expired DST Root CA X3. Deleting and revoking to force re-issuance." - ... + REV_ACMESERVER=("https://acme-v02.api.letsencrypt.org/directory") + if [[ -f /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem ]]; then + certbot revoke --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini --non-interactive --cert-path /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem --server "${REV_ACMESERVER[@]}" || true + else + certbot revoke --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini --non-interactive --cert-name "${ORIGDOMAIN}" --server "${REV_ACMESERVER[@]}" || true + fi + rm -rf /config/etc/letsencrypt/{accounts,archive,live,renewal} fi # if zerossl is selected or staging is set to true, use the relevant server From feaf30f630450571d5a62d88b3593ed3e77bc584 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Fri, 8 May 2026 17:15:26 +0200 Subject: [PATCH 06/11] remove the check for old root --- .../s6-overlay/s6-rc.d/init-certbot-config/run | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run index dd24ba1d..f0cf5826 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run @@ -208,21 +208,6 @@ fi # saving new variables echo -e "ORIGURL=\"${URL}\" ORIGSUBDOMAINS=\"${SUBDOMAINS}\" ORIGONLY_SUBDOMAINS=\"${ONLY_SUBDOMAINS}\" ORIGEXTRA_DOMAINS=\"${EXTRA_DOMAINS}\" ORIGVALIDATION=\"${VALIDATION}\" ORIGDNSPLUGIN=\"${DNSPLUGIN}\" ORIGPROPAGATION=\"${PROPAGATION}\" ORIGSTAGING=\"${STAGING}\" ORIGCERTPROVIDER=\"${CERTPROVIDER}\" ORIGEMAIL=\"${EMAIL}\" ORIGCERT_PROFILE=\"${CERT_PROFILE}\"" >/config/.donoteditthisfile.conf -# Check if the cert is chained through the expired DST Root CA X3, revoke and regen if necessary -if [[ -f "/config/keys/letsencrypt/chain.pem" ]] && \ - { [[ "${CERTPROVIDER}" == "letsencrypt" ]] || [[ "${CERTPROVIDER}" == "" ]]; } && \ - [[ "${STAGING}" != "true" ]] && \ - openssl x509 -in /config/keys/letsencrypt/chain.pem -noout -issuer | grep -q "DST Root CA X3"; then - echo "The cert is chained through the expired DST Root CA X3. Deleting and revoking to force re-issuance." - REV_ACMESERVER=("https://acme-v02.api.letsencrypt.org/directory") - if [[ -f /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem ]]; then - certbot revoke --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini --non-interactive --cert-path /config/etc/letsencrypt/live/"${ORIGDOMAIN}"/fullchain.pem --server "${REV_ACMESERVER[@]}" || true - else - certbot revoke --config-dir /config/etc/letsencrypt --logs-dir /config/log/letsencrypt --work-dir /tmp/letsencrypt --config /config/etc/letsencrypt/cli.ini --non-interactive --cert-name "${ORIGDOMAIN}" --server "${REV_ACMESERVER[@]}" || true - fi - rm -rf /config/etc/letsencrypt/{accounts,archive,live,renewal} -fi - # if zerossl is selected or staging is set to true, use the relevant server if [[ "${CERTPROVIDER}" = "zerossl" ]] && [[ "${STAGING}" = "true" ]]; then echo "ZeroSSL does not support staging mode, ignoring STAGING variable" From 36fe23e72b4ad2b8d1827f05c81b10f7973ce40f Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Sat, 9 May 2026 01:10:08 +0200 Subject: [PATCH 07/11] Increase renewal cron frequency and randomize offset for ARI Run certbot renew every 6 hours instead of once daily, as recommended by Let's Encrypt for short-lived certs with ARI support. Randomize the cron minute offset at container start to spread renewal load across instances. Co-Authored-By: Claude Sonnet 4.6 --- root/etc/crontabs/root | 2 +- root/etc/s6-overlay/s6-rc.d/init-certbot-config/run | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/root/etc/crontabs/root b/root/etc/crontabs/root index c848b3f5..4477060a 100644 --- a/root/etc/crontabs/root +++ b/root/etc/crontabs/root @@ -5,4 +5,4 @@ 0 3 * * 6 run-parts /etc/periodic/weekly 0 5 1 * * run-parts /etc/periodic/monthly -8 2 * * * /app/le-renew.sh >> /config/log/letsencrypt/renewal.log 2>&1 +8 */6 * * * /app/le-renew.sh >> /config/log/letsencrypt/renewal.log 2>&1 diff --git a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run index f0cf5826..a7d50234 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run @@ -373,3 +373,8 @@ if [[ -d /config/keys/letsencrypt ]]; then rm -rf /config/keys/cert.key ln -s ./letsencrypt/privkey.pem /config/keys/cert.key fi + +# Randomize the cron minute offset on each start to spread renewal load +CRON_MINUTE=$((RANDOM % 60)) +sed -i "s|^[0-9]\+\([[:space:]]\+\*/6.*le-renew\)|${CRON_MINUTE}\1|" /etc/crontabs/root +echo "Renewal cron scheduled at minute ${CRON_MINUTE} of every 6th hour" From 9a0c50d95e8016401ddebd917b3f8dcf19bcf2ea Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Sat, 9 May 2026 01:29:07 +0200 Subject: [PATCH 08/11] Move cron randomization to init-renew and fix stale log message Relocate the renewal cron minute randomization from init-certbot-config to init-renew so the expiry check is logged before the cron schedule. Also remove the hardcoded "overnight (2:08am)" from the log message. Co-Authored-By: Claude Sonnet 4.6 --- root/etc/s6-overlay/s6-rc.d/init-certbot-config/run | 5 ----- root/etc/s6-overlay/s6-rc.d/init-renew/run | 7 ++++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run index a7d50234..f0cf5826 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run +++ b/root/etc/s6-overlay/s6-rc.d/init-certbot-config/run @@ -373,8 +373,3 @@ if [[ -d /config/keys/letsencrypt ]]; then rm -rf /config/keys/cert.key ln -s ./letsencrypt/privkey.pem /config/keys/cert.key fi - -# Randomize the cron minute offset on each start to spread renewal load -CRON_MINUTE=$((RANDOM % 60)) -sed -i "s|^[0-9]\+\([[:space:]]\+\*/6.*le-renew\)|${CRON_MINUTE}\1|" /etc/crontabs/root -echo "Renewal cron scheduled at minute ${CRON_MINUTE} of every 6th hour" diff --git a/root/etc/s6-overlay/s6-rc.d/init-renew/run b/root/etc/s6-overlay/s6-rc.d/init-renew/run index b402a0b0..628ad0d5 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-renew/run +++ b/root/etc/s6-overlay/s6-rc.d/init-renew/run @@ -3,9 +3,14 @@ # Check if the cert is expired or expires within a day, if so, renew if openssl x509 -in /config/keys/letsencrypt/fullchain.pem -noout -checkend 86400 >/dev/null; then - echo "The cert does not expire within the next day. Letting the cron script handle the renewal attempts overnight (2:08am)." + echo "The cert does not expire within the next day. Letting the cron script handle the renewal attempts." else echo "The cert is either expired or it expires within the next day. Attempting to renew. This could take up to 10 minutes." /app/le-renew.sh sleep 1 fi + +# Randomize the cron minute offset on each start to spread renewal load +CRON_MINUTE=$((RANDOM % 60)) +sed -i "s|^[0-9]\+\([[:space:]]\+\*/6.*le-renew\)|${CRON_MINUTE}\1|" /etc/crontabs/root +echo "Renewal cron scheduled at minute ${CRON_MINUTE} of every 6th hour" From ad40eb741a80a034719346721218bd0519138f75 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Sat, 9 May 2026 02:22:00 +0200 Subject: [PATCH 09/11] Update renewal cron to twice daily with ARI and fix docs - Change certbot renew cron from once to twice daily per Let's Encrypt ARI recommendation - Fix cron randomization sed pattern to match */12 hour field - Update log message to reflect twice daily schedule - Update readme to reflect twice daily cert checks via ARI - Remove outdated reference to Let's Encrypt expiration emails Co-Authored-By: Claude Sonnet 4.6 --- readme-vars.yml | 4 +++- root/etc/crontabs/root | 2 +- root/etc/s6-overlay/s6-rc.d/init-renew/run | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/readme-vars.yml b/readme-vars.yml index 4713fc9f..74b6576d 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -66,7 +66,7 @@ app_setup_block: | 2. Certs that cover sub-subdomains of your main subdomain (ie. `*.yoursubdomain.duckdns.org`, set the `SUBDOMAINS` variable to `wildcard`) * `--cap-add=NET_ADMIN` is required for fail2ban to modify iptables * After setup, navigate to `https://example.com` to access the default homepage (http access through port 80 is disabled by default, you can enable it by editing the default site config at `/config/nginx/site-confs/default.conf`). - * Certs are checked nightly and if expiration is within 30 days, renewal is attempted. If your cert is about to expire in less than 30 days, check the logs under `/config/log/letsencrypt` to see why the renewals have been failing. It is recommended to input your e-mail in docker parameters so you receive expiration notices from Let's Encrypt in those circumstances. + * Certs are checked twice daily using ACME Renewal Information (ARI) to determine the optimal renewal window. If your cert is about to expire, check the logs under `/config/log/letsencrypt` to see why the renewals have been failing. ### Certbot Plugins @@ -220,6 +220,8 @@ init_diagram: | "swag:latest" <- Base Images # changelog changelogs: + - {date: "09.05.26:", desc: "Run certbot renew twice daily and randomize cron minute offset on startup for better ARI renewal window coverage. See [Let's Encrypt Integration Guide](https://letsencrypt.org/docs/integration-guide/)." } + - {date: "09.05.26:", desc: "Update docs to reflect twice daily cert checks via ARI and remove outdated reference to Let's Encrypt expiration notification emails which [ended in June 2025](https://letsencrypt.org/2025/06/26/expiration-notification-service-has-ended)." } - {date: "05.05.26:", desc: "Added support for Let's Encrypt cert profiles." } - {date: "23.01.26:", desc: "Reorder init to fix proxy conf version checks."} - {date: "21.12.25:", desc: "Add support for hetzner-cloud dns validation."} diff --git a/root/etc/crontabs/root b/root/etc/crontabs/root index 4477060a..876e7541 100644 --- a/root/etc/crontabs/root +++ b/root/etc/crontabs/root @@ -5,4 +5,4 @@ 0 3 * * 6 run-parts /etc/periodic/weekly 0 5 1 * * run-parts /etc/periodic/monthly -8 */6 * * * /app/le-renew.sh >> /config/log/letsencrypt/renewal.log 2>&1 +8 */12 * * * /app/le-renew.sh >> /config/log/letsencrypt/renewal.log 2>&1 diff --git a/root/etc/s6-overlay/s6-rc.d/init-renew/run b/root/etc/s6-overlay/s6-rc.d/init-renew/run index 628ad0d5..d88feba7 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-renew/run +++ b/root/etc/s6-overlay/s6-rc.d/init-renew/run @@ -12,5 +12,5 @@ fi # Randomize the cron minute offset on each start to spread renewal load CRON_MINUTE=$((RANDOM % 60)) -sed -i "s|^[0-9]\+\([[:space:]]\+\*/6.*le-renew\)|${CRON_MINUTE}\1|" /etc/crontabs/root -echo "Renewal cron scheduled at minute ${CRON_MINUTE} of every 6th hour" +sed -i "s|^[0-9]\+\([[:space:]]\+\*/12.*le-renew\)|${CRON_MINUTE}\1|" /etc/crontabs/root +echo "Renewal cron scheduled at minute ${CRON_MINUTE}, twice daily" From 0af9b47b66627099a76cbe76d6ba768779ccf736 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Sat, 9 May 2026 02:27:25 +0200 Subject: [PATCH 10/11] Show next scheduled renewal check time at startup Co-Authored-By: Claude Sonnet 4.6 --- root/etc/s6-overlay/s6-rc.d/init-renew/run | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/root/etc/s6-overlay/s6-rc.d/init-renew/run b/root/etc/s6-overlay/s6-rc.d/init-renew/run index d88feba7..acabd253 100755 --- a/root/etc/s6-overlay/s6-rc.d/init-renew/run +++ b/root/etc/s6-overlay/s6-rc.d/init-renew/run @@ -13,4 +13,18 @@ fi # Randomize the cron minute offset on each start to spread renewal load CRON_MINUTE=$((RANDOM % 60)) sed -i "s|^[0-9]\+\([[:space:]]\+\*/12.*le-renew\)|${CRON_MINUTE}\1|" /etc/crontabs/root -echo "Renewal cron scheduled at minute ${CRON_MINUTE}, twice daily" + +CURRENT_EPOCH=$(date +%s) +read -r HOUR MIN SEC <<< "$(date '+%H %M %S')" +MIDNIGHT_EPOCH=$(( CURRENT_EPOCH - HOUR * 3600 - MIN * 60 - SEC )) +RUN1_EPOCH=$(( MIDNIGHT_EPOCH + CRON_MINUTE * 60 )) +RUN2_EPOCH=$(( MIDNIGHT_EPOCH + 12 * 3600 + CRON_MINUTE * 60 )) +if [[ $CURRENT_EPOCH -lt $RUN1_EPOCH ]]; then + NEXT_EPOCH=$RUN1_EPOCH +elif [[ $CURRENT_EPOCH -lt $RUN2_EPOCH ]]; then + NEXT_EPOCH=$RUN2_EPOCH +else + NEXT_EPOCH=$(( MIDNIGHT_EPOCH + 86400 + CRON_MINUTE * 60 )) +fi +NEXT_RUN=$(date -d "@${NEXT_EPOCH}" "+%H:%M") +echo "Renewal cron scheduled at minute ${CRON_MINUTE}, twice daily. Next check at ${NEXT_RUN}." From bf4afc97164ccc6b913f2b5ec34cb73fa54364c2 Mon Sep 17 00:00:00 2001 From: sausageroll2077 Date: Sat, 9 May 2026 04:18:26 +0200 Subject: [PATCH 11/11] Fix changelog entry tense to match imperative style> --- readme-vars.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme-vars.yml b/readme-vars.yml index 74b6576d..58dc09a6 100644 --- a/readme-vars.yml +++ b/readme-vars.yml @@ -222,7 +222,7 @@ init_diagram: | changelogs: - {date: "09.05.26:", desc: "Run certbot renew twice daily and randomize cron minute offset on startup for better ARI renewal window coverage. See [Let's Encrypt Integration Guide](https://letsencrypt.org/docs/integration-guide/)." } - {date: "09.05.26:", desc: "Update docs to reflect twice daily cert checks via ARI and remove outdated reference to Let's Encrypt expiration notification emails which [ended in June 2025](https://letsencrypt.org/2025/06/26/expiration-notification-service-has-ended)." } - - {date: "05.05.26:", desc: "Added support for Let's Encrypt cert profiles." } + - {date: "05.05.26:", desc: "Add support for Let's Encrypt cert profiles." } - {date: "23.01.26:", desc: "Reorder init to fix proxy conf version checks."} - {date: "21.12.25:", desc: "Add support for hetzner-cloud dns validation."} - {date: "04.11.25:", desc: "Switch default Gandi credentials from API Key to Token, allow DNS propagation time for Azure DNS plugin."}