Compare commits

..

9 Commits
2.650 ... 2.651

Author SHA1 Message Date
Jamie Cameron
1c9d57f5dd Safe mode in the custom commands module just allows execution of commands
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-06-28 17:17:21 -07:00
Jamie Cameron
4ffa02d636 New version bump 2026-06-28 15:39:34 -07:00
Ilia Ross
57ad5fa535 Update labels
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
2026-06-28 23:02:39 +02:00
Ilia Ross
a3f5ea3381 Update changelog
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-06-28 02:40:46 +02:00
Ilia Ross
2579cf54d9 Fix output 2026-06-27 23:45:31 +02:00
Ilia Ross
fa06c02be5 Update changelog
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
https://forum.virtualmin.com/t/cant-create-letsencrypt-cert-with-webmin-2-650/137484/14?u=ilia
2026-06-27 23:26:28 +02:00
Ilia Ross
d02f0b6cb5 Fix Let's Encrypt Certbot PEM path parsing
ⓘ Prevent Webmin from swallowing Certbot's key-path output when extracting PEM paths, while preserving IPv6 cert-name support and adding regression coverage.
2026-06-27 22:59:21 +02:00
Ilia Ross
81d44f8491 Fix live activation of Linux bond interfaces
ⓘ Create and configure missing bond devices with ip link, attach partner interfaces before assigning addresses, avoid legacy module auto-creation when ip is available, and add regression coverage.

Ref.: https://github.com/webmin/webmin/pull/2777
2026-06-27 18:16:38 +02:00
Ilia Ross
6135c01d57 Update changelog
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
2026-06-26 23:30:00 +02:00
15 changed files with 216 additions and 39 deletions

View File

