Can't create letsencrypt cert with webmin 2.650

Hello,

Confirmed. This is a regression in Webmin 2.650! Sorry about that!

The certificate request itself may succeed, but Webmin then parses Certbot’s output incorrectly after IPv6 certificate-name support was added. That makes Webmin look for a bogus path like.

So this is not a DNS, Apache, or Let’s Encrypt validation problem. It happens after Certbot has already issued and saved the certificate.

A fix is ready:

Webmin now extracts only the first PEM path from Certbot output, while still supporting IPv6 certificate names.

To fix it immediately for those who already installed Webmin 2.650, apply the following patch:

{ cd /usr/libexec/webmin ||
  cd /usr/share/webmin; } &&
webmin patch /dev/stdin <<'EOF'
From d02f0b6cb5998ef2f9fbb4930e0e5719fd064b3e Mon Sep 17 00:00:00 2001
From: Ilia Ross
Date: Sat, 27 Jun 2026 22:59:21 +0200
Subject: [PATCH] Fix Let's Encrypt Certbot PEM path parsing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

ⓘ Prevent Webmin from swallowing Certbot's key-path output when extracting PEM paths
---
diff --git a/webmin/letsencrypt-lib.pl b/webmin/letsencrypt-lib.pl
index b335ddbe03..06315ba5f1 100755
--- a/webmin/letsencrypt-lib.pl
+++ b/webmin/letsencrypt-lib.pl
@@ -131,6 +131,20 @@ sub get_letsencrypt_install_message
  "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 @@ sub request_letsencrypt_cert
    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;
EOF