From 6026a20424119fc861abbf7a7b9f4f546d90a8cf Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 6 Jun 2026 00:22:19 +0200 Subject: [PATCH 1/2] Fix LE renewal to schedule by elapsed interval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Webmin SSL LE renewal setting is labeled as "Months between automatic renewal", but it was previously saved as a calendar-style cron month expression like `*/N`. That is not the same as an elapsed renewal interval. Webmin’s cron matcher evaluates month schedules against calendar month numbers, so values like `*/5`, `*/12`, or values above `12` do not reliably mean “renew every N months”. This could cause uneven or dangerously late renewal timing. This changes the renewal job to use Webmin cron’s elapsed `interval` support instead of calendar-month matching. - Saves automatic renewal as `renew * 30 * 24 * 60 * 60` seconds. - Clears the cron time fields so the scheduler uses the interval path only. - Keeps `months => '*/N'` so the SSL UI can continue to display the saved renewal value. - Resets the renewal timer only after a newly issued certificate. - Preserves the existing renewal timer for settings-only saves. - Migrates existing month-based Let's Encrypt renewal jobs during postinstall. --- webmin/letsencrypt.cgi | 27 +++++++++++++++++++-------- webmin/postinstall.pl | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/webmin/letsencrypt.cgi b/webmin/letsencrypt.cgi index 6aefb1bb4..75bfcad75 100755 --- a/webmin/letsencrypt.cgi +++ b/webmin/letsencrypt.cgi @@ -132,7 +132,7 @@ else { &save_renewal_only(\@doms, $webroot, $mode, $size, $in{'subset'}, $in{'use'}, $in{'directory_url'}, - $in{'eab_kid'}, $in{'eab_hmac'}); + $in{'eab_kid'}, $in{'eab_hmac'}, 1); # Copy cert, key and chain to Webmin if ($in{'use'}) { @@ -182,12 +182,12 @@ else { } # save_renewal_only(&doms, webroot, mode, size, subset-mode, used-by-webmin, -# directory-url, eab-kid, eab-hmac) +# directory-url, eab-kid, eab-hmac, [reset-renewal-time]) # Save for future renewals sub save_renewal_only { my ($doms, $webroot, $mode, $size, $subset, $usewebmin, $directory_url, - $eab_kid, $eab_hmac) = @_; + $eab_kid, $eab_hmac, $reset_renewal_time) = @_; $config{'letsencrypt_doms'} = join(" ", @$doms); $config{'letsencrypt_webroot'} = $webroot; $config{'letsencrypt_mode'} = $mode; @@ -219,14 +219,25 @@ if (&foreign_check("webmincron")) { &webmincron::delete_webmin_cron($job) if ($job); } else { - my @tm = localtime(time() - 60); + # When a cert was just issued, delete and re-create the job so + # it gets a fresh ID and its elapsed-interval countdown restarts + # from now. Re-using the ID could keep a stale last-run time + # that is already past the interval, firing a renewal + # immediately and wasting a Let's Encrypt issuance + if ($job && $reset_renewal_time) { + &webmincron::delete_webmin_cron($job); + $job = undef; + } $job ||= { 'module' => $module_name, 'func' => 'renew_letsencrypt_cert' }; - $job->{'mins'} ||= $tm[1]; - $job->{'hours'} ||= $tm[2]; - $job->{'days'} ||= $tm[3]; + # Scheduling is driven by 'interval' (elapsed seconds); 'months' + # is retained only so edit_ssl.cgi can show the renewal interval + $job->{'mins'} = ''; + $job->{'hours'} = ''; + $job->{'days'} = ''; $job->{'months'} = '*/'.$in{'renew'}; - $job->{'weekdays'} = '*'; + $job->{'weekdays'} = ''; + $job->{'interval'} = $in{'renew'}*30*24*60*60; &webmincron::create_webmin_cron($job); } } diff --git a/webmin/postinstall.pl b/webmin/postinstall.pl index b520b1b5c..8cccf5d09 100755 --- a/webmin/postinstall.pl +++ b/webmin/postinstall.pl @@ -34,6 +34,21 @@ elsif ($miniserv{'cipher_list_def'} == 2 || $miniserv{'cipher_list_def'} == 3) { $strong_ssl_ciphers : $pfs_ssl_ciphers; } +# Convert old Let's Encrypt renewal schedules to elapsed interval timers +if (&foreign_check("webmincron")) { + my $job = &find_letsencrypt_cron_job(); + if ($job && !$job->{'interval'} && + $job->{'months'} =~ /^\*\/([1-9][0-9]*)$/) { + my $renew = $1; + $job->{'mins'} = ''; + $job->{'hours'} = ''; + $job->{'days'} = ''; + $job->{'weekdays'} = ''; + $job->{'interval'} = $renew*30*24*60*60; + &webmincron::save_webmin_cron($job); + } + } + # If this is the first install, enable recording of logins by default if (!-r $first_install_file || $miniserv{'login_script'} eq $record_login_cmd) { &foreign_require("cron"); From 80497c60b9f1d47f8aee3e8cfbebb47707242603 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 6 Jun 2026 12:59:44 +0200 Subject: [PATCH 2/2] Update comment --- webmin/letsencrypt.cgi | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/webmin/letsencrypt.cgi b/webmin/letsencrypt.cgi index 75bfcad75..fb7481dbb 100755 --- a/webmin/letsencrypt.cgi +++ b/webmin/letsencrypt.cgi @@ -219,11 +219,10 @@ if (&foreign_check("webmincron")) { &webmincron::delete_webmin_cron($job) if ($job); } else { - # When a cert was just issued, delete and re-create the job so - # it gets a fresh ID and its elapsed-interval countdown restarts - # from now. Re-using the ID could keep a stale last-run time - # that is already past the interval, firing a renewal - # immediately and wasting a Let's Encrypt issuance + # A manual cert request does not update Webmin cron's last-run + # state, so re-create the job to start the elapsed renewal + # interval from now instead of immediately running an overdue + # job with the old ID if ($job && $reset_renewal_time) { &webmincron::delete_webmin_cron($job); $job = undef;