@@ -1,5 +1,13 @@
## Changelog
#### 2.651 (June 28, 2026)
* Fix Certbot-backed certificate requests and renewals to correctly parse PEM paths after issuance
* Fix live activation of Linux bond interfaces [#2777](https://github.com/webmin/webmin/pull/2777)
* Update the Authentic theme to the latest version with various improvements and fixes:
- Fix search-result all-items delete in File Manager
- Fix search-result delete ordering in File Manager
- Fix to speed up search-result deletion cleanup in File Manager
#### 2.650 (June 25, 2026)
* Add new Systemd Services and Units module
* Add new GRUB 2 Boot Loader module
@@ -32,8 +40,9 @@
* Fix PHP-FPM monitor on EL systems when using `/etc/php.ini` as the config file
* Fix RPC-only accounts to block browser/module access before module ACL checks
* Fix reflected XSS in Webmin status messages
* Fix authentication state handling for SSL certificate logins and proxied keep-alive requests
* Fix path validation in File Manager, package delete helpers, and Apache virtual host files
* Fix authentication state handling for SSL certificate logins and proxied keep-alive requests
* Update session handling to improve security, which will require users to re-authenticate after upgrading
* Update the Authentic theme to the latest version with various improvements and fixes:
- Add zooming to stats history graphs by holding shift and scrolling in the dashboard
- Add support for saving live stats history for up to 24 hours without performance impact

2
custom/safeacl Normal file
View File

@@ -0,0 +1,2 @@
edit=0
cmds=*

View File

@@ -258,7 +258,44 @@ if (&has_command("ip") && $a->{'virtual'} ne '' && !$a->{'up'}) {
return;
}
if (!&has_command("ifconfig") && &has_command("ip")) {
if (&has_command("ip") && $a->{'bond'} && $a->{'up'} && !$old) {
# Create the bond before assigning addresses to it.
my $bcmd = "ip link add ".quotemeta($a->{'name'})." type bond";
if (defined($a->{'mode'}) && $a->{'mode'} ne '') {
$bcmd .= " mode ".quotemeta(&bond_mode_name($a->{'mode'}));
}
if ($a->{'miimon'}) {
$bcmd .= " miimon ".quotemeta($a->{'miimon'});
}
if ($a->{'updelay'}) {
$bcmd .= " updelay ".quotemeta($a->{'updelay'});
}
if ($a->{'downdelay'}) {
$bcmd .= " downdelay ".quotemeta($a->{'downdelay'});
}
my $out = &backquote_logged("$bcmd 2>&1");
&error("Failed to create bond device : $out") if ($?);
foreach my $slave (grep { $_ ne '' } split(/\s+/, $a->{'partner'})) {
$bcmd = "ip link set dev ".quotemeta($slave)." down";
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to bring down bond slave : $out") if ($?);
$bcmd = "ip link set dev ".quotemeta($slave)." master ".
quotemeta($a->{'name'});
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to add bond slave : $out") if ($?);
$bcmd = "ip link set dev ".quotemeta($slave)." up";
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to bring up bond slave : $out") if ($?);
}
if ($a->{'primary'}) {
$bcmd = "ip link set dev ".quotemeta($a->{'name'}).
" type bond primary ".quotemeta($a->{'primary'});
$out = &backquote_logged("$bcmd 2>&1");
&error("Failed to set bond primary interface : $out") if ($?);
}
}
if (($a->{'bond'} || !&has_command("ifconfig")) && &has_command("ip")) {
# For a real interface, activate or de-activate the link
if ($a->{'virtual'} eq '' && $a->{'up'} && (!$old || !$old->{'up'})) {
# Bring up
@@ -646,6 +683,19 @@ local $out = &backquote_logged("$cmd 2>&1");
&error($out) if ($?);
}
# bond_mode_name(mode)
# Convert Webmin's numeric bonding mode to the name expected by ip(8).
sub bond_mode_name
{
my ($mode) = @_;
my @modes = ("balance-rr", "active-backup", "balance-xor", "broadcast",
"802.3ad", "balance-tlb", "balance-alb");
if ($mode =~ /^\d+$/ && defined($modes[$mode])) {
return $modes[$mode];
}
return $mode eq "activebackup" ? "active-backup" : $mode;
}
# Tries to unload the module
# unload_module(name)
sub unload_module

View File

@@ -377,7 +377,7 @@ else {
$err && &error("<pre>$err</pre>");
}
else {
if ($in{'bond'}) {
if ($in{'bond'} && !&has_command("ip")) {
if (($gconfig{'os_type'} eq 'debian-linux') &&
($gconfig{'os_version'} >= 5)) {}
else {&load_module($b);}
@@ -389,4 +389,3 @@ else {
"bifc", $b->{'fullname'}, $b);
}
&redirect("list_ifcs.cgi?mode=boot");

View File

@@ -617,4 +617,47 @@ is_deeply(\@commands,
[ "ip addr del 10\\.211\\.55\\.25\\/24 dev enp0s5 2>&1" ],
"Linux active virtual interface is removed when saved down");
@commands = ( );
{
no warnings 'redefine';
no warnings 'once';
local *main::has_command = sub {
return $_[0] eq "ip" ? "/sbin/ip" :
$_[0] eq "ifconfig" ? "/sbin/ifconfig" : undef;
};
local *main::active_interfaces = sub {
return ( );
};
local *main::backquote_command = sub {
return "";
};
main::activate_interface({ 'name' => 'bond0',
'fullname' => 'bond0',
'virtual' => '',
'bond' => 1,
'partner' => 'eth0 eth1',
'mode' => '1',
'primary' => 'eth0',
'miimon' => '100',
'updelay' => '200',
'downdelay' => '200',
'address' => '10.0.0.2',
'netmask' => '255.255.255.0',
'address6' => [ ],
'netmask6' => [ ],
'up' => 1 });
}
is_deeply(\@commands,
[ "ip link add bond0 type bond mode active\\-backup miimon 100 updelay 200 downdelay 200 2>&1",
"ip link set dev eth0 down 2>&1",
"ip link set dev eth0 master bond0 2>&1",
"ip link set dev eth0 up 2>&1",
"ip link set dev eth1 down 2>&1",
"ip link set dev eth1 master bond0 2>&1",
"ip link set dev eth1 up 2>&1",
"ip link set dev bond0 type bond primary eth0 2>&1",
"ip link set dev bond0 up 2>&1",
"cd / ; ip addr add 10\\.0\\.0\\.2/24 dev bond0 2>&1" ],
"Linux active bond interface is created before assigning an address");
done_testing();

View File

@@ -0,0 +1,50 @@
#!/usr/bin/perl
# Regression tests for Certbot output path parsing used by Webmin SSL.
use strict;
use warnings;
use Test::More;
use File::Basename qw(dirname);
use File::Spec;
our %config;
our $module_config_directory = "/etc/webmin/webmin";
sub has_command { return undef; }
my $script = File::Spec->rel2abs(
File::Spec->catfile(dirname(__FILE__), '..',
'webmin', 'letsencrypt-lib.pl'));
do $script or die "failed to load $script: $@ $!";
my $certbot_out = <<'EOF';
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/test.example/fullchain.pem
Key is saved at: /etc/letsencrypt/live/test.example/privkey.pem
This certificate expires on 2026-09-25.
EOF
is(main::get_letsencrypt_output_pem_path($certbot_out),
'/etc/letsencrypt/live/test.example/fullchain.pem',
'certbot output path stops at the first PEM path');
my $ipv6_out = <<'EOF';
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/2001:db8::1/fullchain.pem
Key is saved at: /etc/letsencrypt/live/2001:db8::1/privkey.pem
EOF
is(main::get_letsencrypt_output_pem_path($ipv6_out),
'/etc/letsencrypt/live/2001:db8::1/fullchain.pem',
'IPv6 certificate names can still contain colons');
my $wrapped_out = <<'EOF';
Certificate is saved at: /etc/letsencrypt/live/wrapped.example/
fullchain.pem
EOF
is(main::get_letsencrypt_output_pem_path($wrapped_out),
'/etc/letsencrypt/live/wrapped.example/fullchain.pem',
'wrapped PEM paths are normalized');
done_testing();

View File

@@ -1 +1 @@
2.650
2.651

View File

@@ -3,13 +3,13 @@ standard_url=URL of standard modules list,3,On webmin.com
third_url=URL of third party modules list,3,On webmin.com
cron_mode=Show update times as,1,0-Simple interface,1-Cron time selector
warn_days=Days before password expiry to warn users,0,5
line2=Let's Encrypt configuration,11
letsencrypt_cmd=Full path to Let's Encrypt client command,3,Find automatically
letsencrypt_directory_url=Custom ACME directory URL,3,Use Let's Encrypt production
line2=ACME provider configuration,11
letsencrypt_cmd=Full path to ACME client command,3,Find automatically
letsencrypt_directory_url=Custom ACME directory URL,3,Use default production server
letsencrypt_eab_kid=External Account Binding key ID,3,None
letsencrypt_eab_hmac=External Account Binding HMAC key,12
letsencrypt_algo=Encryption algorithm for Let's Encrypt private key,1,rsa-RSA,ecdsa-ECC
letsencrypt_dns_wait=Seconds to wait for Let's Encrypt DNS propagation,0,5
letsencrypt_before=Command to run before Let's Encrypt request,0,60
letsencrypt_after=Command to run after Let's Encrypt request,0,60
letsencrypt_reuse=Re-use existing Let's Encrypt keys?,1,1-Yes,0-No
letsencrypt_algo=Encryption algorithm for certificate private key,1,rsa-RSA,ecdsa-ECC
letsencrypt_dns_wait=Seconds to wait for ACME DNS propagation,0,5
letsencrypt_before=Command to run before ACME certificate request,0,60
letsencrypt_after=Command to run after ACME certificate request,0,60
letsencrypt_reuse=Re-use existing certificate keys?,1,1-Yes,0-No

View File

@@ -2,4 +2,4 @@ standard_url=URL de la llista de mòduls estàndard,3,A webmin.com
third_url=URL de la llista de mòduls de tercers,3,A webmin.com
cron_mode=Mostra els temps d'actualització com,1,0-Interfície simple,1-Selector de temps cron
warn_days=Dies abans de l'expiració de la contrasenya per avisar els usuaris,0,5
letsencrypt_cmd=Camí complet de l'ordre client de Let's Encrypt,3,Troba'l automàticament
letsencrypt_cmd=Camí complet de l'ordre client ACME,3,Troba'l automàticament

View File

@@ -2,4 +2,4 @@ standard_url=URL der Standardmodul&#45;Liste,3,Von webmin.com
third_url=URL der Drittanbieter&#45;Webmin&#45;Modulliste,3,Von webmin.com
cron_mode=Zeige Aktualisierungszeiten als,1,0-Einfache Anzeige,1-Cron&#45;Zeit&#45;Auswahl
warn_days=Tage vor Ablauf des Passworts zu warnen an Benutzer,0,5
letsencrypt_cmd=Voller Pfad zum Let's Encrypt Client&#45;Befehl,0
letsencrypt_cmd=Voller Pfad zum ACME-Client&#45;Befehl,0

View File

@@ -3,6 +3,6 @@ standard_url=URL de la liste des modules standard,3,webmin.com
third_url=URL de la liste des modules non-standard,3,webmin.com
cron_mode=Afficher les heures de mise à jour comme,1,0-Interface simple,1-Sélecteur de temps Cron
warn_days=Jours avant l'expiration du mot de passe pour avertir les utilisateurs,0,5
line2=Let's Encrypt configuration,11
letsencrypt_cmd=Chemin complet de la commande client Let's Encrypt,3,Trouver automatiquement
letsencrypt_dns_wait=Quelques secondes à attendre pour la propagation DNS de Let's Encrypt,0,5
line2=Configuration du fournisseur ACME,11
letsencrypt_cmd=Chemin complet de la commande client ACME,3,Trouver automatiquement
letsencrypt_dns_wait=Quelques secondes à attendre pour la propagation DNS ACME,0,5

View File

@@ -3,6 +3,6 @@ standard_url=標準モジュールリストに使用する URL,3,webmin.com
third_url=サードパーティ製モジュールリストに使用する URL,3,webmin.com
cron_mode=アップデート時刻の表示方法,1,0-シンプル,1-Cron 選択方式
warn_days=ユーザのパスワード期限切れの通知(日前),0,5
line2=Let's Encrypt の設定,11
letsencrypt_cmd=Let's Encrypt クライアントのパス,3,自動設定
letsencrypt_dns_wait=Let's Encrypt DNS の更新待機時間(秒),0,5
line2=ACME プロバイダーの設定,11
letsencrypt_cmd=ACME クライアントのパス,3,自動設定
letsencrypt_dns_wait=ACME DNS の更新待機時間(秒),0,5

View File

@@ -447,7 +447,7 @@ ssl_letsrenew_days=days
ssl_letsnotrenew=Only renew manually
ssl_staging=Provider server
ssl_staging0=Production
ssl_staging1=Staging (test only)
ssl_staging1=Test endpoint
ssl_acmeopts=Provider settings
ssl_acmeextra=Show advanced settings
ssl_acmedir=Custom ACME directory URL
@@ -1262,13 +1262,13 @@ letsencrypt_doing=Requesting a new certificate for $1, using the website directo
letsencrypt_doingdns=Requesting a new certificate for $1, using DNS validation ..
letsencrypt_doingcertbot=Requesting a new certificate for $1, using the Certbot webserver ..
letsencrypt_failed=.. request failed : $1
letsencrypt_done=.. request succeeded!
letsencrypt_show=The new certificate and private key were written to the following files :
letsencrypt_done=.. done
letsencrypt_show=The new certificate and private key were written to the following files:
letsencrypt_cert=SSL certificate
letsencrypt_key=SSL private key
letsencrypt_chain=Chained CA certificate
letsencrypt_webmin=Configuring Webmin to use new cert and key ..
letsencrypt_wdone=.. done!
letsencrypt_wdone=.. done
letsencrypt_eaccountkey=Failed to generate account key : $1
letsencrypt_etiny=Failed to request certificate : $1
letsencrypt_echain=Failed to download chained certificate : $1

View File

@@ -131,6 +131,20 @@ return &software::missing_install_link(
"certbot", $text{'letsencrypt_certbot'}, $rlink, $rmsg);
}
# get_letsencrypt_output_pem_path(output)
# Returns the first certbot PEM path from command output, or undef
sub get_letsencrypt_output_pem_path
{
my ($out) = @_;
if ($out =~ /((?:\/usr\/local)?\/etc\/letsencrypt\/(?:live|archive)\/[a-zA-Z0-9\.\_\-:\/\*]+\.pem)/ ||
$out =~ /((?:\/usr\/local)?\/etc\/letsencrypt\/(?:live|archive)\/[a-zA-Z0-9\.\_\-:\/\r\n\* ]*?\.pem)/) {
my $full = $1;
$full =~ s/\s//g;
return $full;
}
return undef;
}
# request_letsencrypt_cert(domain|&domains|&ips, webroot, [email], [keysize],
# [request-mode], [use-staging], [account-email],
# [key-type], [reuse-key],
@@ -387,14 +401,15 @@ 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 ($full = &get_letsencrypt_output_pem_path($out)) {
# Output contained the full path
$full = $1;
$full =~ s/\s//g;
}
else {
# Try searching common paths
my @fulls = (glob("/etc/letsencrypt/live/$certname-*/cert.pem"),
my @fulls = grep { -r $_ } (
"/etc/letsencrypt/live/$certname/cert.pem",
glob("/etc/letsencrypt/live/$certname-*/cert.pem"),
"/usr/local/etc/letsencrypt/live/$certname/cert.pem",
glob("/usr/local/etc/letsencrypt/live/$certname-*/cert.pem"));
if (@fulls) {
my %stats = map { $_, [ stat($_) ] } @fulls;

View File

@@ -132,7 +132,7 @@ else {
$mode eq 'certbot' ? 'letsencrypt_doingcertbot' :
'letsencrypt_doing',
"<tt>".&html_escape(join(", ", @doms))."</tt>",
"<tt>".&html_escape($webroot)."</tt>"),"<p>\n";
"<tt>".&html_escape($webroot)."</tt>"),"<br>\n";
my ($ok, $cert, $key, $chain) = &request_letsencrypt_cert(
\@doms, $webroot, undef, $size, $mode, $in{'staging'},
undef, undef, undef, $in{'directory_url'},
@@ -142,7 +142,20 @@ else {
}
else {
# Worked, now copy to Webmin
print $text{'letsencrypt_done'},"<p>\n";
my @grid = ( $text{'letsencrypt_cert'}, $cert,
$text{'letsencrypt_key'}, $key );
push(@grid, $text{'letsencrypt_chain'}, $chain) if ($chain);
my $details = &html_escape($text{'letsencrypt_show'})."<p>\n".
&ui_grid_table(\@grid, 2);
print &ui_details({
'html' => 1,
'title' => &ui_tag('span',
&html_escape($text{'letsencrypt_done'}),
{ 'data-second-print' => undef }),
'content' => $details,
'class' => 'inline inlined',
});
print "<div data-x-br=\"\"></div>\n";
# Save the renewal schedule
&save_renewal_only(\@doms, $webroot, $mode,
@@ -183,15 +196,11 @@ else {
&webmin_log("letsencrypt");
&restart_miniserv(1);
print $text{'letsencrypt_wdone'},"<p>\n";
print &ui_tag('span',
&html_escape($text{'letsencrypt_wdone'}),
{ 'data-second-print' => undef });
print "<br><div data-x-br=\"\"></div>\n";
}
# Tell the user what was done
print $text{'letsencrypt_show'},"<p>\n";
my @grid = ( $text{'letsencrypt_cert'}, $cert,
$text{'letsencrypt_key'}, $key );
push(@grid, $text{'letsencrypt_chain'}, $chain) if ($chain);
print &ui_grid_table(\@grid, 2);
}
&ui_print_footer("", $text{'index_return'});