Fix IP address support for Webmin Let’s Encrypt certificates
ⓘ Support IPv4/IPv6 identifiers in Webmin Let’s Encrypt requests, add days/months renewal scheduling, and enforce safe automatic renewal defaults for short-lived IP certificates. https://github.com/webmin/webmin/commit/a56748a3fc136ddaa6d24a626d100beb9a8b870a#r188893457
This commit is contained in:
+23
-3
@@ -406,9 +406,29 @@ else {
|
||||
|
||||
# Renewal option
|
||||
my $job = &find_letsencrypt_cron_job();
|
||||
my $renew = $job && $job->{'months'} =~ /^\*\/(\d+)$/ ? $1 : undef;
|
||||
print &ui_table_row($text{'ssl_letsrenew'},
|
||||
&ui_opt_textbox("renew", $renew, 4, $text{'ssl_letsnotrenew'}));
|
||||
my $has_ip_doms = &letsencrypt_doms_have_ips(\@doms);
|
||||
my $renew_unit = $has_ip_doms ? "days" :
|
||||
($config{'letsencrypt_renew_unit'} || "months");
|
||||
my $renew;
|
||||
if ($job && $renew_unit eq "days" && $job->{'interval'}) {
|
||||
$renew = int($job->{'interval'} / (24*60*60));
|
||||
}
|
||||
elsif ($job && $job->{'months'} =~ /^\*\/(\d+)$/) {
|
||||
$renew_unit = "months";
|
||||
$renew = $1;
|
||||
}
|
||||
elsif ($job && $job->{'interval'}) {
|
||||
$renew_unit = "months";
|
||||
$renew = int($job->{'interval'} / (30*24*60*60));
|
||||
}
|
||||
my @renew_units = ( [ "months", $text{'ssl_letsrenew_months'} ],
|
||||
[ "days", $text{'ssl_letsrenew_days'} ] );
|
||||
my $renew_disabled = $renew eq '';
|
||||
print &ui_table_row(&hlink($text{'ssl_letsrenew'}, "letsrenew"),
|
||||
&ui_opt_textbox("renew", $renew, 4, $text{'ssl_letsnotrenew'},
|
||||
undef, undef, [ "renew_unit" ])." ".
|
||||
&ui_select("renew_unit", $renew_unit, \@renew_units,
|
||||
undef, undef, undef, $renew_disabled));
|
||||
|
||||
print &ui_table_end();
|
||||
print &ui_form_end([ [ undef, $text{'ssl_letsok'} ],
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<header>Automatic renewal interval</header>
|
||||
|
||||
<p>Sets how often Webmin should request a replacement certificate from the ACME
|
||||
provider.</p>
|
||||
|
||||
<p>For hostname certificates, a renewal interval measured in months is usually
|
||||
appropriate. You can also choose days if you want Webmin to rotate the
|
||||
certificate more frequently.</p>
|
||||
|
||||
<p>IP address certificates are issued as short-lived certificates by CAs such as
|
||||
Let's Encrypt. Let's Encrypt currently makes these certificates valid for
|
||||
160 hours, just over six days, and requires IP address certificates to use the
|
||||
short-lived profile because IP addresses can change ownership more often than
|
||||
domain names.</p>
|
||||
|
||||
<p>For IP address certificates, Webmin defaults to days and using 1 or 2 days
|
||||
is recommended so there is time to retry before the certificate expires. If
|
||||
Webmin is managing automatic renewal, the interval must be no more than 5 days.
|
||||
Manual renewal remains an administrator choice, especially if renewal is handled
|
||||
by separate automation or another operational policy.</p>
|
||||
|
||||
<footer>
|
||||
+10
-6
@@ -427,11 +427,11 @@ ssl_already2=Previously generated certificate signing request :
|
||||
ssl_letsdesc=This form lets you request a trusted SSL certificate for Webmin from an ACME-compatible provider such as Let's Encrypt.
|
||||
ssl_letserr=Unfortunately, this system cannot request certificates from an SSL provider : $1.
|
||||
ssl_letserr2=Alternately, check the <a href='$1'>module configuration</a> page to ensure you are using the correct path to the configured ACME client command.
|
||||
ssl_letsdesc2=The new certificate will replace the one currently configured in Webmin. Before it can be issued, the ACME provider must verify control of each hostname using either a temporary file in the website's document directory or a temporary DNS TXT record.
|
||||
ssl_letsdesc2=The new certificate will replace the one currently configured in Webmin. Before it can be issued, the ACME provider must verify control of each hostname or IP address. Hostnames can be verified using either a temporary file in the website's document directory or a temporary DNS TXT record; IP addresses require webserver-based validation.
|
||||
ssl_letsheader=Certificate request options
|
||||
ssl_letsdoms=Hostnames for certificate
|
||||
ssl_subset=Skip unverifiable hostnames?
|
||||
ssl_letsmode=Domain validation method
|
||||
ssl_letsdoms=Hostnames or IP addresses for certificate
|
||||
ssl_subset=Skip unverifiable hostnames or IP addresses?
|
||||
ssl_letsmode=Validation method
|
||||
ssl_letsmode0=Apache virtual host matching hostname
|
||||
ssl_letsmode1=Selected Apache virtual host
|
||||
ssl_letsmode2=Other webserver document directory
|
||||
@@ -441,7 +441,9 @@ ssl_certbotprehook=Nevertheless, this warning can be ignored if the pre-request
|
||||
ssl_letsok=Request Certificate
|
||||
ssl_letsonly=Just Update Renewal
|
||||
ssl_usewebmin=Copy new key and certificate to Webmin?
|
||||
ssl_letsrenew=Months between automatic renewal
|
||||
ssl_letsrenew=Automatic renewal interval
|
||||
ssl_letsrenew_months=months
|
||||
ssl_letsrenew_days=days
|
||||
ssl_letsnotrenew=Only renew manually
|
||||
ssl_staging=Provider server
|
||||
ssl_staging0=Production
|
||||
@@ -1237,8 +1239,10 @@ letsencrypt_epythonver=Failed to get the Python version : $1
|
||||
letsencrypt_epythonver2=Python version $1 or above is required, but you only have version $2.
|
||||
letsencrypt_certbot=Certbot
|
||||
letsencrypt_edroot=The Apache virtual host $1 has no document directory!
|
||||
letsencrypt_edom=Missing or invalid domain name
|
||||
letsencrypt_edom=Missing or invalid domain name or IP address
|
||||
letsencrypt_erenew=Missing or non-numeric renewal interval
|
||||
letsencrypt_erenewunit=Invalid renewal interval unit
|
||||
letsencrypt_erenewipauto=Automatic renewal for IP address certificates must use days and be no more than $1 days
|
||||
letsencrypt_eacmedir=Missing or invalid ACME directory URL
|
||||
letsencrypt_eeabdir=An ACME directory URL must be entered when using External Account Binding
|
||||
letsencrypt_eeabpair=Both the External Account Binding key ID and HMAC key must be entered
|
||||
|
||||
@@ -18,6 +18,26 @@ $letsencrypt_chain_urls = [
|
||||
"https://letsencrypt.org/certs/lets-encrypt-e1.pem",
|
||||
];
|
||||
|
||||
# letsencrypt_is_ip_cert_identifier(identifier)
|
||||
# Returns 1 if some certificate identifier is an IPv4 or IPv6 address
|
||||
sub letsencrypt_is_ip_cert_identifier
|
||||
{
|
||||
my ($id) = @_;
|
||||
return 1 if (&check_ipaddress($id));
|
||||
return $id !~ /\// && $id =~ /:/ && &check_ip6address($id);
|
||||
}
|
||||
|
||||
# letsencrypt_doms_have_ips(&domains)
|
||||
# Returns 1 if any certificate identifier is an IP address
|
||||
sub letsencrypt_doms_have_ips
|
||||
{
|
||||
my ($doms) = @_;
|
||||
foreach my $d (@$doms) {
|
||||
return 1 if (&letsencrypt_is_ip_cert_identifier($d));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
# check_letsencrypt()
|
||||
# Returns undef if all dependencies are installed, or an error message
|
||||
sub check_letsencrypt
|
||||
@@ -126,8 +146,8 @@ my ($dom, $webroot, $email, $size, $mode, $staging, $account_email,
|
||||
$subset) = @_;
|
||||
my @doms = ref($dom) ? @$dom : ($dom);
|
||||
@doms = &unique(@doms);
|
||||
my @ips = grep { &check_ipaddress($_) } @doms;
|
||||
@doms = grep { !&check_ipaddress($_) } @doms;
|
||||
my @ips = grep { &letsencrypt_is_ip_cert_identifier($_) } @doms;
|
||||
@doms = grep { !&letsencrypt_is_ip_cert_identifier($_) } @doms;
|
||||
$email ||= (@doms ? "root\@$doms[0]" : "root\@".&get_system_hostname());
|
||||
$mode ||= "web";
|
||||
$reuse_key = $config{'letsencrypt_reuse'} if (!defined($reuse_key));
|
||||
@@ -152,9 +172,11 @@ if (@ips) {
|
||||
"for IP addresses");
|
||||
$letsencrypt_cmd || return (0, "The certbot command is required for ".
|
||||
"IP address certificates");
|
||||
&compare_version_numbers($cmd_ver, "5.3") >= 0 ||
|
||||
return (0, "Certbot version 5.3 or later is required for ".
|
||||
"IP address certificates");
|
||||
my $ip_min_certbot = $mode eq "web" ? "5.4" : "5.3";
|
||||
&compare_version_numbers($cmd_ver, $ip_min_certbot) >= 0 ||
|
||||
return (0, "Certbot version $ip_min_certbot or later is ".
|
||||
"required for IP address certificates".
|
||||
($mode eq "web" ? " in webroot mode" : ""));
|
||||
}
|
||||
|
||||
if ($mode eq "web") {
|
||||
@@ -365,7 +387,7 @@ if ($letsencrypt_cmd) {
|
||||
goto FAILED;
|
||||
}
|
||||
my ($full, $cert, $key, $chain);
|
||||
if ($out =~ /((?:\/usr\/local)?\/etc\/letsencrypt\/(?:live|archive)\/[a-zA-Z0-9\.\_\-\/\r\n\* ]*\.pem)/) {
|
||||
if ($out =~ /((?:\/usr\/local)?\/etc\/letsencrypt\/(?:live|archive)\/[a-zA-Z0-9\.\_\-:\/\r\n\* ]*\.pem)/) {
|
||||
# Output contained the full path
|
||||
$full = $1;
|
||||
$full =~ s/\s//g;
|
||||
|
||||
+32
-7
@@ -16,6 +16,8 @@ our $module_name;
|
||||
our $letsencrypt_cmd;
|
||||
&error_setup($text{'letsencrypt_err'});
|
||||
|
||||
my $letsencrypt_ip_cert_renewal_max_days = 5;
|
||||
|
||||
# Re-check if let's encrypt is available
|
||||
my $err = &check_letsencrypt();
|
||||
&error($err) if ($err);
|
||||
@@ -24,8 +26,11 @@ my $err = &check_letsencrypt();
|
||||
&ReadParse();
|
||||
my @doms = split(/\s+/, $in{'dom'});
|
||||
foreach my $dom (@doms) {
|
||||
$dom =~ /^(\*\.)?[a-z0-9\-\.\_]+$/i || &error($text{'letsencrypt_edom'});
|
||||
$dom =~ /^(\*\.)?[a-z0-9\-\.\_]+$/i ||
|
||||
&letsencrypt_is_ip_cert_identifier($dom) ||
|
||||
&error($text{'letsencrypt_edom'});
|
||||
}
|
||||
my $has_ip_doms = &letsencrypt_doms_have_ips(\@doms);
|
||||
$in{'directory_url'} = &trim($in{'directory_url'});
|
||||
$in{'eab_kid'} = &trim($in{'eab_kid'});
|
||||
$in{'eab_hmac'} = &trim($in{'eab_hmac'});
|
||||
@@ -39,8 +44,19 @@ if ($in{'eab_kid'} || $in{'eab_hmac'}) {
|
||||
&error($text{'letsencrypt_eeabpair'});
|
||||
$letsencrypt_cmd || &error($text{'letsencrypt_eeabnative'});
|
||||
}
|
||||
$in{'renew_def'} || $in{'renew'} =~ /^[1-9][0-9]*$/ ||
|
||||
&error($text{'letsencrypt_erenew'});
|
||||
if (!$in{'renew_def'}) {
|
||||
$in{'renew'} =~ /^[1-9][0-9]*$/ ||
|
||||
&error($text{'letsencrypt_erenew'});
|
||||
$in{'renew_unit'} ||= $has_ip_doms ? "days" : "months";
|
||||
$in{'renew_unit'} =~ /^(days|months)$/ ||
|
||||
&error($text{'letsencrypt_erenewunit'});
|
||||
my $ip_max_days = $letsencrypt_ip_cert_renewal_max_days;
|
||||
if ($has_ip_doms && ($in{'renew_unit'} ne "days" ||
|
||||
$in{'renew'} > $ip_max_days)) {
|
||||
&error(&text('letsencrypt_erenewipauto',
|
||||
$ip_max_days));
|
||||
}
|
||||
}
|
||||
$in{'size_def'} || $in{'size'} =~ /^\d+$/ ||
|
||||
&error($text{'newkey_esize'});
|
||||
my $size = $in{'size_def'} ? undef : $in{'size'};
|
||||
@@ -194,6 +210,11 @@ $config{'letsencrypt_mode'} = $mode;
|
||||
$config{'letsencrypt_size'} = $size;
|
||||
$config{'letsencrypt_subset'} = $subset;
|
||||
$config{'letsencrypt_nouse'} = $usewebmin ? 0 : 1;
|
||||
my $renew_has_ip_doms = &letsencrypt_doms_have_ips($doms);
|
||||
my $renew_unit = $in{'renew_unit'} =~ /^(days|months)$/ ?
|
||||
$in{'renew_unit'} :
|
||||
($renew_has_ip_doms ? "days" : "months");
|
||||
$config{'letsencrypt_renew_unit'} = $renew_unit;
|
||||
if ($directory_url) {
|
||||
$config{'letsencrypt_directory_url'} = $directory_url;
|
||||
}
|
||||
@@ -229,14 +250,18 @@ if (&foreign_check("webmincron")) {
|
||||
}
|
||||
$job ||= { 'module' => $module_name,
|
||||
'func' => 'renew_letsencrypt_cert' };
|
||||
# Scheduling is driven by 'interval' (elapsed seconds); 'months'
|
||||
# is retained only so edit_ssl.cgi can show the renewal interval
|
||||
# Scheduling is driven by 'interval' (elapsed seconds).
|
||||
# 'months' is retained only for month-based schedules so
|
||||
# edit_ssl.cgi can show the renewal interval.
|
||||
$job->{'mins'} = '';
|
||||
$job->{'hours'} = '';
|
||||
$job->{'days'} = '';
|
||||
$job->{'months'} = '*/'.$in{'renew'};
|
||||
$job->{'months'} = $renew_unit eq "months" ?
|
||||
'*/'.$in{'renew'} : '';
|
||||
$job->{'weekdays'} = '';
|
||||
$job->{'interval'} = $in{'renew'}*30*24*60*60;
|
||||
$job->{'interval'} = $renew_unit eq "days" ?
|
||||
$in{'renew'}*24*60*60 :
|
||||
$in{'renew'}*30*24*60*60;
|
||||
&webmincron::create_webmin_cron($job);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user