diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php
index fe2e5415a73a311c0e5f265f3e4ad0d1ea141692..84d634fa10981e7303876aae97a4bc2761d2514d 100644
--- a/install/lib/installer_base.lib.php
+++ b/install/lib/installer_base.lib.php
@@ -3006,33 +3006,33 @@ class installer_base extends stdClass {
}
}
$dns_ips = array();
- if (checkdnsrr($hostname, 'A')) {
- $dnsa=dns_get_record($hostname, DNS_A);
+ if(checkdnsrr($hostname, 'A')) {
+ $dnsa = dns_get_record($hostname, DNS_A);
if($dnsa) {
- foreach ($dnsa as $rec) {
+ foreach($dnsa as $rec) {
if(is_array($rec) && isset($rec['ip'])) $dns_ips[] = $rec['ip'];
}
}
}
- if (checkdnsrr($hostname, 'AAAA')) {
- $dnsaaaa=dns_get_record($hostname, DNS_AAAA);
+ if(checkdnsrr($hostname, 'AAAA')) {
+ $dnsaaaa = dns_get_record($hostname, DNS_AAAA);
if($dnsaaaa) {
- foreach ($dnsaaaa as $rec) {
+ foreach($dnsaaaa as $rec) {
if(is_array($rec) && isset($rec['ip'])) $dns_ips[] = $rec['ip'];
}
}
}
//* Define and check ISPConfig SSL folder */
- $ssl_dir = $conf['ispconfig_install_dir'].'/interface/ssl';
+ $ssl_dir = $conf['ispconfig_install_dir'] . '/interface/ssl';
if(!@is_dir($ssl_dir)) {
mkdir($ssl_dir, 0755, true);
}
- $ssl_crt_file = $ssl_dir.'/ispserver.crt';
- $ssl_csr_file = $ssl_dir.'/ispserver.csr';
- $ssl_key_file = $ssl_dir.'/ispserver.key';
- $ssl_pem_file = $ssl_dir.'/ispserver.pem';
+ $ssl_crt_file = $ssl_dir . '/ispserver.crt';
+ $ssl_csr_file = $ssl_dir . '/ispserver.csr';
+ $ssl_key_file = $ssl_dir . '/ispserver.key';
+ $ssl_pem_file = $ssl_dir . '/ispserver.pem';
$date = new DateTime();
@@ -3040,17 +3040,100 @@ class installer_base extends stdClass {
swriteln('Checking / creating certificate for ' . $hostname);
- $acme_cert_dir = '/usr/local/ispconfig/server/scripts/' . $hostname;
- $check_acme_file = $acme_cert_dir . '/' . $hostname . '.cer';
- if(!@is_dir($acme_cert_dir)) {
- $acme_cert_dir = '/root/.acme.sh/' . $hostname;
- $check_acme_file = $acme_cert_dir . '/' . $hostname . '.cer';
- if(!@is_dir($acme_cert_dir)) {
+ // Get the default LE client name and version
+ $which_certbot = shell_exec('which certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot letsencrypt');
+ $certbot = explode("\n", $which_certbot ?: '');
+ $certbot = reset($certbot);
+ $certbot_version = '0.0.0-unknown';
+ if($certbot) {
+ $matches = [];
+ $output = shell_exec($certbot . ' --version 2>&1');
+ if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $output, $matches)) {
+ $certbot_version = $matches[2];
+ swriteln('Discovered certbot version ' . $certbot_version . ' with certificate home /etc/letsencrypt');
+ } else {
+ $certbot = '';
+ }
+ }
+
+ // Check for Neilpang acme.sh as well and install it when we did not find certbot
+ $which_acme = shell_exec('which acme.sh /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh');
+ $acme = explode("\n", $which_acme ? $which_acme : '');
+ $acme = reset($acme);
+ $acme_version = '0.0.0-unknown';
+
+ if(!$certbot && !$acme) {
+ $this->install_acme();
+ $which_acme = shell_exec('which acme.sh /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh');
+ $acme = explode("\n", $which_acme ?: '');
+ $acme = reset($acme);
+ }
+ if($acme) {
+ // always update acme.sh
+ $this->update_acme();
+ $matches = [];
+ $output = shell_exec($acme . ' --version 2>&1') ?: '';
+ if(preg_match('/^v(\d+(\.\d+)+)$/m', $output, $matches)) {
+ $acme_version = $matches[1];
+ } else {
+ $acme = '';
+ }
+ if($acme) {
+ $ret = 0;
+ $acme_cert_home = [];
+ exec(join(' ; ', [
+ '_info() { :',
+ ' _info_stdout=$(' . escapeshellarg($acme) . ' --info 2>/dev/null)',
+ ' _info_ret=$?',
+ '}',
+ '_echo_home() { :',
+ ' eval "$_info_stdout"',
+ ' _info_ret=$?',
+ ' if [ $_info_ret -eq 0 ]; then :',
+ ' if [ -z "$CERT_HOME" ]',
+ ' then echo "$LE_CONFIG_HOME"',
+ ' else echo "$CERT_HOME"',
+ ' fi',
+ ' else :',
+ ' echo "Error eval-ing --info output (exit code $_info_ret). stdout was: $_info_stdout"',
+ ' exit 1',
+ ' fi',
+ '}',
+ '_info',
+ 'if [ $_info_ret -eq 0 ]; then :',
+ ' _echo_home',
+ 'else :',
+ ' echo "--info failed. stdout was: $_info_stdout"',
+ ' exit 1',
+ 'fi',
+ ]), $acme_cert_home, $ret);
+ $acme_cert_home = trim(implode("\n", $acme_cert_home));
+ if($ret != 0 || empty($acme_cert_home) || !is_dir($acme_cert_home)) {
+ swriteln('Cannot find acme.sh certificate home: ' . $acme_cert_home);
+ $acme = '';
+ } else {
+ swriteln('Discovered acme.sh version ' . $acme_version . ' with certificate home ' . $acme_cert_home);
+ }
+ }
+ }
+
+ $acme_cert_dir = 'not found';
+ $check_acme_file = '';
+ if($certbot) {
+ if(version_compare($certbot_version, '2.0', '>=')) {
+ $acme_cert_dir = '/etc/letsencrypt/live/' . $hostname . '_ecc';
+ } else {
$acme_cert_dir = '/etc/letsencrypt/live/' . $hostname;
- $check_acme_file = $acme_cert_dir . '/cert.pem';
}
+ $check_acme_file = $acme_cert_dir . '/cert.pem';
+ swriteln('Using certificate path ' . $acme_cert_dir . ' / ' . $check_acme_file);
+ } elseif($acme) {
+ $acme_cert_dir = $acme_cert_home . '/' . $hostname . '_ecc'; // always use ECC since we updated acme.sh
+ $check_acme_file = $acme_cert_dir . '/' . $hostname . '.cer';
+ swriteln('Using certificate path ' . $acme_cert_dir . ' / ' . $check_acme_file);
+ } else {
+ swriteln('Failed discovering certbot or acme.sh and installing acme.sh. Will not be able to issue certificate during install.');
}
- swriteln('Using certificate path ' . $acme_cert_dir);
if(!is_dir($conf['ispconfig_log_dir'])) {
mkdir($conf['ispconfig_log_dir'], 0755, true);
@@ -3059,8 +3142,8 @@ class installer_base extends stdClass {
$ip_address_match = false;
if(!(($svr_ip4 && in_array($svr_ip4, $dns_ips)) || ($svr_ip6 && in_array($svr_ip6, $dns_ips)))) {
- swriteln('Server\'s public ip(s) (' . $svr_ip4 . ($svr_ip6 ? ', ' . $svr_ip6 : '') . ') not found in A/AAAA records for ' . $hostname . ': ' . implode(', ', $dns_ips));
- if(strtolower($this->simple_query('Ignore DNS check and continue to request certificate?', array('y', 'n') , 'n','ignore_hostname_dns')) == 'y') {
+ swriteln('Server\'s public ip(s) (' . implode(', ', array_filter([$svr_ip4, $svr_ip6])) . ') not found in A/AAAA records for ' . $hostname . ': ' . implode(', ', $dns_ips));
+ if(strtolower($this->simple_query('Ignore DNS check and continue to request certificate?', array('y', 'n'), 'n', 'ignore_hostname_dns')) == 'y') {
$ip_address_match = true;
}
} else {
@@ -3068,14 +3151,31 @@ class installer_base extends stdClass {
}
// Get subject and issuer of ispserver.crt to check if it is self-signed cert
- if (file_exists($ssl_crt_file)) {
- $crt_subject = exec("openssl x509 -in ".escapeshellarg($ssl_crt_file)." -inform PEM -noout -subject");
- $crt_issuer = exec("openssl x509 -in ".escapeshellarg($ssl_crt_file)." -inform PEM -noout -issuer");
+ $self_signed = false;
+ if(file_exists($ssl_crt_file)) {
+ $crt_subject = exec("openssl x509 -in " . escapeshellarg($ssl_crt_file) . " -inform PEM -noout -subject");
+ $crt_issuer = exec("openssl x509 -in " . escapeshellarg($ssl_crt_file) . " -inform PEM -noout -issuer");
+ // strip the subject= and issuer= prefix to check for equality
+ if(is_string($crt_subject) && strpos($crt_subject, 'subject=') !== false) {
+ $crt_subject = explode('=', $crt_subject, 2)[1];
+ }
+ if(is_string($crt_issuer) && strpos($crt_issuer, 'issuer=') !== false) {
+ $crt_issuer = explode('=', $crt_issuer, 2)[1];
+ }
+ $self_signed = $crt_subject == $crt_issuer;
+ if ($self_signed) {
+ swriteln('ISPConfig currently is using a self-signed certificate.');
+ }
}
$issued_successfully = false;
- if ((@file_exists($ssl_crt_file) && ($crt_subject == $crt_issuer)) || (!@is_dir($acme_cert_dir) || !@file_exists($check_acme_file) || !@file_exists($ssl_crt_file) || md5_file($check_acme_file) != md5_file($ssl_crt_file)) && $ip_address_match == true) {
+ // if we have certbot or acme.sh, the required DNS records and our desired certificate is not the current one, try to get it
+ if(
+ ($acme || $certbot) && $ip_address_match
+ && ($self_signed ||
+ (!@is_dir($acme_cert_dir) || !@file_exists($check_acme_file) || !@file_exists($ssl_crt_file) || md5_file($check_acme_file) != md5_file($ssl_crt_file)))
+ ) {
// This script is needed earlier to check and open http port 80 or standalone might fail
// Make executable and temporary symlink latest letsencrypt pre, post and renew hook script before install
@@ -3104,48 +3204,19 @@ class installer_base extends stdClass {
chmod('/usr/local/bin/letsencrypt_renew_hook.sh', 0700);
}
- // Check http port 80 status as it cannot be determined at post hook stage
- $port80_status=exec('true &>/dev/null 0) print "open"; else print "close";}\'');
- // Set pre-, post- and renew hook
- $pre_hook = "--pre-hook \"letsencrypt_pre_hook.sh\"";
- $renew_hook = " --renew-hook \"letsencrypt_renew_hook.sh\"";
+ // Set pre-, post- and renew hook (acme.sh and certbot use the same arguments)
+ $pre_hook = '--pre-hook "letsencrypt_pre_hook.sh"';
+ $renew_hook = ' --renew-hook "letsencrypt_renew_hook.sh"';
if($port80_status == 'close') {
- $post_hook = " --post-hook \"letsencrypt_post_hook.sh\"";
+ $post_hook = ' --post-hook "letsencrypt_post_hook.sh"';
$hook = $pre_hook . $post_hook . $renew_hook;
} else {
$hook = $pre_hook . $renew_hook;
}
- $which_certbot = shell_exec('which certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot letsencrypt');
-
- // Get the default LE client name and version
- $le_client = explode("\n", $which_certbot ? $which_certbot : '');
- $le_client = reset($le_client);
-
- $which_acme = shell_exec('which acme.sh /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh');
- // Check for Neilpang acme.sh as well
- $acme = explode("\n", $which_acme ? $which_acme : '');
- $acme = reset($acme);
-
- if((!$acme || !is_executable($acme)) && (!$le_client || !is_executable($le_client))) {
- $success = $this->install_acme();
- if(!$success) {
- swriteln('Failed installing acme.sh. Will not be able to issue certificate during install.');
- } else {
- $acme = explode("\n", shell_exec('which acme.sh /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh'));
- $acme = reset($acme);
- if($acme && is_executable($acme)) {
- swriteln('Installed acme.sh and using it for certificate creation during install.');
-
- // we do this even on install to enable automatic updates
- $this->update_acme();
- } else {
- swriteln('Failed installing acme.sh. Will not be able to issue certificate during install.');
- }
- }
- }
-
$restore_conf_symlink = false;
// we only need this for apache, so use fixed conf index
@@ -3158,18 +3229,18 @@ class installer_base extends stdClass {
$server = 'nginx';
} elseif($conf['apache']['installed'] == true) {
swriteln('Using apache for certificate validation');
- if($this->is_update == false && @is_link($vhost_conf_enabled_dir.'/000-ispconfig.conf')) {
+ if($this->is_update == false && @is_link($vhost_conf_enabled_dir . '/000-ispconfig.conf')) {
$restore_conf_symlink = true;
- unlink($vhost_conf_enabled_dir.'/000-ispconfig.conf');
+ unlink($vhost_conf_enabled_dir . '/000-ispconfig.conf');
}
$server = 'apache';
}
if($conf[$server]['installed'] == true && $conf[$server]['init_script'] != '') {
if($this->is_update) {
- system($this->getinitcommand($conf[$server]['init_script'], 'force-reload').' &> /dev/null || ' . $this->getinitcommand($conf[$server]['init_script'], 'restart').' &> /dev/null');
+ system($this->getinitcommand($conf[$server]['init_script'], 'force-reload') . ' &> /dev/null || ' . $this->getinitcommand($conf[$server]['init_script'], 'restart') . ' &> /dev/null');
} else {
- system($this->getinitcommand($conf[$server]['init_script'], 'restart').' &> /dev/null');
+ system($this->getinitcommand($conf[$server]['init_script'], 'restart') . ' &> /dev/null');
}
}
@@ -3188,24 +3259,20 @@ class installer_base extends stdClass {
// - actual file copied to tmp name.
// if cert request is successful, rename tmp copy to perm rename;
// if cert request fails, delete tmp copy
- $cert_files = array( $ssl_crt_file, $ssl_key_file, $ssl_pem_file );
- foreach ($cert_files as $f) {
- if (is_link($f) && ! file_exists($f)) {
- rename($f, $f.'-'.$date->format('YmdHis').'.bak');
- } elseif (is_link($f)) {
- rename($f, $f.'-temporary.bak');
- copy($f.'-temporary.bak', $f);
+ $cert_files = array($ssl_crt_file, $ssl_key_file, $ssl_pem_file);
+ foreach($cert_files as $f) {
+ if(is_link($f) && !file_exists($f)) {
+ rename($f, $f . '-' . $date->format('YmdHis') . '.bak');
+ } elseif(is_link($f)) {
+ rename($f, $f . '-temporary.bak');
+ copy($f . '-temporary.bak', $f);
} elseif(file_exists($f)) {
- copy($f, $f.'-temporary.bak');
+ copy($f, $f . '-temporary.bak');
}
}
// Attempt to use Neilpang acme.sh first, as it is now the preferred LE client
- if (is_executable($acme)) {
- $acme_cert_dir = dirname($acme) . '/' . $hostname;
-
- swriteln('acme.sh is installed, overriding certificate path to use ' . $acme_cert_dir);
-
+ if($acme) {
# acme.sh does not set umask, resulting in incorrect permissions (ispconfig issue #6015)
$old_umask = umask(0022);
@@ -3215,13 +3282,11 @@ class installer_base extends stdClass {
$out = null;
$ret = null;
if($conf['nginx']['installed'] == true || $conf['apache']['installed'] == true) {
- exec("$acme --issue --keylength 4096 --log $acme_log -w /usr/local/ispconfig/interface/acme -d " . escapeshellarg($hostname) . " $renew_hook", $out, $ret);
- }
- // Else, it is not webserver, so we use standalone
- else {
- exec("$acme --issue --keylength 4096 --log $acme_log --standalone -d " . escapeshellarg($hostname) . " $hook", $out, $ret);
+ exec("$acme --issue --keylength ec-256 --ecc --log $acme_log -w /usr/local/ispconfig/interface/acme -d " . escapeshellarg($hostname) . " $renew_hook", $out, $ret);
+ } else { // Else, it is not webserver, so we use standalone
+ exec("$acme --issue --keylength ec-256 --ecc --log $acme_log --standalone -d " . escapeshellarg($hostname) . " $hook", $out, $ret);
}
-
+ umask($old_umask);
if($ret == 0 || ($ret == 2 && file_exists($check_acme_file))) {
// acme.sh returns with 2 on issue for already existing certificate
@@ -3231,117 +3296,89 @@ class installer_base extends stdClass {
//$acme_cert = "--cert-file $acme_cert_dir/cert.pem";
$acme_key = "--key-file " . escapeshellarg($ssl_key_file);
$acme_chain = "--fullchain-file " . escapeshellarg($ssl_crt_file);
- exec("$acme --install-cert --log $acme_log -d " . escapeshellarg($hostname) . " $acme_key $acme_chain");
+ exec("$acme --install-cert --log $acme_log -d " . escapeshellarg($hostname) . " --ecc $acme_key $acme_chain");
$issued_successfully = true;
- umask($old_umask);
-
- // Make temporary backup of self-signed certs permanent
- foreach ($cert_files as $f) {
- if (is_link($f.'-temporary.bak')) {
- unlink($f.'-temporary.bak');
- } elseif(file_exists($f.'-temporary.bak')) {
- rename($f.'-temporary.bak', $f.'-'.$date->format('YmdHis').'.bak');
- }
- }
-
} else {
swriteln('Issuing certificate via acme.sh failed. Please check that your hostname can be verified by letsencrypt');
-
- umask($old_umask);
-
- // Restore/cleanup temporary backup of self-signed certs
- foreach ($cert_files as $f) {
- if (is_link($f.'-temporary.bak')) {
- @unlink($f);
- rename($f.'-temporary.bak', $f);
- } elseif(file_exists($f.'-temporary.bak')) {
- unlink($f.'-temporary.bak');
- }
- }
}
- // Else, we attempt to use the official LE certbot client certbot
- } else {
-
- // But only if it is otherwise available
- if(is_executable($le_client)) {
- $out = null;
- $ret = null;
- // Get its version info due to be used for webroot arguement issues
- $le_info = exec($le_client . ' --version 2>&1', $ret, $val);
- if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $le_info, $matches)) {
- $le_version = $matches[2];
- }
+ // Else, we attempt to use the official LE certbot client certbot
+ } else {
+ $out = null;
+ $ret = null;
- // Define certbot commands
- $acme_version = '--server https://acme-v0' . (($le_version >=0.22) ? '2' : '1') . '.api.letsencrypt.org/directory';
+ if(version_compare($certbot_version, '0.22', '>=')) {
+ $acme_version = '--server https://acme-v02.api.letsencrypt.org/directory';
+ } else {
+ $acme_version = '--server https://acme-v01.api.letsencrypt.org/directory';
+ }
+ if(version_compare($certbot_version, '2.0', '>=')) {
+ $certonly = 'certonly --agree-tos --non-interactive --expand --cert-name ' . escapeshellarg($hostname . '_ecc') . ' --elliptic-curve secp256r1';
+ } elseif(version_compare($certbot_version, '0.30', '>=')) {
+ $certonly = 'certonly --agree-tos --non-interactive --expand --cert-name ' . escapeshellarg($hostname) . ' --rsa-key-size 4096';
+ } else {
$certonly = 'certonly --agree-tos --non-interactive --expand --rsa-key-size 4096';
+ }
- // If this is a webserver
- if($conf['nginx']['installed'] == true || $conf['apache']['installed'] == true) {
- exec("$le_client $certonly $acme_version --authenticator webroot --webroot-path /usr/local/ispconfig/interface/acme --email " . escapeshellarg('postmaster@' . $hostname) . " -d " . escapeshellarg($hostname) . " $renew_hook", $out, $ret);
- }
- // Else, it is not webserver, so we use standalone
- else {
- exec("$le_client $certonly $acme_version --standalone --email " . escapeshellarg('postmaster@' . $hostname) . " -d " . escapeshellarg($hostname) . " $hook", $out, $ret);
- }
-
- if($ret == 0) {
- // certbot returns with 0 on issue for already existing certificate
-
- $acme_cert_dir = '/etc/letsencrypt/live/' . $hostname;
- foreach (array( $ssl_crt_file, $ssl_key_file) as $f) {
- if (file_exists($f) && ! is_link($f)) {
- unlink($f);
- }
- }
- symlink($acme_cert_dir . '/fullchain.pem', $ssl_crt_file);
- symlink($acme_cert_dir . '/privkey.pem', $ssl_key_file);
-
- $issued_successfully = true;
+ // If this is a webserver
+ if($conf['nginx']['installed'] == true || $conf['apache']['installed'] == true) {
+ exec("$certbot $certonly $acme_version --authenticator webroot --webroot-path /usr/local/ispconfig/interface/acme --email " . escapeshellarg('postmaster@' . $hostname) . " -d " . escapeshellarg($hostname) . " $renew_hook", $out, $ret);
+ } else { // Else, it is not webserver, so we use standalone
+ exec("$certbot $certonly $acme_version --standalone --email " . escapeshellarg('postmaster@' . $hostname) . " -d " . escapeshellarg($hostname) . " $hook", $out, $ret);
+ }
- // Make temporary backup of self-signed certs permanent
- foreach ($cert_files as $f) {
- if (is_link($f.'-temporary.bak')) {
- unlink($f.'-temporary.bak');
- } elseif(file_exists($f.'-temporary.bak')) {
- rename($f.'-temporary.bak', $f.'-'.$date->format('YmdHis').'.bak');
- }
- }
+ if($ret == 0 && is_dir($acme_cert_dir)) {
+ // certbot returns with 0 on issue for already existing certificate
- } else {
- swriteln('Issuing certificate via certbot failed. Please check log files and make sure that your hostname can be verified by letsencrypt');
-
- // Restore/cleanup temporary backup of self-signed certs
- foreach ($cert_files as $f) {
- if (is_link($f.'-temporary.bak')) {
- @unlink($f);
- rename($f.'-temporary.bak', $f);
- } elseif(file_exists($f.'-temporary.bak')) {
- unlink($f.'-temporary.bak');
- }
+ foreach(array($ssl_crt_file, $ssl_key_file) as $f) {
+ if(file_exists($f) && !is_link($f)) {
+ unlink($f);
}
-
}
+ symlink($acme_cert_dir . '/fullchain.pem', $ssl_crt_file);
+ symlink($acme_cert_dir . '/privkey.pem', $ssl_key_file);
+
+ $issued_successfully = true;
} else {
- swriteln('Did not find any valid acme client (acme.sh or certbot)');
+ swriteln('Issuing certificate via certbot failed. Please check log files and make sure that your hostname can be verified by letsencrypt');
}
}
if($restore_conf_symlink) {
- if(!@is_link($vhost_conf_enabled_dir.'/000-ispconfig.conf')) {
- symlink($vhost_conf_dir.'/ispconfig.conf', $vhost_conf_enabled_dir.'/000-ispconfig.conf');
+ if(!@is_link($vhost_conf_enabled_dir . '/000-ispconfig.conf')) {
+ symlink($vhost_conf_dir . '/ispconfig.conf', $vhost_conf_enabled_dir . '/000-ispconfig.conf');
+ }
+ }
+
+ if($issued_successfully) {
+ // Make temporary backup of self-signed certs permanent
+ foreach($cert_files as $f) {
+ if(is_link($f . '-temporary.bak')) {
+ unlink($f . '-temporary.bak');
+ } elseif(file_exists($f . '-temporary.bak')) {
+ rename($f . '-temporary.bak', $f . '-' . $date->format('YmdHis') . '.bak');
+ }
+ }
+ } else {
+ // Restore/cleanup temporary backup of self-signed certs
+ foreach($cert_files as $f) {
+ if(is_link($f . '-temporary.bak')) {
+ @unlink($f);
+ rename($f . '-temporary.bak', $f);
+ } elseif(file_exists($f . '-temporary.bak')) {
+ unlink($f . '-temporary.bak');
+ }
}
}
} else {
if($ip_address_match) {
- // the directory already exists so we have to assume that it was created previously
+ // the directory already exists, so we have to assume that it was created previously
$issued_successfully = true;
}
}
// If the LE SSL certs for this hostname exists
- if(!is_dir($acme_cert_dir) || !file_exists($check_acme_file) || !isset($issued_successfully) || !$issued_successfully) {
+ if(!is_dir($acme_cert_dir) || !file_exists($check_acme_file) || !$issued_successfully) {
if(!$issued_successfully) {
swriteln('Could not issue letsencrypt certificate, falling back to self-signed.');
} else {
@@ -3350,7 +3387,7 @@ class installer_base extends stdClass {
// We can still use the old self-signed method
$openssl_cmd = 'openssl req -nodes -newkey rsa:4096 -x509 -days 3650 -keyout ' . escapeshellarg($ssl_key_file) . ' -out ' . escapeshellarg($ssl_crt_file);
- if(AUTOINSTALL){
+ if(AUTOINSTALL) {
$openssl_cmd .= ' -subj ' . escapeshellarg('/C=' . $autoinstall['ssl_cert_country'] . '/ST=' . $autoinstall['ssl_cert_state'] . '/L=' . $autoinstall['ssl_cert_locality'] . '/O=' . $autoinstall['ssl_cert_organisation'] . '/OU=' . $autoinstall['ssl_cert_organisation_unit'] . '/CN=' . $autoinstall['ssl_cert_common_name']);
}
exec($openssl_cmd);
@@ -3361,18 +3398,18 @@ class installer_base extends stdClass {
exec("cat $ssl_key_file $ssl_crt_file > $ssl_pem_file; chmod 600 $ssl_pem_file");
// Extend LE SSL certs to postfix
- if ($conf['postfix']['installed'] == true && strtolower($this->simple_query('Symlink ISPConfig SSL certs to Postfix?', array('y', 'n'), 'y','ispconfig_postfix_ssl_symlink')) == 'y') {
+ if($conf['postfix']['installed'] == true && strtolower($this->simple_query('Symlink ISPConfig SSL certs to Postfix?', array('y', 'n'), 'y', 'ispconfig_postfix_ssl_symlink')) == 'y') {
// Define folder, file(s)
$cf = $conf['postfix'];
$postfix_dir = $cf['config_dir'];
if(!is_dir($postfix_dir)) $this->error("The Postfix configuration directory '$postfix_dir' does not exist.");
- $smtpd_crt = $postfix_dir.'/smtpd.cert';
- $smtpd_key = $postfix_dir.'/smtpd.key';
+ $smtpd_crt = $postfix_dir . '/smtpd.cert';
+ $smtpd_key = $postfix_dir . '/smtpd.key';
// Backup existing postfix ssl files
- if (file_exists($smtpd_crt)) rename($smtpd_crt, $smtpd_crt . '-' .$date->format('YmdHis') . '.bak');
- if (file_exists($smtpd_key)) rename($smtpd_key, $smtpd_key . '-' .$date->format('YmdHis') . '.bak');
+ if(file_exists($smtpd_crt)) rename($smtpd_crt, $smtpd_crt . '-' . $date->format('YmdHis') . '.bak');
+ if(file_exists($smtpd_key)) rename($smtpd_key, $smtpd_key . '-' . $date->format('YmdHis') . '.bak');
// Create symlink to ISPConfig SSL files
symlink($ssl_crt_file, $smtpd_crt);
@@ -3380,26 +3417,25 @@ class installer_base extends stdClass {
}
// Extend LE SSL certs to pureftpd
- if ($conf['pureftpd']['installed'] == true && strtolower($this->simple_query('Symlink ISPConfig SSL certs to Pure-FTPd? Creating dhparam file may take some time.', array('y', 'n'), 'y','ispconfig_pureftpd_ssl_symlink')) == 'y') {
+ if($conf['pureftpd']['installed'] == true && strtolower($this->simple_query('Symlink ISPConfig SSL certs to Pure-FTPd? Creating dhparam file may take some time.', array('y', 'n'), 'y', 'ispconfig_pureftpd_ssl_symlink')) == 'y') {
// Define folder, file(s)
$pureftpd_dir = '/etc/ssl/private';
if(!is_dir($pureftpd_dir)) mkdir($pureftpd_dir, 0755, true);
- $pureftpd_pem = $pureftpd_dir.'/pure-ftpd.pem';
+ $pureftpd_pem = $pureftpd_dir . '/pure-ftpd.pem';
// Backup existing pureftpd ssl files
- if (file_exists($pureftpd_pem)) rename($pureftpd_pem, $pureftpd_pem . '-' .$date->format('YmdHis') . '.bak');
+ if(file_exists($pureftpd_pem)) rename($pureftpd_pem, $pureftpd_pem . '-' . $date->format('YmdHis') . '.bak');
// Create symlink to ISPConfig SSL files
symlink($ssl_pem_file, $pureftpd_pem);
- if (!file_exists("$pureftpd_dir/pure-ftpd-dhparams.pem"))
- symlink('/usr/local/ispconfig/interface/ssl/dhparam4096.pem', $pureftpd_dir.'/pure-ftpd-dhparams.pem');
- //exec("cd $pureftpd_dir; openssl dhparam -out dhparam2048.pem 2048; ln -sf dhparam2048.pem pure-ftpd-dhparams.pem");
+ if(!file_exists("$pureftpd_dir/pure-ftpd-dhparams.pem"))
+ symlink('/usr/local/ispconfig/interface/ssl/dhparam4096.pem', $pureftpd_dir . '/pure-ftpd-dhparams.pem');
+ //exec("cd $pureftpd_dir; openssl dhparam -out dhparam2048.pem 2048; ln -sf dhparam2048.pem pure-ftpd-dhparams.pem");
}
}
exec("chown -R root:root $ssl_dir");
-
}
public function install_ispconfig() {
diff --git a/install/tpl/server.ini.master b/install/tpl/server.ini.master
index 49971f80f6b0cb8a10186847d5e2adda82c75839..a82a486c2708d4e715a79455b0ba9aed39eb0e93 100644
--- a/install/tpl/server.ini.master
+++ b/install/tpl/server.ini.master
@@ -142,6 +142,11 @@ vhost_proxy_protocol_enabled=n
vhost_proxy_protocol_protocols=ipv4
vhost_proxy_protocol_http_port=880
vhost_proxy_protocol_https_port=8443
+le_signature_type=ECDSA
+le_delete_on_site_remove=y
+le_auto_cleanup=y
+le_revoke_before_delete=y
+le_auto_cleanup_denylist=[server_name]
[dns]
bind_user=root
diff --git a/interface/lib/classes/validate_domain.inc.php b/interface/lib/classes/validate_domain.inc.php
index 3555135eae028efd7163769ff41c6e6808500da9..0378b2ee204c0a8d29821fc1e960797fbd78d04f 100644
--- a/interface/lib/classes/validate_domain.inc.php
+++ b/interface/lib/classes/validate_domain.inc.php
@@ -278,5 +278,115 @@ class validate_domain {
return true; // admin may always add wildcard domain
}
+ /**
+ * Parses $expression to check if it is a valid shell glob pattern.
+ * Does not support extended glob matching syntax.
+ *
+ * @see https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html
+ * @param string $expression
+ * @param string $allowed_chars regexp of allowed characters in expression. ? and * are always allowed
+ * @param string|null $allowed_brace_chars regexp of allowed characters in brace ([...]). Dash is always allowed. If empty, then $allowed_chars will be used
+ * @return bool
+ */
+ private function validate_glob($expression, $allowed_chars = '/^.$/u', $allowed_brace_chars = null) {
+ $escaping = false;
+ $in_brace = false;
+ $brace_content = [];
+ $chars = preg_split('//u', $expression, -1, PREG_SPLIT_NO_EMPTY);
+ foreach($chars as $i => $c) {
+ if($in_brace) {
+ // the first char after brace start can be a ].
+ if(($c == ']' && empty($brace_content)) || $c != ']') {
+ $brace_content[] = $c;
+ } else {
+ $in_brace = false;
+ $last_is_dash = false;
+ foreach($brace_content as $bi => $bc) {
+ // dashes are always allowed
+ if($bc == '-') {
+ // ... but we consider consecutive dashes as invalid
+ if($last_is_dash) {
+ return false;
+ }
+ // ... and need to validate it as allowed char when it is first or last
+ if(($bi == 0 || $bi == count($brace_content) - 1) && !preg_match($allowed_brace_chars ?: $allowed_chars, '-')) {
+ return false;
+ }
+ $last_is_dash = true;
+ } else {
+ $last_is_dash = false;
+ // negate chars are always allowed
+ if($bi == 0 && ($bc == '^' || $bc == '!') && count($brace_content) > 1) {
+ continue;
+ }
+ if(!preg_match($allowed_brace_chars ?: $allowed_chars, $bc)) {
+ return false;
+ }
+ }
+ }
+ }
+ } else {
+ $peek = $i == count($chars) - 1 ? '' : $chars[$i + 1];
+ if($c == '\\' && in_array($peek, ['[', ']', '*', '?'])) {
+ $escaping = true;
+ continue;
+ } elseif($c == '[' && !$escaping) {
+ $in_brace = true;
+ $brace_content = [];
+ } elseif($escaping || ($c != '?' && $c != '*')) {
+ if(!preg_match($allowed_chars, $c)) {
+ return false;
+ }
+ }
+ $escaping = false;
+ }
+ }
+ return !$in_brace && !$escaping;
+ }
+
+ /**
+ * Validates that input is a comma separated list of domain globs.
+ * Can be used for fnmatch() as input.
+ */
+ function domain_glob_list($field_name, $field_value, $validator) {
+ global $app;
+ $allowempty = $validator['allowempty'] ?: 'n';
+ $exceptions = $validator['exceptions'] ?: [];
+ $allow_exception_as_substring = $validator['allow_exception_as_substring'] ?: 'y';
+ if(!$field_value) {
+ if($allowempty == 'y') {
+ return '';
+ }
+ return $this->get_error($validator['errmsg']);
+ }
+ $parts = explode(',', $field_value);
+ foreach($parts as $part) {
+ $part = trim($part);
+ // an empty part means there is a stray comma
+ if(empty($part)) {
+ return $this->get_error($validator['errmsg']);
+ }
+ // allow list placeholders that you will replace with real values at evaluation
+ if(in_array($part, $exceptions, true)) {
+ continue;
+ }
+ // optionally do not allow placeholders to be part of an expression
+ if($allow_exception_as_substring == 'n') {
+ foreach($exceptions as $exception) {
+ if(strpos($part, $exception) !== false) {
+ return $this->get_error($validator['errmsg']);
+ }
+ }
+ }
+ // A domain glob needs to:
+ // * be a valid glob with only a-z0-9._- as characters
+ // * have at least one dot in it
+ // * not have two consecutive dots
+ if(!$this->validate_glob($part, '/^[a-z0-9._-]$/ui') || strpos($part, '.') === false || strpos($part, '..') !== false) {
+ return $this->get_error($validator['errmsg']);
+ }
+ }
+ return '';
+ }
}
diff --git a/interface/web/admin/form/server_config.tform.php b/interface/web/admin/form/server_config.tform.php
index 5dd1a6d7f991bd93ce4cd1bacc5de2930f816d63..b6f21239d2c862197d35d4abb9562b5d00d8619a 100644
--- a/interface/web/admin/form/server_config.tform.php
+++ b/interface/web/admin/form/server_config.tform.php
@@ -574,15 +574,6 @@ $form["tabs"]['mail'] = array(
'default' => '2048',
'value' => array('1024' => 'weak (1024)', '2048' => 'normal (2048)', '4096' => 'strong (4096)')
),
- 'relayhost_password' => array(
- 'datatype' => 'VARCHAR',
- 'formtype' => 'TEXT',
- 'default' => '',
- 'value' => '',
- 'width' => '40',
- 'maxlength' => '255'
- ),
-
'pop3_imap_daemon' => array(
'datatype' => 'VARCHAR',
'formtype' => 'SELECT',
@@ -1642,6 +1633,49 @@ $form["tabs"]['web'] = array(
'width' => '40',
'maxlength' => '255'
),
+ 'le_signature_type' => array(
+ 'datatype' => 'VARCHAR',
+ 'formtype' => 'SELECT',
+ 'default' => 'ECDSA',
+ 'value' => array('RSA' => 'RSA (RSA encryption with SHA-256)', 'ECDSA' => 'ECDSA (Elliptic Curve Digital Signature Algorithm)')
+ ),
+ 'le_delete_on_site_remove' => array(
+ 'datatype' => 'VARCHAR',
+ 'formtype' => 'CHECKBOX',
+ 'default' => 'y',
+ 'value' => array(0 => 'n', 1 => 'y')
+ ),
+ 'le_auto_cleanup' => array(
+ 'datatype' => 'VARCHAR',
+ 'formtype' => 'CHECKBOX',
+ 'default' => 'y',
+ 'value' => array(0 => 'n', 1 => 'y')
+ ),
+ 'le_auto_cleanup_denylist' => array(
+ 'validators' => array(
+ array (
+ 'type' => 'CUSTOM',
+ 'class' => 'validate_domain',
+ 'function' => 'domain_glob_list',
+ 'allowempty' => 'y',
+ 'exceptions' => array('[server_name]'),
+ 'allow_exception_as_substring' => 'n',
+ 'errmsg'=> 'le_auto_cleanup_denylist_error_custom'
+ ),
+ ),
+ 'datatype' => 'VARCHAR',
+ 'formtype' => 'TEXT',
+ 'default' => '[server_name]',
+ 'value' => '',
+ 'width' => '40',
+ 'maxlength' => '255'
+ ),
+ 'le_revoke_before_delete' => array(
+ 'datatype' => 'VARCHAR',
+ 'formtype' => 'CHECKBOX',
+ 'default' => 'y',
+ 'value' => array(0 => 'n', 1 => 'y')
+ ),
//#################################
// END Datatable fields
//#################################
diff --git a/interface/web/admin/lib/lang/ar_server_config.lng b/interface/web/admin/lib/lang/ar_server_config.lng
index 4a0d25ff05df17b2aaac59facb9888bc1f5bfedf..f9f79b2e6d45acbee53727eb8cca90b70aa8a00d 100644
--- a/interface/web/admin/lib/lang/ar_server_config.lng
+++ b/interface/web/admin/lib/lang/ar_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Número de copias de seguridad del sistema';
$wb['sysbackup_copies_error_empty'] = 'El número de copias de seguridad del sistema no debe estar vacío';
$wb['sysbackup_copies_error_regex'] = 'El número de copias de seguridad del sistema debe ser un número entre 1 y 3';
$wb['sysbackup_copies_note_txt'] = '(0 = desactivado)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/bg_server_config.lng b/interface/web/admin/lib/lang/bg_server_config.lng
index 2eae0f276d97108c607251bf0fec6000aa267059..18c23d714751476d673b9a3bda209935123bef1c 100644
--- a/interface/web/admin/lib/lang/bg_server_config.lng
+++ b/interface/web/admin/lib/lang/bg_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/br_server_config.lng b/interface/web/admin/lib/lang/br_server_config.lng
index 6ead4c174f97931e69c2d8e142696a737ffc677a..e8140025ada7e6358a5d57d74d41d058c915e8e2 100644
--- a/interface/web/admin/lib/lang/br_server_config.lng
+++ b/interface/web/admin/lib/lang/br_server_config.lng
@@ -364,3 +364,10 @@ $wb['sysbackup_copies_error_empty'] = 'O número de copias de seguridad do siste
$wb['sysbackup_copies_error_regex'] = 'O número de copias de seguridad do sistema deve ser um número entre 1 e 3';
$wb['sysbackup_copies_note_txt'] = '(0 = desativado)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/ca_server_config.lng b/interface/web/admin/lib/lang/ca_server_config.lng
index 972a624a137c6a3695f819f1cea7e523f5eb1d65..1c75cae586604cea4cb798a2ff4bf1826f8856cd 100644
--- a/interface/web/admin/lib/lang/ca_server_config.lng
+++ b/interface/web/admin/lib/lang/ca_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/cn_server_config.lng b/interface/web/admin/lib/lang/cn_server_config.lng
index 7f3f69b16c02aa6e9f98f972d629065b4d17c3c3..b49a6e399aaecc3ea1de86175a701256e2ec6f9e 100644
--- a/interface/web/admin/lib/lang/cn_server_config.lng
+++ b/interface/web/admin/lib/lang/cn_server_config.lng
@@ -52,13 +52,8 @@ $wb['relayhost_txt'] = '中继主机';
$wb['relayhost_user_txt'] = '中继主机用户名';
$wb['relayhost_password_txt'] = '中继主机密码';
$wb['reject_sender_login_mismatch_txt'] = '拒绝发件人和登录用户不匹配';
-$wb['reject_unknown_txt'] = '拒绝未知主机名';
-$wb['tooltip_reject_unknown_txt'] = '需要主机名通过DNS检查。对经过身份验证的用户不进行检查。';
$wb['reject_unknown_txt'] = '拒绝未知的helo主机名';
$wb['tooltip_reject_unknown_txt'] = '需要主机名通过DNS检测。对已认证用户不进行检查。';
-$wb['reject_unknown_helo_txt'] = '拒绝未知的 helo 主机名';
-$wb['reject_unknown_client_txt'] = '拒绝未知的客户端主机名';
-$wb['reject_unknown_client_helo_txt'] = '拒绝未知的helo和客户端主机名';
$wb['reject_unknown_helo_txt'] = '拒绝未知的helo主机名';
$wb['reject_unknown_client_txt'] = '拒绝未知的客户端主机名';
$wb['reject_unknown_client_helo_txt'] = '拒绝未知的helo和客户端主机名';
@@ -241,13 +236,12 @@ $wb['overquota_db_notify_threshold_txt'] = 'DB配额警告使用水平';
$wb['overquota_db_notify_admin_txt'] = '向管理员发送DB配额警告';
$wb['overquota_db_notify_reseller_txt'] = '向经销商发送DB配额警告';
$wb['overquota_db_notify_client_txt'] = '向客户发送DB配额警告';
-$wb['monitor_system_updates_txt'] = '检查Linux更新';
+$wb['monitor_system_updates_txt'] = '检查 Linux 更新';
$wb['php_handler_txt'] = '默认的 PHP 处理器';
$wb['php_fpm_default_chroot_txt'] = '默认的 PHP-FPM chroot';
$wb['php_fpm_incron_reload_txt'] = '安装 incron 触发器文件以重新加载 PHP-FPM';
$wb['disabled_txt'] = '已禁用';
$wb['dkim_strength_txt'] = 'DKIM 强度';
-$wb['monitor_system_updates_txt'] = '检查 Linux 更新';
$wb['invalid_apache_user_txt'] = '无效的 Apache 用户。';
$wb['invalid_apache_group_txt'] = '无效的 Apache 组。';
$wb['backup_dir_error_regex'] = '无效的备份目录。';
@@ -363,3 +357,16 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['dkim_path_txt'] = 'DKIM Path';
+$wb['bind_zonefiles_masterprefix_txt'] = 'BIND master zonefiles prefix';
+$wb['bind_zonefiles_slaveprefix_txt'] = 'BIND slave zonefiles prefix';
+$wb['bind_zonefiles_masterprefix_error_regex'] = 'Invalid BIND zonefiles master prefix.';
+$wb['bind_zonefiles_slaveprefix_error_regex'] = 'Invalid BIND zonefiles slave prefix.';
+$wb['vhost_proxy_protocol_protocols_txt'] = 'Use PROXY Protocol on';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/cz_server_config.lng b/interface/web/admin/lib/lang/cz_server_config.lng
index e89c37d2069a519a6439f6f94ec527365bd299dd..14d217721da1eaba4e7f695aaebc8650777c6d5c 100644
--- a/interface/web/admin/lib/lang/cz_server_config.lng
+++ b/interface/web/admin/lib/lang/cz_server_config.lng
@@ -150,7 +150,7 @@ $wb['php_fpm_socket_dir_error_empty'] = 'PHP-FPM adresář pro socket je prázdn
$wb['try_rescue_txt'] = 'Povolit monitorování služeb a restartovat při selhání';
$wb['do_not_try_rescue_mysql_txt'] = 'Zakázat MySQL monitorování';
$wb['do_not_try_rescue_mail_txt'] = 'Zakázat E-mail monitorování';
-$wb['rescue_description_txt'] = 'Informace: Pokud chcete např. vypnout MySQL službu zatrhněte políčko \"Zakázat MySQL monitorování\" změna se provede do 2-3 minut.
Pokud nepočkáte 2-3 minuty, monitorování nastartuje službu MySQL automaticky znovu !';
+$wb['rescue_description_txt'] = 'Informace: Pokud chcete např. vypnout MySQL službu zatrhněte políčko "Zakázat MySQL monitorování" změna se provede do 2-3 minut.
Pokud nepočkáte 2-3 minuty, monitorování nastartuje službu MySQL automaticky znovu !';
$wb['enable_sni_txt'] = 'Aktivovat SNI (Server Name Indication)';
$wb['do_not_try_rescue_httpd_txt'] = 'Zakázat HTTPD monitorování';
$wb['set_folder_permissions_on_update_txt'] = 'Nastavení oprávnění složky při aktualizaci';
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/de_server_config.lng b/interface/web/admin/lib/lang/de_server_config.lng
index f84771445363e97fc46ee07810b52e3d21db0c30..09c16f1d7c59767f33214dd0b48a45638b124ca8 100644
--- a/interface/web/admin/lib/lang/de_server_config.lng
+++ b/interface/web/admin/lib/lang/de_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Anzahl der ISPConfig Sicherungen (0 = aus)';
$wb['sysbackup_copies_error_empty'] = 'Anzahl der ISPConfig Sicherungen darf nicht leer sein';
$wb['sysbackup_copies_error_regex'] = 'Anzahl der ISPConfig Sicherungen muss eine Zahl zwischen 1 und 3 sein';
$wb['sysbackup_copies_note_txt'] = '(0 = aus)';
+$wb['le_signature_type_txt'] = 'Zertifikat Signaturtyp';
+$wb['le_auto_cleanup_txt'] = 'Entferne unbenutzte Let\'s Encrypt Zertifikate automatisch';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains die nie entfernt werden sollen';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Komma-separierte Liste von Domain-Globs, die niemals entfernt werden sollen.
Z.B. mail.*, externally-managed.example.com
Platzhalter:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Domain-Glob Liste ungültig';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/dk_server_config.lng b/interface/web/admin/lib/lang/dk_server_config.lng
index 965a1d897f75f25b89e22a9f2cf7b17aba697a01..fe60600b64932b7ea3efc23282dcf535dcbcfe37 100644
--- a/interface/web/admin/lib/lang/dk_server_config.lng
+++ b/interface/web/admin/lib/lang/dk_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/el_server_config.lng b/interface/web/admin/lib/lang/el_server_config.lng
index ae8b948c76e4f79d6cb416e107d52efd63a8fd8c..830ff7e7bda63283fe7a014c8fab833d9a408cdf 100644
--- a/interface/web/admin/lib/lang/el_server_config.lng
+++ b/interface/web/admin/lib/lang/el_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/en_server_config.lng b/interface/web/admin/lib/lang/en_server_config.lng
index b14271c319e7853facd1b797fd6457fd3a5d0d6b..7511ab258b0811a2bbfbd600c6eb8a6b0a13ac37 100644
--- a/interface/web/admin/lib/lang/en_server_config.lng
+++ b/interface/web/admin/lib/lang/en_server_config.lng
@@ -370,3 +370,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/es_server_config.lng b/interface/web/admin/lib/lang/es_server_config.lng
index e42fb1c9929cae7f4fc38eb621b76c7320170823..62be233b8e9c47dca745ea7f803b93a6a6a13321 100644
--- a/interface/web/admin/lib/lang/es_server_config.lng
+++ b/interface/web/admin/lib/lang/es_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Número de copias de seguridad del sistema';
$wb['sysbackup_copies_error_empty'] = 'El número de copias de seguridad del sistema no debe estar vacío';
$wb['sysbackup_copies_error_regex'] = 'El número de copias de seguridad del sistema debe ser un número entre 1 y 3';
$wb['sysbackup_copies_note_txt'] = '(0 = desactivado)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/fi_server_config.lng b/interface/web/admin/lib/lang/fi_server_config.lng
index 1eba458c5fec1b29864b8701fe13ffe0a4969c0e..1c9032be94814378597863a9bf73de206ae423b2 100644
--- a/interface/web/admin/lib/lang/fi_server_config.lng
+++ b/interface/web/admin/lib/lang/fi_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/fr_server_config.lng b/interface/web/admin/lib/lang/fr_server_config.lng
index 60508917ee6df7aa471be863fdae06362df4f111..e83bf96b8d0b86450aa811a5b95880a9b87acea9 100644
--- a/interface/web/admin/lib/lang/fr_server_config.lng
+++ b/interface/web/admin/lib/lang/fr_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Nombre de sauvegardes système';
$wb['sysbackup_copies_error_empty'] = 'Le nombre de sauvegardes système ne doit pas être vide';
$wb['sysbackup_copies_error_regex'] = 'Le nombre de sauvegardes système doit être un nombre entre 1 et 3';
$wb['sysbackup_copies_note_txt'] = '(0 = désactivé)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/hr_server_config.lng b/interface/web/admin/lib/lang/hr_server_config.lng
index 2966dfd01c3a838911e07ab8601788b4c2d9355b..84efd8307c4784859b510fd865077fed8f1a4909 100644
--- a/interface/web/admin/lib/lang/hr_server_config.lng
+++ b/interface/web/admin/lib/lang/hr_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/hu_server_config.lng b/interface/web/admin/lib/lang/hu_server_config.lng
index 722e33d69713a68758173277f78c9b983b739c2d..ef12a7fa723bccae0c8cb6e4bc08c4d674bc64e7 100644
--- a/interface/web/admin/lib/lang/hu_server_config.lng
+++ b/interface/web/admin/lib/lang/hu_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/id_server_config.lng b/interface/web/admin/lib/lang/id_server_config.lng
index ce1d7af21e71d1d4901840402851508986a90b68..42686637b4e03a1f3c8e5d1199a133ff094fdc39 100644
--- a/interface/web/admin/lib/lang/id_server_config.lng
+++ b/interface/web/admin/lib/lang/id_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/it_server_config.lng b/interface/web/admin/lib/lang/it_server_config.lng
index 53b694087cb22c1de140a1bb952047f2b99ca1f4..49d66acf874e86bbfa287a0129a401df77aee287 100644
--- a/interface/web/admin/lib/lang/it_server_config.lng
+++ b/interface/web/admin/lib/lang/it_server_config.lng
@@ -362,3 +362,11 @@ $wb['sysbackup_copies_txt'] = 'Numero di backup di sistema';
$wb['sysbackup_copies_error_empty'] = 'Il numero di backup di sistema non deve essere vuoto';
$wb['sysbackup_copies_error_regex'] = 'Il numero di backup di sistema deve essere un numero compreso tra 1 e 3';
$wb['sysbackup_copies_note_txt'] = '(0 = disattivato)';
+$wb['overquota_db_notify_threshold_txt'] = 'DB quota warning usage level';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/ja_server_config.lng b/interface/web/admin/lib/lang/ja_server_config.lng
index 5cb1e4dd0c2e1c201cde57247ba0a7d01d3b13e5..e2fee94a799b7f2b37e9a9770e0375eef7993c19 100644
--- a/interface/web/admin/lib/lang/ja_server_config.lng
+++ b/interface/web/admin/lib/lang/ja_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/nl_server_config.lng b/interface/web/admin/lib/lang/nl_server_config.lng
index 3378f37bc22c4f5dbb66ebf1f17496f4502ee211..4b9e934f845caf1976ea33dcf94f2c1635a37f95 100644
--- a/interface/web/admin/lib/lang/nl_server_config.lng
+++ b/interface/web/admin/lib/lang/nl_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Aantal systeemback-ups';
$wb['sysbackup_copies_error_empty'] = 'Aantal systeemback-ups mag niet leeg zijn';
$wb['sysbackup_copies_error_regex'] = 'Aantal systeemback-ups moet een getal tussen 1 en 3 zijn';
$wb['sysbackup_copies_note_txt'] = '(0 = uit)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/pl_server_config.lng b/interface/web/admin/lib/lang/pl_server_config.lng
index 2d7b4bf23906dcc7d1971d6208107451d50b6daa..b64350b8a9b10b2a037f1f521642268a3e0434fe 100644
--- a/interface/web/admin/lib/lang/pl_server_config.lng
+++ b/interface/web/admin/lib/lang/pl_server_config.lng
@@ -362,4 +362,11 @@ $wb['soft_delete_keep_365_txt'] = 'Purge after 365 days';
$wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
-$wb['sysbackup_copies_note_txt'] = '(0 = off)';
\ No newline at end of file
+$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/pt_server_config.lng b/interface/web/admin/lib/lang/pt_server_config.lng
index 2ec1c9be08d973a01afdd6a01bd516dfd994c5f5..b09ad7d5dce7559a62af33d795af5207cd8ce261 100644
--- a/interface/web/admin/lib/lang/pt_server_config.lng
+++ b/interface/web/admin/lib/lang/pt_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/ro_server_config.lng b/interface/web/admin/lib/lang/ro_server_config.lng
index b0869c4793124a70c5275d612ef92d5e1833efc3..18d9b7e42a9d2fa4e41950658aa59253ed1fca82 100644
--- a/interface/web/admin/lib/lang/ro_server_config.lng
+++ b/interface/web/admin/lib/lang/ro_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/ru_server_config.lng b/interface/web/admin/lib/lang/ru_server_config.lng
index 1246a93d7fc5ad25732128f5a968812655163a42..2a8b668f0ecb2875a7318b1d2bbfacb94016a394 100644
--- a/interface/web/admin/lib/lang/ru_server_config.lng
+++ b/interface/web/admin/lib/lang/ru_server_config.lng
@@ -363,3 +363,10 @@ $wb['sysbackup_copies_txt'] = 'Number of ISPConfig backups';
$wb['sysbackup_copies_error_empty'] = 'Number of ISPConfig backups must not be empty';
$wb['sysbackup_copies_error_regex'] = 'Number of ISPConfig backups must be a number between 1 and 3';
$wb['sysbackup_copies_note_txt'] = '(0 = off)';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/se_server_config.lng b/interface/web/admin/lib/lang/se_server_config.lng
index 08addb72c7e7d7c7496d0cee56d6d826bbc7ab13..faefe9d3657811daf59b49f1630ad85f656eaa8d 100644
--- a/interface/web/admin/lib/lang/se_server_config.lng
+++ b/interface/web/admin/lib/lang/se_server_config.lng
@@ -359,3 +359,10 @@ $wb['soft_delete_keep_7_txt'] = 'Purge after 7 days';
$wb['soft_delete_keep_30_txt'] = 'Purge after 30 days';
$wb['soft_delete_keep_90_txt'] = 'Purge after 90 days';
$wb['soft_delete_keep_365_txt'] = 'Purge after 365 days';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/sk_server_config.lng b/interface/web/admin/lib/lang/sk_server_config.lng
index ae5c4c7ae57525cdc12ceb298bd8c5c2776f8f0d..6278aa8603191b026aa669127ba4a8f8ed226f03 100644
--- a/interface/web/admin/lib/lang/sk_server_config.lng
+++ b/interface/web/admin/lib/lang/sk_server_config.lng
@@ -359,3 +359,10 @@ $wb['soft_delete_keep_7_txt'] = 'Purge after 7 days';
$wb['soft_delete_keep_30_txt'] = 'Purge after 30 days';
$wb['soft_delete_keep_90_txt'] = 'Purge after 90 days';
$wb['soft_delete_keep_365_txt'] = 'Purge after 365 days';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/lib/lang/tr_server_config.lng b/interface/web/admin/lib/lang/tr_server_config.lng
index 0c5bfdfc7f5a4ce1d0895fdb594995effc7b1dd5..a0f11a4e01f31b4811ecdf0803bf6392c090ed3d 100644
--- a/interface/web/admin/lib/lang/tr_server_config.lng
+++ b/interface/web/admin/lib/lang/tr_server_config.lng
@@ -234,12 +234,11 @@ $wb['overquota_db_notify_threshold_txt'] = 'DB quota warning usage level';
$wb['overquota_db_notify_admin_txt'] = 'Veritabanı Kotası Bildirimleri Yöneticiye Gönderilsin';
$wb['overquota_db_notify_reseller_txt'] = 'Send DB quota warnings to reseller';
$wb['overquota_db_notify_client_txt'] = 'Veritabanı Kotası Bildirimleri Müşteriye Gönderilsin';
-$wb['monitor_system_updates_txt'] = 'Linux Güncellemeleri Denetlensin';
+$wb['monitor_system_updates_txt'] = 'Linux Güncelleme Denetimi';
$wb['php_handler_txt'] = 'Varsayılan PHP İşleyici';
$wb['php_fpm_default_chroot_txt'] = 'Default chrooted PHP-FPM';
$wb['disabled_txt'] = 'Devre Dışı';
$wb['dkim_strength_txt'] = 'DKIM zorluğu';
-$wb['monitor_system_updates_txt'] = 'Linux Güncelleme Denetimi';
$wb['invalid_apache_user_txt'] = 'Apache kullanıcısı geçersiz.';
$wb['invalid_apache_group_txt'] = 'Apache grubu geçersiz.';
$wb['backup_dir_error_regex'] = 'Yedek klasörü geçersiz.';
@@ -356,3 +355,14 @@ $wb['soft_delete_keep_7_txt'] = 'Purge after 7 days';
$wb['soft_delete_keep_30_txt'] = 'Purge after 30 days';
$wb['soft_delete_keep_90_txt'] = 'Purge after 90 days';
$wb['soft_delete_keep_365_txt'] = 'Purge after 365 days';
+$wb['php_fpm_incron_reload_txt'] = 'Install incron trigger file to reload PHP-FPM';
+$wb['error_mailbox_message_size_txt'] = 'Mailbox size must be larger or equal to message size';
+$wb['php_fpm_reload_mode_txt'] = 'PHP-FPM reload mode';
+$wb['content_filter_txt'] = 'Content Filter';
+$wb['le_signature_type_txt'] = 'Certificate signature type';
+$wb['le_auto_cleanup_txt'] = 'Automatically purge unused Let\'s Encrypt certificates';
+$wb['le_auto_cleanup_denylist_txt'] = 'Domains that should never be purged';
+$wb['le_auto_cleanup_denylist_note_txt'] = 'Comma separated list of domain globs that should not be purged.
E.g. mail.*, externally-managed.example.com
Placeholders:';
+$wb['le_auto_cleanup_denylist_error_custom'] = 'Invalid list of domain globs';
+$wb['le_delete_on_site_remove_txt'] = 'Delete Let\'s Encrypt certificate on website removal';
+$wb['le_revoke_before_delete_txt'] = 'Revoke a certificate before deleting it (prevents Let\'s Encrypt renewal warnings)';
diff --git a/interface/web/admin/templates/server_config_web_edit.htm b/interface/web/admin/templates/server_config_web_edit.htm
index 54124bd9ac7486df5d368d401158c7138ba9c7e3..956fd3039a9f9fee9c791afe0597c9e8389135fd 100644
--- a/interface/web/admin/templates/server_config_web_edit.htm
+++ b/interface/web/admin/templates/server_config_web_edit.htm
@@ -248,6 +248,28 @@
+ * $this->outputTable([['one', 'two'], ['1', '2']]);
+ *
+ *
+ * You can define minimum column lengths:
+ *
+ * $this->outputTable([['one', 'two'], ['1', '2']], ['min_lengths' => [10, 10]]);
+ *
+ *
+ * The table tries to size itself to fit in the terminal window width. As default all column width 5 or wider will shrink,
+ * but you can also specify which column(s) should shrink:
+ *
+ * $this->outputTable([
+ * ['variable', 'fixed', 'variable'],
+ * [str_repeat('this is a long column ', 40), '2', str_repeat('another long column ', 60)]
+ * ], ['variable_columns' => [0,2]]);
+ *
+ *
+ * Small tables do not fill the available terminal window width by default.
+ * You can change this with the `expand` $option:
+ *
+ * $this->outputTable([['one', 'two'], ['1', '2']], ['expand' => true]);
+ *
+ *
+ * @param array $table array of arrays (rows -> columns) to display as table.
+ * @param array{min_lengths: array|null, variable_columns: array|string|int|null, expand: bool}|null $options optional formatting options
+ * @return void
+ */
+ public function outputTable($table, $options = null) {
+ if(empty($table)) {
+ $this->swriteln('No data to display');
+ return;
+ }
+ if(!is_array($options)) {
+ $options = [];
+ }
+ if(!is_array($options['min_lengths'])) {
+ $options['min_lengths'] = [];
+ }
+
+ // process input $table
+ $columns = [];
+ $rows = [];
+ $num_columns = false;
+ foreach($table as $row) {
+ $c = count($row);
+ if(!$c || $num_columns !== false && $c != $num_columns) {
+ $this->error("every input row of outputTable input needs to the same size (not null)");
+ }
+ $num_columns = $c;
+ $r = [];
+ $index = 0;
+ foreach($row as $value) {
+ $value_as_string = (string)$value;
+ $max = 0;
+ foreach(explode("\n", $value_as_string) as $line) {
+ $len = $this->stringWidth($line);
+ $max = max($max, max($options['min_lengths'][$index] ?: 0, $len));
+ }
+ if(!isset($columns[$index])) {
+ $columns[$index] = ['length' => 0, 'first' => $index == 0, 'last' => $index == $num_columns - 1];
+ }
+ if($columns[$index]['length'] < $max) {
+ $columns[$index]['length'] = $max;
+ }
+ $r[$index] = $value_as_string;
+ $index += 1;
+ }
+ $rows[] = $r;
+ }
+
+ // fit table to terminal
+ if($this->isStdOutATty()) {
+ if(!isset($options['variable_columns']) || $options['variable_columns'] == 'all') {
+ $options['variable_columns'] = range(0, $num_columns - 1);
+ } elseif(!is_array($options['variable_columns'])) {
+ $options['variable_columns'] = explode(',', (string)$options['variable_columns']);
+ }
+ $minimum_variable_column_width = 5;
+ $shrink_variable_columns = array_values(array_filter($options['variable_columns'], function($index) use ($minimum_variable_column_width, $columns, $num_columns) {
+ return $index < $num_columns && $columns[$index]['length'] >= $minimum_variable_column_width;
+ }));
+ $expand_variable_columns = array_values(array_filter($options['variable_columns'], function($index) use ($num_columns) {
+ return $index < $num_columns;
+ }));
+ $terminal_width = intval(trim(exec("tput cols") ?: '')) ?: 80;
+ $table_width = array_reduce($columns, function($sum, $column) {
+ return $sum + $column['length'] + 3;
+ }, 1);
+ if(count($shrink_variable_columns) > 0 && $table_width > $terminal_width) {
+ $diff = $table_width - $terminal_width;
+ $diff_per_column = $this->getDiscreetDistribution($diff, count($shrink_variable_columns));
+ foreach($shrink_variable_columns as $i => $index) {
+ $new_length = $columns[$index]['length'] - $diff_per_column[$i];
+ $columns[$index]['length'] = max($minimum_variable_column_width, $new_length);
+ }
+ } elseif(count($expand_variable_columns) > 0 && $options['expand'] && $table_width < $terminal_width) {
+ $diff = $terminal_width - $table_width;
+ $diff_per_column = $this->getDiscreetDistribution($diff, count($expand_variable_columns));
+ foreach($expand_variable_columns as $i => $index) {
+ $new_length = $columns[$index]['length'] + $diff_per_column[$i];
+ $columns[$index]['length'] = $new_length;
+ }
+ }
+ }
+
+ // output table
+ $separator = function($first_start, $mid_start, $mid, $mid_end, $last_end) use ($columns) {
+ $this->swriteln(array_reduce($columns, function($line, $column) use ($first_start, $mid_start, $mid, $mid_end, $last_end) {
+ return $line . ($column['first'] ? $first_start : $mid_start) . str_repeat($mid, $column['length']) . ($column['last'] ? $last_end : $mid_end);
+ }, ''));
+ };
+ foreach($rows as $row_index => $row) {
+ if($row_index == 0) {
+ $separator('╔═', '╤═', '═', '═', '═╗');
+ }
+ // first pass -> re-wrap lines and get max number of lines of this row
+ $height = 1;
+ $lines = [];
+ foreach($columns as $index => $column) {
+ $lines[$index] = explode("\n", $this->wrapText($row[$index], $column['length']));
+ $height = max($height, count($lines[$index]));
+ }
+ // second pass -> output row, line by line
+ for($inner = 0; $inner < $height; $inner++) {
+ $line = "";
+ foreach($columns as $index => $column) {
+ $value = $lines[$index][$inner] ?: '';
+ $value .= str_repeat(' ', $column['length'] - $this->stringWidth($value));
+ $line .= ($column['first'] ? '║ ' : '│ ') . $value . ($column['last'] ? ' ║' : ' ');
+ }
+ $this->swriteln($line);
+ }
+ if($row_index < count($rows) - 1) {
+ $separator('╟─', '┼─', '─', '─', '─╢');
+ } else {
+ $separator('╚═', '╧═', '═', '═', '═╝');
+ }
+ }
+ }
}
diff --git a/server/lib/classes/cron.d/100-mailbox_stats_hourly.inc.php b/server/lib/classes/cron.d/100-mailbox_stats_hourly.inc.php
index 33acf5fa5cef20603f6acbe879cb57b4b1223250..690c627216cbad08ea3b865a5c8308526a0d595b 100644
--- a/server/lib/classes/cron.d/100-mailbox_stats_hourly.inc.php
+++ b/server/lib/classes/cron.d/100-mailbox_stats_hourly.inc.php
@@ -94,7 +94,7 @@ class cronjob_mailbox_stats_hourly extends cronjob {
$line = strtok($log_lines, PHP_EOL);
while ($line !== FALSE) {
$matches = [];
- // Match pop3/imap logings, or alternately smtp logins.
+ // Match pop3/imap logins, or alternately smtp logins.
if (preg_match('/(.*) (imap|pop3)-login: Login: user=\<([\w\.@-]+)\>/', $line, $matches) || preg_match('/(.*) sasl_method=PLAIN, sasl_username=([\w\.@-]+)/', $line, $matches)) {
$user = isset($matches[3]) ? $matches[3] : $matches[2];
$updatedUsers[] = $user;
diff --git a/server/lib/classes/cron.d/500-backup_mail.inc.php b/server/lib/classes/cron.d/500-backup_mail.inc.php
index 92051fa4f228c511ccc9eb8e1069a6831a6ae19a..77c47e7e84f9c78507e2fa622c7dd9618ae8a244 100644
--- a/server/lib/classes/cron.d/500-backup_mail.inc.php
+++ b/server/lib/classes/cron.d/500-backup_mail.inc.php
@@ -58,7 +58,7 @@ class cronjob_backup_mail extends cronjob {
$server_config = $app->getconf->get_server_config($conf['server_id'], 'server');
$mail_config = $app->getconf->get_server_config($conf['server_id'], 'mail');
$global_config = $app->getconf->get_global_config('sites');
-
+
$backup_dir = trim($server_config['backup_dir']);
$backup_dir_permissions =0750;
@@ -92,7 +92,7 @@ class cronjob_backup_mail extends cronjob {
$domain_rec=$app->db->queryOneRecord("SELECT * FROM mail_domain WHERE domain = ?", $domain);
if($rec['backup_interval'] == 'daily' or ($rec['backup_interval'] == 'weekly' && date('w') == 0) or ($rec['backup_interval'] == 'monthly' && date('d') == '01')) {
-
+
$backupusername = 'root';
$backupgroup = 'root';
if ($global_config['backups_include_into_web_quota'] == 'y') {
@@ -112,7 +112,7 @@ class cronjob_backup_mail extends cronjob {
$backupusername = $webdomain['system_user'];
$backupgroup = $webdomain['system_group'];
}
- }
+ }
$mail_backup_dir = $backup_dir.'/mail'.$domain_rec['domain_id'];
if(!is_dir($mail_backup_dir)) mkdir($mail_backup_dir, 0750);
@@ -127,7 +127,7 @@ class cronjob_backup_mail extends cronjob {
if (empty($this->tmp_backup_dir)) $this->tmp_backup_dir = $rec['maildir'];
// Create temporary backup-mailbox
$app->system->exec_safe("su -c ?", 'dsync -o plugin/acl= -o plugin/quota= backup -u "'.$rec["email"].'" mdbox:' . $this->tmp_backup_dir . '/backup');
-
+
if($backup_mode == 'userzip') {
$mail_backup_file.='.zip';
$app->system->exec_safe('cd ? && zip ? -b ? -r backup > /dev/null && rm -rf backup', $this->tmp_backup_dir, $mail_backup_dir.'/'.$mail_backup_file, $backup_tmp);
@@ -142,7 +142,7 @@ class cronjob_backup_mail extends cronjob {
$retval = $app->system->last_exec_retcode();
}
}
-
+
if ($retval != 0) {
// Cleanup
if(file_exists($this->tmp_backup_dir . '/backup')) {
@@ -154,11 +154,11 @@ class cronjob_backup_mail extends cronjob {
$domain_dir=explode('/',$rec['maildir']);
$_temp=array_pop($domain_dir);unset($_temp);
$domain_dir=implode('/',$domain_dir);
-
+
$parts=explode('/',$rec['maildir']);
$source_dir=array_pop($parts);
unset($parts);
-
+
//* create archives
if($backup_mode == 'userzip') {
$mail_backup_file.='.zip';
@@ -175,15 +175,15 @@ class cronjob_backup_mail extends cronjob {
$retval = $app->system->last_exec_retcode();
}
}
-
- if($retval == 0 || ($backup_mode != 'userzip' && $retval == 1) || ($backup_mode == 'userzip' && $retval == 12)){// tar can return 1, zip can return 12(due to harmless warings) and still create valid backups
+
+ if($retval == 0 || ($backup_mode != 'userzip' && $retval == 1) || ($backup_mode == 'userzip' && $retval == 12)){// tar can return 1, zip can return 12(due to harmless warnings) and still create valid backups
chown($mail_backup_dir.'/'.$mail_backup_file, $backupusername);
chgrp($mail_backup_dir.'/'.$mail_backup_file, $backupgroup);
chmod($mail_backup_dir.'/'.$mail_backup_file, 0640);
/* Insert mail backup record in database */
$filesize = filesize($mail_backup_dir.'/'.$mail_backup_file);
$sql = "INSERT INTO mail_backup (server_id, parent_domain_id, mailuser_id, backup_mode, tstamp, filename, filesize) VALUES (?, ?, ?, ?, ?, ?, ?)";
- $app->db->query($sql, $conf['server_id'], $domain_rec['domain_id'], $rec['mailuser_id'], $backup_mode, time(), $mail_backup_file, $filesize);
+ $app->db->query($sql, $conf['server_id'], $domain_rec['domain_id'], $rec['mailuser_id'], $backup_mode, time(), $mail_backup_file, $filesize);
if($app->running_on_slaveserver()) $app->dbmaster->query($sql, $conf['server_id'], $domain_rec['domain_id'], $rec['mailuser_id'], $backup_mode, time(), $mail_backup_file, $filesize);
unset($filesize);
} else {
@@ -195,7 +195,7 @@ class cronjob_backup_mail extends cronjob {
$app->system->exec_safe('rm -rf ?', $rec['maildir'] . '/backup');
}
}
- $app->log($mail_backup_file.' NOK:'.implode('',$tmp_output), LOGLEVEL_WARN);
+ $app->log($mail_backup_file.' NOK:'.implode('',$app->system->last_exec_out()), LOGLEVEL_WARN);
}
/* Remove old backups */
$backup_copies = intval($rec['backup_copies']);
diff --git a/server/lib/classes/cron.d/500-clean_mailboxes.inc.php b/server/lib/classes/cron.d/500-clean_mailboxes.inc.php
index a1a053fcca482405372d8dcc646a50229f854feb..93a47246facb34917418100b26fbb26f26d04289 100755
--- a/server/lib/classes/cron.d/500-clean_mailboxes.inc.php
+++ b/server/lib/classes/cron.d/500-clean_mailboxes.inc.php
@@ -31,8 +31,8 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
class cronjob_clean_mailboxes extends cronjob {
// should run before quota notify and backup
- // quota notify and backup is both '0 0 * * *'
-
+ // quota notify and backup is both '0 0 * * *'
+
// job schedule
protected $_schedule = '00 22 * * *';
@@ -77,7 +77,7 @@ class cronjob_clean_mailboxes extends cronjob {
WHERE maildir_format = 'maildir' AND disableimap = 'n' AND server_id = ?
AND (purge_trash_days > 0 OR purge_junk_days > 0)",
$server_id);
-
+
if(is_array($records) && !empty($records)) {
foreach($records as $email) {
@@ -147,7 +147,7 @@ class cronjob_clean_mailboxes extends cronjob {
global $app, $conf;
$sql = "SELECT email FROM mail_user WHERE maildir_format = 'mdbox' AND server_id = ?";
- $records = $app->db->queryAllRecords($sql, $server_id);
+ $records = $app->db->queryAllRecords($sql, $conf['server_id']);
if(is_array($records)) {
foreach($records as $rec) {
diff --git a/server/lib/classes/cron.d/800-letsencrypt_cleanup.inc.php b/server/lib/classes/cron.d/800-letsencrypt_cleanup.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ed1f702be643732be673c6bda6b3678fde89b20
--- /dev/null
+++ b/server/lib/classes/cron.d/800-letsencrypt_cleanup.inc.php
@@ -0,0 +1,143 @@
+uses('letsencrypt,ini_parser,getconf');
+
+ $server_db_record = $app->db->queryOneRecord("SELECT * FROM server WHERE server_id = ?", $conf['server_id']);
+ if(!$server_db_record || !$server_db_record['web_server']) {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'Webserver not active, not running Let\'s Encrypt cleanup.' . "\n";
+ }
+ parent::onRunJob();
+ return;
+ }
+
+ $server_config = $app->getconf->get_server_config($conf['server_id'], 'server');
+ if((isset($server_config['migration_mode']) ? $server_config['migration_mode'] : 'n') == 'y') {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'Migration mode active, not running Let\'s Encrypt cleanup.' . "\n";
+ }
+ parent::onRunJob();
+ return;
+ }
+
+ if(!$app->letsencrypt->get_acme_script() && !$app->letsencrypt->get_certbot_script()) {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'No Let\'s Encrypt client found, not running Let\'s Encrypt cleanup.' . "\n";
+ }
+ parent::onRunJob();
+ return;
+ }
+
+ $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
+ $le_auto_cleanup = empty($web_config['le_auto_cleanup']) ? 'n' : $web_config['le_auto_cleanup'];
+ if($le_auto_cleanup == 'n') {
+ parent::onRunJob();
+ return;
+ }
+
+ $used_serials = [];
+ $all_letsencrypt_websites = $app->db->queryAllRecords(
+ "SELECT * FROM web_domain WHERE ssl_letsencrypt = 'y' AND `ssl` = 'y' AND document_root IS NOT NULL AND server_id = ?",
+ $conf['server_id']
+ );
+ foreach($all_letsencrypt_websites as $record) {
+ $cert_paths = $app->letsencrypt->get_website_certificate_paths(['new' => $record]);
+ if(is_readable($cert_paths['crt'])) {
+ $info = $app->letsencrypt->extract_x509($cert_paths['crt'], $cert_paths['bundle']);
+ if($info) {
+ if($record['active'] == 'y') {
+ // when website is active we unconditionally deem the certificate as used (even when it is not valid anymore)
+ $used = true;
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'active website ' . $record['domain_id'] . '/' . $record['domain'] . ' is using ' . ($info['is_valid'] ? 'valid' : 'invalid') . ' certificate ' . $cert_paths['crt'] . "\n";
+ }
+ } else {
+ // when website is inactive, we only consider its certificates used when the certificate is still valid
+ $used = $info['is_valid'];
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'inactive website ' . $record['domain_id'] . '/' . $record['domain'] . ' is using ' . ($info['is_valid'] ? 'valid' : 'invalid') . ' certificate ' . $cert_paths['crt'] . ($used ? '' : ' but we consider it as unused') . "\n";
+ }
+ }
+ if($used) {
+ $used_serials[] = $info['serial_number'];
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'mark serial number ' . $info['serial_number'] . ' as used from ' . $cert_paths['crt'] . "\n";
+ }
+ } else {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'serial number ' . $info['serial_number'] . ' is referenced but we deem it as unused ' . $cert_paths['crt'] . "\n";
+ }
+ }
+ } else {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'cannot extract X509 information from ' . $cert_paths['crt'] . "\n";
+ }
+ }
+ } else {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print $cert_paths['crt'] . ' is not readable' . "\n";
+ }
+ }
+ }
+
+ $certificates = $app->letsencrypt->get_certificate_list();
+ foreach($certificates as $certificate) {
+ if(in_array($certificate['serial_number'], $used_serials)) {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'Skip ' . $certificate['id'] . ' because it still gets used by ISPConfig' . "\n";
+ }
+ continue;
+ }
+ $on_deny_list = $app->letsencrypt->check_deny_list($certificate);
+ if(!empty($on_deny_list)) {
+ if($conf['log_priority'] <= LOGLEVEL_DEBUG) {
+ print 'Skip ' . $certificate['id'] . ' because one of its domains is on deny list or a wildcard domain (' . join(', ', $on_deny_list) . ')' . "\n";
+ }
+ continue;
+ }
+ if($app->letsencrypt->remove_certificate($certificate, null, false)) {
+ print 'Removed unused certificate ' . $certificate['id'] . "\n";
+ } else {
+ $app->log('Error removing certificate ' . $certificate['id'], LOGLEVEL_WARN);
+ print 'Error removing certificate ' . $certificate['id'] . "\n";
+ }
+ }
+
+ parent::onRunJob();
+ }
+}
diff --git a/server/lib/classes/letsencrypt.inc.php b/server/lib/classes/letsencrypt.inc.php
index fe6766e600d77df99fc48aa821c14f230b853895..0fff2c10b1c21d5a1c114220822f4b91908295a5 100644
--- a/server/lib/classes/letsencrypt.inc.php
+++ b/server/lib/classes/letsencrypt.inc.php
@@ -30,21 +30,10 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
class letsencrypt {
- /**
- * Construct for this class
- *
- * @return system
- */
- private $base_path = '/etc/letsencrypt';
private $renew_config_path = '/etc/letsencrypt/renewal';
- private $certbot_use_certcommand = false;
-
- public function __construct(){
-
- }
public function get_acme_script() {
- $acme = explode("\n", shell_exec('which acme.sh /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh 2> /dev/null') ?? '');
+ $acme = explode("\n", shell_exec('which acme.sh /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh 2> /dev/null') ?: '');
$acme = reset($acme);
if(is_executable($acme)) {
return $acme;
@@ -53,30 +42,80 @@ class letsencrypt {
}
}
- public function get_acme_command($domains, $key_file, $bundle_file, $cert_file, $server_type = 'apache') {
+ public function get_acme_command($domains, $key_file, $bundle_file, $cert_file, $server_type = 'apache', &$cert_type = 'RSA') {
global $app, $conf;
- $letsencrypt = $this->get_acme_script();
-
- $cmd = '';
- // generate cli format
- foreach($domains as $domain) {
- $cmd .= (string) " -d " . $domain;
+ if(empty($domains)) {
+ return false;
}
- if($cmd == '') {
+ $acme_sh = '';
+ $use_acme = $this->use_acme($acme_sh);
+ if(!$use_acme || !$acme_sh) {
+ return false;
+ }
+ $version = $this->get_acme_version($acme_sh);
+ if(empty($version)) {
return false;
}
+ $acme_sh .= ' --log ' . escapeshellarg($conf['ispconfig_log_dir'] . '/acme.log');
+
+ $domain_args = ' -d ' . join(' -d ', array_map('escapeshellarg', $domains));
+ $files_to_install = ' --key-file ' . escapeshellarg($key_file);
if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) {
- $cert_arg = '--fullchain-file ' . escapeshellarg($cert_file);
+ $files_to_install .= ' --fullchain-file ' . escapeshellarg($cert_file);
} else {
- $cert_arg = '--fullchain-file ' . escapeshellarg($bundle_file) . ' --cert-file ' . escapeshellarg($cert_file);
+ $files_to_install .= ' --fullchain-file ' . escapeshellarg($bundle_file) . ' --cert-file ' . escapeshellarg($cert_file);
}
- $cmd = 'R=0 ; C=0 ; ' . $letsencrypt . ' --issue ' . $cmd . ' -w /usr/local/ispconfig/interface/acme --always-force-new-domain-key --keylength 4096; R=$? ; if [ $R -eq 0 -o $R -eq 2 ] ; then ' . $letsencrypt . ' --install-cert ' . $cmd . ' --key-file ' . escapeshellarg($key_file) . ' ' . $cert_arg . ' --reloadcmd ' . escapeshellarg($this->get_reload_command()) . ' --log ' . escapeshellarg($conf['ispconfig_log_dir'].'/acme.log') . '; C=$? ; fi ; if [ $C -eq 0 ] ; then exit $R ; else exit $C ; fi';
+ // the minimum acme.sh version for ECDSA might be lower, but this version should work OK
+ if($cert_type == 'ECDSA' && version_compare($version, '2.6.4', '>=')) {
+ $app->log('acme.sh version is ' . $version . ', so using --keylength ec-256 instead of --keylength 4096', LOGLEVEL_DEBUG);
+ $certificate_type_arg = ' --keylength ec-256';
+ $conf_selection_arg = ' --ecc';
+ } else {
+ $certificate_type_arg = ' --keylength 4096';
+ $conf_selection_arg = '';
+ if($cert_type != 'RSA') {
+ $cert_type = 'RSA';
+ $app->log($cert_type . ' was requested by we use RSA because acme.sh version is ' . $version, LOGLEVEL_DEBUG);
+ }
+ }
- return $cmd;
+ $commands = [
+ 'R=0 ; C=0',
+ $acme_sh . ' --issue ' . $domain_args . ' -w /usr/local/ispconfig/interface/acme --always-force-new-domain-key ' . $conf_selection_arg . $certificate_type_arg,
+ 'R=$?',
+ 'if [ $R -eq 0 ] || [ $R -eq 2 ]; then :',
+ ' ' . $acme_sh . ' --install-cert ' . $domain_args . $conf_selection_arg . $files_to_install . ' --reloadcmd ' . escapeshellarg($this->get_reload_command($server_type)),
+ ' C=$?',
+ 'fi',
+ 'if [ $C -eq 0 ]',
+ ' then exit $R',
+ ' else exit $C',
+ 'fi'
+ ];
+
+ return join(' ; ', $commands);
+ }
+
+ private function install_acme() {
+ $install_cmd = 'wget -O - https://get.acme.sh | sh';
+ $ret = null;
+ $val = 0;
+ exec($install_cmd . ' 2>&1', $ret, $val);
+
+ return $val == 0;
+ }
+
+ private function get_acme_version($acme_script) {
+ $matches = array();
+ $output = shell_exec($acme_script . ' --version 2>&1') ?: '';
+ if(preg_match('/^v(\d+(\.\d+)+)$/m', $output, $matches)) {
+ return $matches[1];
+ }
+ return false;
}
public function get_certbot_script() {
@@ -90,178 +129,163 @@ class letsencrypt {
}
}
- private function install_acme() {
- $install_cmd = 'wget -O - https://get.acme.sh | sh';
+ private function get_certbot_version($certbot_script) {
+ $matches = array();
$ret = null;
$val = 0;
- exec($install_cmd . ' 2>&1', $ret, $val);
-
- return ($val == 0 ? true : false);
- }
-
- private function get_reload_command() {
- global $app, $conf;
-
- $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
-
- $daemon = '';
- switch ($web_config['server_type']) {
- case 'nginx':
- $daemon = $web_config['server_type'];
- break;
- default:
- if(is_file($conf['init_scripts'] . '/' . 'httpd24-httpd') || is_dir('/opt/rh/httpd24/root/etc/httpd')) {
- $daemon = 'httpd24-httpd';
- } elseif(is_file($conf['init_scripts'] . '/' . 'httpd') || is_dir('/etc/httpd')) {
- $daemon = 'httpd';
- } else {
- $daemon = 'apache2';
- }
+ $letsencrypt_version = exec($certbot_script . ' --version 2>&1', $ret, $val);
+ if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $letsencrypt_version, $matches)) {
+ $letsencrypt_version = $matches[2];
}
-
- $cmd = $app->system->getinitcommand($daemon, 'force-reload');
- return $cmd;
+ return $letsencrypt_version;
}
- public function get_certbot_command($domains) {
+ public function get_certbot_command($domains, &$cert_type = 'RSA') {
global $app;
- $letsencrypt = $this->get_certbot_script();
-
- $cmd = '';
- // generate cli format
- foreach($domains as $domain) {
- $cmd .= (string) " --domains " . $domain;
- }
-
- if($cmd == '') {
+ if(empty($domains)) {
return false;
}
+ $letsencrypt = $this->get_certbot_script();
+
$primary_domain = $domains[0];
- $matches = array();
- $ret = null;
- $val = 0;
- $letsencrypt_version = exec($letsencrypt . ' --version 2>&1', $ret, $val);
- if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $letsencrypt_version, $matches)) {
- $letsencrypt_version = $matches[2];
- }
- if (version_compare($letsencrypt_version, '0.22', '>=')) {
+ $letsencrypt_version = $this->get_certbot_version($letsencrypt);
+
+ if(version_compare($letsencrypt_version, '0.22', '>=')) {
$acme_version = 'https://acme-v02.api.letsencrypt.org/directory';
} else {
$acme_version = 'https://acme-v01.api.letsencrypt.org/directory';
}
- if (version_compare($letsencrypt_version, '0.30', '>=')) {
- $app->log("LE version is " . $letsencrypt_version . ", so using certificates command and --cert-name instead of --expand", LOGLEVEL_DEBUG);
- $this->certbot_use_certcommand = true;
- $webroot_map = array();
- for($i = 0; $i < count($domains); $i++) {
- $webroot_map[$domains[$i]] = '/usr/local/ispconfig/interface/acme';
+
+ if($cert_type == 'ECDSA' && version_compare($letsencrypt_version, '2.0', '>=')) {
+ $app->log('LE version is ' . $letsencrypt_version . ', so using --elliptic-curve secp256r1 instead of --rsa-key-size 4096', LOGLEVEL_DEBUG);
+ $certificate_type_arg = "--elliptic-curve secp256r1";
+ $name_suffix = '_ecc';
+ } else {
+ $certificate_type_arg = "--rsa-key-size 4096";
+ $name_suffix = '';
+ if($cert_type != 'RSA') {
+ $cert_type = 'RSA';
+ $app->log($cert_type . ' was requested by we use RSA because certbot version is ' . $letsencrypt_version, LOGLEVEL_DEBUG);
+ }
+ }
+
+ if(version_compare($letsencrypt_version, '0.30', '>=')) {
+ $app->log('LE version is ' . $letsencrypt_version . ', so using --cert-name instead of --expand', LOGLEVEL_DEBUG);
+ $webroot_map = [];
+ foreach($domains as $domain) {
+ $webroot_map[$domain] = '/usr/local/ispconfig/interface/acme';
}
$webroot_args = "--webroot-map " . escapeshellarg(str_replace(array("\r", "\n"), '', json_encode($webroot_map)));
// --cert-name might be working with earlier versions of certbot, but there is no exact version documented
// So for safety reasons we add it to the 0.30 version check as it is documented to work as expected in this version
- $cert_selection_command = "--cert-name $primary_domain";
+ $cert_selection_command = "--cert-name $primary_domain$name_suffix";
} else {
+ $cmd = ' --domains ' . join(' --domains ', array_map('escapeshellarg', $domains));
$webroot_args = "$cmd --webroot-path /usr/local/ispconfig/interface/acme";
$cert_selection_command = "--expand";
}
- $cmd = $letsencrypt . " certonly -n --text --agree-tos $cert_selection_command --authenticator webroot --server $acme_version --rsa-key-size 4096 --email webmaster@$primary_domain $webroot_args";
+ return $letsencrypt . " certonly -n --text --agree-tos $cert_selection_command --authenticator webroot --server $acme_version $certificate_type_arg --email webmaster@$primary_domain $webroot_args";
+ }
+
+ private function get_reload_command($server_type) {
+ global $app, $conf;
+
+ $daemon = '';
+ switch($server_type) {
+ case 'nginx':
+ $daemon = 'nginx';
+ break;
+ default:
+ if(is_file($conf['init_scripts'] . '/' . 'httpd24-httpd') || is_dir('/opt/rh/httpd24/root/etc/httpd')) {
+ $daemon = 'httpd24-httpd';
+ } elseif(is_file($conf['init_scripts'] . '/' . 'httpd') || is_dir('/etc/httpd')) {
+ $daemon = 'httpd';
+ } else {
+ $daemon = 'apache2';
+ }
+ }
+ $cmd = $app->system->getinitcommand($daemon, 'force-reload');
return $cmd;
}
- public function get_letsencrypt_certificate_paths($domains = array()) {
+ private function use_acme(&$script = null) {
global $app;
- if($this->get_acme_script()) {
- return false;
+ $script = $this->get_acme_script();
+ if($script) {
+ return true;
}
+ $script = $this->get_certbot_script();
+ if(!$script) {
+ $app->log("Unable to find Let's Encrypt client, installing acme.sh.", LOGLEVEL_DEBUG);
+ // acme and le missing
+ $this->install_acme();
+ $script = $this->get_acme_script();
+ if($script) {
+ return true;
+ } else {
+ $app->log("Unable to install acme.sh. Cannot proceed, no Let's Encrypt client found.", LOGLEVEL_WARN);
+ return null;
+ }
+ }
+ return false;
+ }
- if(empty($domains)) return false;
- if(!is_dir($this->renew_config_path)) return false;
+ public function get_letsencrypt_certificate_paths($domains = [], $cert_type = 'RSA') {
+ global $app;
- $dir = opendir($this->renew_config_path);
- if(!$dir) return false;
+ if(empty($domains)) return false;
- $path_scores = array();
+ $all_certificates = $this->get_certificate_list();
+ if(empty($all_certificates)) {
+ return false;
+ }
- $main_domain = reset($domains);
- sort($domains);
+ $primary_domain = reset($domains);
+ $sorted_domains = $domains;
+ sort($sorted_domains);
$min_diff = false;
-
- while($file = readdir($dir)) {
- if($file === '.' || $file === '..' || substr($file, -5) !== '.conf') continue;
- $file_path = $this->renew_config_path . '/' . $file;
- if(!is_file($file_path) || !is_readable($file_path)) continue;
-
- $fp = fopen($file_path, 'r');
- if(!$fp) continue;
-
- $path_scores[$file_path] = array(
- 'domains' => array(),
- 'diff' => 0,
- 'has_main_domain' => false,
- 'cert_paths' => array(
- 'cert' => '',
- 'privkey' => '',
- 'chain' => '',
- 'fullchain' => ''
- )
- );
- $in_list = false;
- while(!feof($fp) && $line = fgets($fp)) {
- $line = trim($line);
- if($line === '') continue;
- elseif(!$in_list) {
- if($line == '[[webroot_map]]') $in_list = true;
-
- $tmp = explode('=', $line, 2);
- if(count($tmp) != 2) continue;
- $key = trim($tmp[0]);
- if($key == 'cert' || $key == 'privkey' || $key == 'chain' || $key == 'fullchain') {
- $path_scores[$file_path]['cert_paths'][$key] = trim($tmp[1]);
- }
-
- continue;
- }
-
- $tmp = explode('=', $line, 2);
- if(count($tmp) != 2) continue;
-
- $domain = trim($tmp[0]);
- if($domain == $main_domain) $path_scores[$file_path]['has_main_domain'] = true;
- $path_scores[$file_path]['domains'][] = $domain;
+ $possible_certificates = [];
+ foreach($all_certificates as $certificate) {
+ if($certificate['signature_type'] != $cert_type) {
+ continue;
}
- fclose($fp);
-
- sort($path_scores[$file_path]['domains']);
- if(count(array_intersect($domains, $path_scores[$file_path]['domains'])) < 1) {
- $path_scores[$file_path]['diff'] = false;
+ $sorted_cert_domains = $certificate['domains'];
+ sort($sorted_cert_domains);
+ if(count(array_intersect($sorted_domains, $sorted_cert_domains)) < 1) {
+ continue;
} else {
- // give higher diff value to missing domains than to those that are too much in there
- $path_scores[$file_path]['diff'] = (count(array_diff($domains, $path_scores[$file_path]['domains'])) * 1.5) + count(array_diff($path_scores[$file_path]['domains'], $domains));
+ // if the domains are exactly the same (including order) consider this better than a certificate that has all domains but in a different order
+ if($domains === $certificate['domains']) {
+ $certificate['diff'] = -1;
+ } else {
+ // give higher diff value to missing domains than to those that are too much in there
+ $certificate['diff'] = (count(array_diff($sorted_domains, $sorted_cert_domains)) * 1.5) + count(array_diff($sorted_cert_domains, $sorted_domains));
+ }
+ $certificate['has_main_domain'] = in_array($primary_domain, $certificate['domains']);
}
-
- if($min_diff === false || $path_scores[$file_path]['diff'] < $min_diff) $min_diff = $path_scores[$file_path]['diff'];
+ if($min_diff === false || ($certificate['diff'] < $min_diff)) $min_diff = $certificate['diff'];
+ $possible_certificates[] = $certificate;
}
- closedir($dir);
if($min_diff === false) return false;
$cert_paths = false;
- $used_path = false;
- foreach($path_scores as $path => $data) {
- if($data['diff'] === $min_diff) {
- $used_path = $path;
- $cert_paths = $data['cert_paths'];
- if($data['has_main_domain'] == true) break;
+ $used_id = false;
+ foreach($possible_certificates as $certificate) {
+ if($certificate['diff'] === $min_diff) {
+ $used_id = $certificate['id'];
+ $cert_paths = $certificate['cert_paths'];
+ if($certificate['has_main_domain']) break;
}
}
- $app->log("Let's Encrypt Cert config path is: " . ($used_path ? $used_path : "not found") . ".", LOGLEVEL_DEBUG);
+ $app->log("Let's Encrypt Cert config path is: " . ($used_id ?: "not found") . ".", LOGLEVEL_DEBUG);
return $cert_paths;
}
@@ -287,284 +311,637 @@ class letsencrypt {
}
public function get_website_certificate_paths($data) {
- $ssl_dir = $data['new']['document_root'].'/ssl';
+ $ssl_dir = $data['new']['document_root'] . '/ssl';
$domain = $this->get_ssl_domain($data);
$cert_paths = array(
'domain' => $domain,
- 'key' => $ssl_dir.'/'.$domain.'.key',
- 'key2' => $ssl_dir.'/'.$domain.'.key.org',
- 'csr' => $ssl_dir.'/'.$domain.'.csr',
- 'crt' => $ssl_dir.'/'.$domain.'.crt',
- 'bundle' => $ssl_dir.'/'.$domain.'.bundle'
+ 'key' => $ssl_dir . '/' . $domain . '.key',
+ 'key2' => $ssl_dir . '/' . $domain . '.key.org',
+ 'csr' => $ssl_dir . '/' . $domain . '.csr',
+ 'crt' => $ssl_dir . '/' . $domain . '.crt',
+ 'bundle' => $ssl_dir . '/' . $domain . '.bundle'
);
if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y') {
$cert_paths = array(
'domain' => $domain,
- 'key' => $ssl_dir.'/'.$domain.'-le.key',
- 'key2' => $ssl_dir.'/'.$domain.'-le.key.org',
+ 'key' => $ssl_dir . '/' . $domain . '-le.key',
+ 'key2' => $ssl_dir . '/' . $domain . '-le.key.org',
'csr' => '', # Not used for LE.
- 'crt' => $ssl_dir.'/'.$domain.'-le.crt',
- 'bundle' => $ssl_dir.'/'.$domain.'-le.bundle'
+ 'crt' => $ssl_dir . '/' . $domain . '-le.crt',
+ 'bundle' => $ssl_dir . '/' . $domain . '-le.bundle'
);
}
return $cert_paths;
}
- public function request_certificates($data, $server_type = 'apache') {
- global $app, $conf;
- $app->uses('getconf');
- $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
- $server_config = $app->getconf->get_server_config($conf['server_id'], 'server');
-
- $use_acme = false;
- if($this->get_acme_script()) {
- $use_acme = true;
- } elseif(!$this->get_certbot_script()) {
- $app->log("Unable to find Let's Encrypt client, installing acme.sh.", LOGLEVEL_DEBUG);
- // acme and le missing
- $this->install_acme();
- if($this->get_acme_script()) {
- $use_acme = true;
- } else {
- $app->log("Unable to install acme.sh. Cannot proceed, no Let's Encrypt client found.", LOGLEVEL_WARN);
- return false;
- }
- }
-
- $tmp = $app->letsencrypt->get_website_certificate_paths($data);
- $domain = $tmp['domain'];
- $key_file = $tmp['key'];
- $crt_file = $tmp['crt'];
- $bundle_file = $tmp['bundle'];
+ private function assemble_domains_to_request($data, $main_domain, $do_check) {
+ global $app, $conf;
- // default values
- $temp_domains = array($domain);
- $cli_domain_arg = '';
- $subdomains = null;
- $aliasdomains = null;
+ $certificate_domains = array($main_domain);
//* be sure to have good domain
- if(substr($domain,0,4) != 'www.' && ($data['new']['subdomain'] == "www" || $data['new']['subdomain'] == "*")) {
- $temp_domains[] = "www." . $domain;
+ if(substr($main_domain, 0, 4) != 'www.' && ($data['new']['subdomain'] == "www" || $data['new']['subdomain'] == "*")) {
+ $certificate_domains[] = "www." . $main_domain;
}
//* then, add subdomain if we have
- $subdomains = $app->db->queryAllRecords('SELECT domain FROM web_domain WHERE parent_domain_id = '.intval($data['new']['domain_id'])." AND active = 'y' AND type = 'subdomain' AND ssl_letsencrypt_exclude != 'y'");
+ $subdomains = $app->db->queryAllRecords("SELECT domain FROM web_domain WHERE parent_domain_id = ? AND active = 'y' AND type = 'subdomain' AND ssl_letsencrypt_exclude != 'y'", intval($data['new']['domain_id']));
if(is_array($subdomains)) {
foreach($subdomains as $subdomain) {
- $temp_domains[] = $subdomain['domain'];
+ $certificate_domains[] = $subdomain['domain'];
}
}
//* then, add alias domain if we have
- $aliasdomains = $app->db->queryAllRecords('SELECT domain,subdomain FROM web_domain WHERE parent_domain_id = '.intval($data['new']['domain_id'])." AND active = 'y' AND type = 'alias' AND ssl_letsencrypt_exclude != 'y'");
- if(is_array($aliasdomains)) {
- foreach($aliasdomains as $aliasdomain) {
- $temp_domains[] = $aliasdomain['domain'];
- if(isset($aliasdomain['subdomain']) && substr($aliasdomain['domain'],0,4) != 'www.' && ($aliasdomain['subdomain'] == "www" OR $aliasdomain['subdomain'] == "*")) {
- $temp_domains[] = "www." . $aliasdomain['domain'];
+ $alias_domains = $app->db->queryAllRecords("SELECT domain,subdomain FROM web_domain WHERE parent_domain_id = ? AND active = 'y' AND type = 'alias' AND ssl_letsencrypt_exclude != 'y'", intval($data['new']['domain_id']));
+ if(is_array($alias_domains)) {
+ foreach($alias_domains as $alias_domain) {
+ $certificate_domains[] = $alias_domain['domain'];
+ if(isset($alias_domain['subdomain']) && substr($alias_domain['domain'], 0, 4) != 'www.' && ($alias_domain['subdomain'] == "www" or $alias_domain['subdomain'] == "*")) {
+ $certificate_domains[] = "www." . $alias_domain['domain'];
}
}
}
// prevent duplicate
- $temp_domains = array_unique($temp_domains);
-
- // check if domains are reachable to avoid letsencrypt verification errors
- $le_rnd_file = uniqid('le-', true) . '.txt';
- $le_rnd_hash = md5(uniqid('le-', true));
- if(!is_dir('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/')) {
- $app->system->mkdir('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/', false, 0755, true);
- }
- file_put_contents('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file, $le_rnd_hash);
+ $certificate_domains = array_values(array_unique($certificate_domains));
+
+ // check if domains are reachable to avoid let's encrypt verification errors
+ if($do_check) {
+ $le_rnd_file = uniqid('le-', true) . '.txt';
+ $le_rnd_hash = md5(uniqid('le-', true));
+ if(!is_dir('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/')) {
+ $app->system->mkdir('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/', false, 0755, true);
+ }
+ file_put_contents('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file, $le_rnd_hash);
- $le_domains = array();
- foreach($temp_domains as $temp_domain) {
- if((isset($web_config['skip_le_check']) && $web_config['skip_le_check'] == 'y') || (isset($server_config['migration_mode']) && $server_config['migration_mode'] == 'y')) {
- $le_domains[] = $temp_domain;
- } else {
- $le_hash_check = trim(@file_get_contents('http://' . $temp_domain . '/.well-known/acme-challenge/' . $le_rnd_file));
+ $checked_domains = [];
+ foreach($certificate_domains as $domain_to_check) {
+ $le_hash_check = trim(@file_get_contents('http://' . $domain_to_check . '/.well-known/acme-challenge/' . $le_rnd_file));
if($le_hash_check == $le_rnd_hash) {
- $le_domains[] = $temp_domain;
- $app->log("Verified domain " . $temp_domain . " should be reachable for letsencrypt.", LOGLEVEL_DEBUG);
+ $checked_domains[] = $domain_to_check;
+ $app->log("Verified domain " . $domain_to_check . " should be reachable for let's encrypt.", LOGLEVEL_DEBUG);
} else {
- $app->log("Could not verify domain " . $temp_domain . ", so excluding it from letsencrypt request.", LOGLEVEL_WARN);
+ $app->log("Could not verify domain " . $domain_to_check . ", so excluding it from let's encrypt request.", LOGLEVEL_WARN);
}
}
+ $certificate_domains = $checked_domains;
+ @unlink('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file);
}
- $temp_domains = $le_domains;
- unset($le_domains);
- @unlink('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file);
- $le_domain_count = count($temp_domains);
+ $le_domain_count = count($certificate_domains);
if($le_domain_count > 100) {
- $temp_domains = array_slice($temp_domains, 0, 100);
+ $certificate_domains = array_slice($certificate_domains, 0, 100);
$app->log("There were " . $le_domain_count . " domains in the domain list. LE only supports 100, so we strip the rest.", LOGLEVEL_WARN);
}
- if ($le_domain_count == 0) {
- return false;
+ return $certificate_domains;
+ }
+
+ private function link_file($target, $source) {
+ global $app;
+
+ $needs_link = true;
+ if(@is_link($target)) {
+ $existing_source = readlink($target);
+ if($existing_source == $source) {
+ $needs_link = false;
+ } else {
+ $app->system->unlink($target);
+ }
+ } elseif(is_file($target)) {
+ $suffix = '.old.' . date('YmdHis');
+ $app->system->copy($target, $target . $suffix);
+ $app->system->chmod($target . $suffix, 0400);
+ $app->system->unlink($target);
}
+ if($needs_link) {
+ $app->system->exec_safe("ln -s ? ?", $source, $target);
+ }
+ }
- // unset useless data
- unset($subdomains);
- unset($aliasdomains);
+ public function request_certificates($data, $server_type = 'apache', $desired_signature_type = '') {
+ global $app, $conf;
- $this->certbot_use_certcommand = false;
- $letsencrypt_cmd = '';
- $allow_return_codes = null;
- $old_umask = umask(0022); # work around acme.sh permission bug, see #6015
- if($use_acme) {
- $letsencrypt_cmd = $this->get_acme_command($temp_domains, $key_file, $bundle_file, $crt_file, $server_type);
- $allow_return_codes = array(2);
- // Cleanup ssl cert symlinks, if exists
- if(@is_link($key_file)) unlink($key_file);
- if(@is_link($bundle_file)) unlink($bundle_file);
- if(@is_link($crt_file)) unlink($crt_file);
- } else {
- $letsencrypt_cmd = $this->get_certbot_command($temp_domains);
- umask($old_umask);
+ $app->uses('getconf');
+ $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
+ $server_config = $app->getconf->get_server_config($conf['server_id'], 'server');
+
+ if(!in_array($desired_signature_type, ['RSA', 'ECDSA'])) {
+ $desired_signature_type = $web_config['le_signature_type'] ?: 'RSA';
}
- $success = false;
- if($letsencrypt_cmd) {
- if(!isset($server_config['migration_mode']) || $server_config['migration_mode'] != 'y') {
- $app->log("Create Let's Encrypt SSL Cert for: $domain", LOGLEVEL_DEBUG);
- $app->log("Let's Encrypt SSL Cert domains: $cli_domain_arg", LOGLEVEL_DEBUG);
+ $certificate_paths = $this->get_website_certificate_paths($data);
+ $key_file = $certificate_paths['key'];
+ $crt_file = $certificate_paths['crt'];
+ $bundle_file = $certificate_paths['bundle'];
+ $main_domain = $certificate_paths['domain'];
+ $migration_mode = isset($server_config['migration_mode']) && $server_config['migration_mode'] == 'y';
+ $do_check = (empty($web_config['skip_le_check']) || $web_config['skip_le_check'] == 'n') && !$migration_mode;
+ $certificate_domains = $this->assemble_domains_to_request($data, $main_domain, $do_check);
- $success = $app->system->_exec($letsencrypt_cmd, $allow_return_codes);
- } else {
- $app->log("Migration mode active, skipping Let's Encrypt SSL Cert creation for: $domain", LOGLEVEL_DEBUG);
- $success = true;
- }
+ if(empty($certificate_domains)) {
+ return false;
+ }
+
+ if($migration_mode) {
+ $app->log("Migration mode active, skipping Let's Encrypt SSL Cert creation for: $main_domain", LOGLEVEL_DEBUG);
}
- if($use_acme === true) {
- umask($old_umask);
- if(!$success) {
- $app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' could not be issued.', LOGLEVEL_WARN);
- $app->log($letsencrypt_cmd, LOGLEVEL_WARN);
+ $use_acme = $this->use_acme();
+ if($use_acme === null) {
+ return false;
+ }
+
+ if($use_acme) {
+ if(!$migration_mode) {
+ $letsencrypt_cmd = $this->get_acme_command($certificate_domains, $key_file, $bundle_file, $crt_file, $server_type, $desired_signature_type);
+ // Cleanup ssl cert symlinks, if exists so that amcme.sh can install copies of its files to the target location
+ if(@is_link($key_file)) unlink($key_file);
+ if(@is_link($bundle_file)) unlink($bundle_file);
+ if(@is_link($crt_file)) unlink($crt_file);
+
+ $app->log("Create Let's Encrypt SSL Cert for " . $main_domain . ' (' . $desired_signature_type . ') via acme.sh, domains to include: ' . join(', ', $certificate_domains), LOGLEVEL_DEBUG);
+ $old_umask = umask(0022); # work around acme.sh permission bug, see #6015
+ $success = $letsencrypt_cmd && $app->system->_exec($letsencrypt_cmd, [2]);
+ umask($old_umask);
+ if(!$success) {
+ $app->log("Let's Encrypt SSL Cert for " . $main_domain . ' via acme.sh could not be issued. Used command: ' . $letsencrypt_cmd, LOGLEVEL_WARN);
+ return false;
+ }
+ }
+ // acme.sh directly installs a copy of the certificate at the place we expect them to be, so we are done here
+ return true;
+ } else {
+ if(!$migration_mode) {
+ $letsencrypt_cmd = $this->get_certbot_command($certificate_domains, $desired_signature_type);
+ // get_certbot_command sets $this->certbot_use_certcommand
+ $app->log("Create Let's Encrypt SSL Cert for " . $main_domain . ' (' . $desired_signature_type . ') via certbot, domains to include: ' . join(', ', $certificate_domains), LOGLEVEL_DEBUG);
+ $success = $letsencrypt_cmd && $app->system->_exec($letsencrypt_cmd);
+ if(!$success) {
+ $app->log("Let's Encrypt SSL Cert for " . $main_domain . ' via certbot could not be issued. Used command: ' . $letsencrypt_cmd, LOGLEVEL_WARN);
+ return false;
+ }
+ }
+
+ $discovered_paths = $this->get_letsencrypt_certificate_paths($certificate_domains, $desired_signature_type);
+ if(empty($discovered_paths)) {
+ $app->log("Let's Encrypt Cert file: could not find the issued certificate", LOGLEVEL_WARN);
return false;
+ }
+ $this->link_file($key_file, $discovered_paths['privkey']);
+ $this->link_file($bundle_file, $discovered_paths['chain']);
+ if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) {
+ $this->link_file($crt_file, $discovered_paths['fullchain']);
} else {
- return true;
+ $this->link_file($crt_file, $discovered_paths['cert']);
}
+ return true;
}
+ }
- $le_files = array();
- if($this->certbot_use_certcommand === true && $letsencrypt_cmd) {
- $cli_domain_arg = '';
- // generate cli format
- foreach($temp_domains as $temp_domain) {
- $cli_domain_arg .= (string) " --domains " . $temp_domain;
- }
-
+ /**
+ * Gets a list of all installed certificates on this server.
+ *
+ * @return array
+ */
+ public function get_certificate_list() {
+ global $app, $conf;
- $letsencrypt_cmd = $this->get_certbot_script() . " certificates " . $cli_domain_arg;
- $output = explode("\n", shell_exec($letsencrypt_cmd . " 2>/dev/null | grep -v '^\$'") ?? '');
- $le_path = '';
- $skip_to_next = true;
- $matches = null;
- foreach($output as $outline) {
- $outline = trim($outline);
- $app->log("LE CERT OUTPUT: " . $outline, LOGLEVEL_DEBUG);
+ $shell_script = '';
+ $use_acme = $this->use_acme($shell_script);
+ if($use_acme === null || !$shell_script) {
+ $app->log('get_certificate_list: did not find acme.sh nor certbot', LOGLEVEL_WARN);
+ return [];
+ }
- if($skip_to_next === true && !preg_match('/^\s*Certificate Name/', $outline)) {
+ $candidates = [];
+ if($use_acme) {
+ // Use an inline shell script to get the configured acme.sh certificate home.
+ // We use a shell script because acme.sh config file is a shell script itself - to support even dynamic configs, we will evaluate the config file.
+ // The used --info command was not always there, so we try to auto-upgrade acme.sh when the command fails
+ $home_extract_cmd = join(' ; ', [
+ '_info() { :',
+ ' _info_stdout=$(' . escapeshellarg($shell_script) . ' --info 2>/dev/null)',
+ ' _info_ret=$?',
+ '}',
+ '_echo_home() { :',
+ ' eval "$_info_stdout"',
+ ' _info_ret=$?',
+ ' if [ $_info_ret -eq 0 ]; then :',
+ ' if [ -z "$CERT_HOME" ]',
+ ' then echo "$LE_CONFIG_HOME"',
+ ' else echo "$CERT_HOME"',
+ ' fi',
+ ' else :',
+ ' echo "Error eval-ing --info output (exit code $_info_ret). stdout was: $_info_stdout"',
+ ' exit 1',
+ ' fi',
+ '}',
+ '_info',
+ 'if [ $_info_ret -eq 0 ]; then :',
+ ' _echo_home',
+ 'else :',
+ ' if ' . escapeshellarg($shell_script) . ' --upgrade 2>&1; then :',
+ ' _info',
+ ' if [ $_info_ret -eq 0 ]; then :',
+ ' _echo_home',
+ ' else :',
+ ' echo "--info failed (exit code $_info_ret). stdout was: $_info_stdout"',
+ ' exit 1',
+ ' fi',
+ ' else :',
+ ' echo "--info failed (exit code $_info_ret) and auto-upgrade failed, too. Initial info stdout was: $_info_stdout"',
+ ' exit 1',
+ ' fi',
+ 'fi',
+ ]);
+ $ret = 0;
+ $cert_home = [];
+ exec($home_extract_cmd, $cert_home, $ret);
+ $cert_home = trim(implode("\n", $cert_home));
+ if($ret != 0 || empty($cert_home) || !is_dir($cert_home)) {
+ $app->log('get_certificate_list: could not find certificate home. Error: ' . $cert_home . '. Command used: ' . $home_extract_cmd, LOGLEVEL_ERROR);
+ return [];
+ }
+ $app->log('get_certificate_list: discovered cert home as ' . $cert_home . '. Command used: ' . $home_extract_cmd, LOGLEVEL_DEBUG);
+ $dir = opendir($cert_home);
+ if(!$dir) {
+ $app->log('get_certificate_list: could not open certificate home ' . $cert_home, LOGLEVEL_ERROR);
+ return [];
+ }
+ while($path = readdir($dir)) {
+ $full_path = $cert_home . '/' . $path;
+ // valid conf dirs have a . in them
+ if($path === '.' || $path === '..' || strpos($path, '.') === false || !is_dir($full_path)) {
continue;
}
- $skip_to_next = false;
-
- if(preg_match('/^\s*Expiry.*?VALID:\s+\D/', $outline)) {
- $app->log("Found LE path is expired or invalid: " . $matches[1], LOGLEVEL_DEBUG);
- $skip_to_next = true;
+ $domain = $path;
+ if(preg_match('/_ecc$/', $path)) {
+ $domain = substr($path, 0, -4);
+ }
+ if(!$this->is_readable_link_or_file($full_path . '/' . $domain . '.conf')) {
+ $app->log('get_certificate_list: skip ' . $full_path . '/' . $domain . '.conf because it is not readable', LOGLEVEL_DEBUG);
+ continue;
+ }
+ $candidates[] = [
+ 'source' => 'acme.sh',
+ 'id' => $path,
+ 'conf' => $full_path,
+ 'cert_paths' => [
+ 'cert' => "$full_path/$domain.cer",
+ 'privkey' => "$full_path/$domain.key",
+ 'chain' => "$full_path/ca.cer",
+ 'fullchain' => "$full_path/fullchain.cer",
+ ]
+ ];
+ }
+ } else {
+ if(!is_dir($this->renew_config_path)) {
+ $app->log('get_certificate_list: certbot renew dir not found: ' . $this->renew_config_path, LOGLEVEL_ERROR);
+ return [];
+ }
+ $dir = opendir($this->renew_config_path);
+ if(!$dir) {
+ $app->log('get_certificate_list: could not open certbot renew dir', LOGLEVEL_ERROR);
+ return [];
+ }
+ while($file = readdir($dir)) {
+ $file_path = $this->renew_config_path . $conf['fs_div'] . $file;
+ if($file === '.' || $file === '..' || substr($file, -5) !== '.conf' || !$this->is_readable_link_or_file($file_path)) {
continue;
}
+ $fp = fopen($file_path, 'r');
+ if(!$fp) continue;
+ $certificate = [
+ 'source' => 'certbot',
+ 'id' => substr($file, 0, -5),
+ 'conf' => $file_path,
+ 'cert_paths' => [
+ 'cert' => '',
+ 'privkey' => '',
+ 'chain' => '',
+ 'fullchain' => ''
+ ]
+ ];
+ while(!feof($fp) && $line = fgets($fp)) {
+ $line = trim($line);
+ if($line === '') continue;
+ if($line == '[[webroot_map]]') break;
+ $tmp = explode('=', $line, 2);
+ if(count($tmp) != 2) continue;
+ $key = trim($tmp[0]);
+ if($key == 'cert' || $key == 'privkey' || $key == 'chain' || $key == 'fullchain') {
+ $certificate['cert_paths'][$key] = trim($tmp[1]);
+ }
+ }
+ fclose($fp);
+ $candidates[] = $certificate;
+ }
+ closedir($dir);
+ }
+
+ $certificates = [];
+ foreach($candidates as $certificate) {
+ if($this->is_readable_link_or_file($certificate['cert_paths']['cert'])
+ && $this->is_readable_link_or_file($certificate['cert_paths']['privkey'])
+ && $this->is_readable_link_or_file($certificate['cert_paths']['fullchain'])
+ && $this->is_readable_link_or_file($certificate['cert_paths']['chain'])) {
+ $info = $this->extract_x509($certificate['cert_paths']['cert'], $certificate['cert_paths']['chain']);
+ if($info) {
+ $certificate = array_merge($certificate, $info);
+ $certificates[] = $certificate;
+ $app->log('get_certificate_list found certificate ' . $certificate['conf'] . ' ' . $certificate['signature_type'] . ' ' . $certificate['serial_number'] . ($certificate['is_valid'] ? ' (valid) ' : ' (invalid) ') . join(', ', $certificate['domains']), LOGLEVEL_DEBUG);
+ } else {
+ $app->log('get_certificate_list certificate candidate ' . $certificate['conf'] . ' invalid because X509 extraction was unsuccessful', LOGLEVEL_DEBUG);
+ }
+ } else {
+ $app->log('get_certificate_list certificate candidate ' . $certificate['conf'] . ' invalid because files are missing', LOGLEVEL_DEBUG);
+ }
+ }
+ return $certificates;
+ }
+
+ /** @var array|null */
+ private $_deny_list_domains = null;
+ /** @var array|null */
+ private $_deny_list_serials = null;
+
+ private function get_deny_list() {
+ global $app, $conf;
+
+ if(is_null($this->_deny_list_domains)) {
+ $server_db_record = $app->db->queryOneRecord("SELECT * FROM server WHERE server_id = ?", $conf['server_id']);
+ $app->uses('getconf');
+ $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
- if(preg_match('/^\s*Certificate Path:\s*(\/.*?)\s*$/', $outline, $matches)) {
- $app->log("Found LE path: " . $matches[1], LOGLEVEL_DEBUG);
- $le_path = dirname($matches[1]);
- if(is_dir($le_path)) {
- break;
- } else {
- $le_path = false;
+ $this->_deny_list_domains = empty($web_config['le_auto_cleanup_denylist']) ? [] : array_filter(array_map(function($pattern) use ($server_db_record) {
+ $pattern = trim($pattern);
+ if($server_db_record && $pattern == '[server_name]') {
+ return $server_db_record['server_name'];
+ }
+
+ return $pattern;
+ }, explode(',', $web_config['le_auto_cleanup_denylist'])));
+
+ $this->_deny_list_domains = array_values(array_unique($this->_deny_list_domains));
+
+ // search certificates the installer creates and automatically add their serial numbers to deny list
+ $this->_deny_list_serials = [];
+ foreach([
+ '/usr/local/ispconfig/interface/ssl/ispserver.crt',
+ '/etc/postfix/smtpd.cert',
+ '/etc/ssl/private/pure-ftpd.pem'
+ ] as $possible_cert_file) {
+ $cert = $this->extract_first_certificate($possible_cert_file);
+ if($cert) {
+ $info = $this->extract_x509($cert);
+ if($info) {
+ $app->log('add serial number ' . $info['serial_number'] . ' from ' . $possible_cert_file . ' to deny list', LOGLEVEL_DEBUG);
+ $this->_deny_list_serials[] = $info['serial_number'];
}
}
}
+ $this->_deny_list_serials = array_values(array_unique($this->_deny_list_serials));
+ }
+ return [$this->_deny_list_domains, $this->_deny_list_serials];
+ }
- if($le_path) {
- $le_files = array(
- 'privkey' => $le_path . '/privkey.pem',
- 'chain' => $le_path . '/chain.pem',
- 'cert' => $le_path . '/cert.pem',
- 'fullchain' => $le_path . '/fullchain.pem'
- );
+ /**
+ * Checks if $certificate is on the deny list or has a wildcard domain.
+ * Returns an array of the deny list patterns and serials numbers that matched the certificate.
+ * An empty array means that the $certificate is not on the deny list.
+ *
+ * @param array $certificate
+ * @return array
+ */
+ public function check_deny_list($certificate) {
+ list($deny_list_domains, $deny_list_serials) = $this->get_deny_list();
+ $on_deny_list = [];
+ foreach($certificate['domains'] as $cert_domain) {
+ if(substr($cert_domain, 0, 2) == '*.') {
+ // wildcard domains are always on the deny list
+ $on_deny_list[] = $cert_domain;
+ } else {
+ $on_deny_list = array_merge($on_deny_list, array_filter($deny_list_domains, function($deny_pattern) use ($cert_domain) {
+ return mb_strtolower($deny_pattern) == mb_strtolower($cert_domain) || fnmatch($deny_pattern, $cert_domain, FNM_CASEFOLD);
+ }));
}
}
- if(empty($le_files)) {
- $le_files = $this->get_letsencrypt_certificate_paths($temp_domains);
+ if(in_array($certificate['serial_number'], $deny_list_serials, true)) {
+ $on_deny_list[] = $certificate['serial_number'];
}
- unset($temp_domains);
+ return $on_deny_list;
+ }
- if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) {
- $crt_tmp_file = $le_files['fullchain'];
- } else {
- $crt_tmp_file = $le_files['cert'];
- }
+ /**
+ * Remove and maybe revoke a certificate.
+ * @param array $certificate the certificate (from get_certificate_list())
+ * @param null|bool $revoke_before_delete try to revoke certificate before deletion. when `null` the configured default is used.
+ * @param bool $check_deny_list refuse to delete certificate when it is on the servers purge deny list.
+ * @return bool whether the certificate could be removed
+ */
+ public function remove_certificate($certificate, $revoke_before_delete = null, $check_deny_list = true) {
+ global $app, $conf;
- $key_tmp_file = $le_files['privkey'];
- $bundle_tmp_file = $le_files['chain'];
+ if(is_null($revoke_before_delete)) {
+ $app->uses('getconf');
+ $web_config = $app->getconf->get_server_config($conf['server_id'], 'web');
+ $revoke_before_delete = !empty($web_config['le_revoke_before_delete']) && $web_config['le_revoke_before_delete'] == 'y';
+ }
- if(!$success) {
- // error issuing cert
- $app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' could not be issued.', LOGLEVEL_WARN);
- $app->log($letsencrypt_cmd, LOGLEVEL_WARN);
+ if($certificate['is_revoked'] && $revoke_before_delete) {
+ $revoke_before_delete = false;
+ $app->log('remove_certificate: skip revokation of ' . $certificate['id'] . ' because it already is revoked', LOGLEVEL_DEBUG);
+ }
- // if cert already exists, dont remove it. Ex. expired/misstyped/noDnsYet alias domain, api down...
- if(!file_exists($crt_tmp_file)) {
+ if($check_deny_list) {
+ $on_deny_list = $this->check_deny_list($certificate);
+ if(!empty($on_deny_list)) {
+ $app->log('remove_certificate: did not remove ' . $certificate['id'] . ' because one of its domains is on deny list or a wildcard domain (' . join(', ', $on_deny_list) . ')', LOGLEVEL_DEBUG);
return false;
}
}
- //* check is been correctly created
- if(file_exists($crt_tmp_file)) {
- $app->log("Let's Encrypt Cert file: $crt_tmp_file exists.", LOGLEVEL_DEBUG);
- $date = date("YmdHis");
-
- //* TODO: check if is a symlink, if target same keep it, either remove it
- if(is_file($key_file)) {
- $app->system->copy($key_file, $key_file.'.old.'.$date);
- $app->system->chmod($key_file.'.old.'.$date, 0400);
- $app->system->unlink($key_file);
+ if($certificate['source'] == 'certbot') {
+ $certbot_script = $this->get_certbot_script();
+ if(!$certbot_script) {
+ $app->log("remove_certificate: certbot not found, cannot delete " . $certificate['id'], LOGLEVEL_WARN);
+ return false;
}
-
- if(@is_link($key_file)) $app->system->unlink($key_file);
- if(@file_exists($key_tmp_file)) $app->system->exec_safe("ln -s ? ?", $key_tmp_file, $key_file);
-
- if(is_file($crt_file)) {
- $app->system->copy($crt_file, $crt_file.'.old.'.$date);
- $app->system->chmod($crt_file.'.old.'.$date, 0400);
- $app->system->unlink($crt_file);
+ $version = $this->get_certbot_version($certbot_script);
+ if($revoke_before_delete && $this->is_readable_link_or_file($certificate['cert_paths']['cert'])) {
+ if(version_compare($version, '0.22', '>=')) {
+ $server = 'https://acme-v02.api.letsencrypt.org/directory';
+ } else {
+ $server = 'https://acme-v01.api.letsencrypt.org/directory';
+ }
+ $app->system->exec_safe($certbot_script . ' revoke -n --server ? --cert-path ? --reason cessationofoperation 2>&1', $server, $certificate['cert_paths']['cert']);
+ if($app->system->last_exec_retcode() == 0) {
+ $app->log('remove_certificate: certbot revoked ' . $certificate['id'] . ' before deletion', LOGLEVEL_DEBUG);
+ } else {
+ $app->log('remove_certificate: certbot revoke ' . $certificate['id'] . ' before deletion failed: ' . $app->system->last_exec_out(), LOGLEVEL_WARN);
+ }
+ } else {
+ $app->log('remove_certificate: certbot skip revoke ' . $certificate['id'] . ' before deletion', LOGLEVEL_DEBUG);
}
+ // the revoke command above might already have done the delete
+ if(is_file($certificate['conf'])) {
+ if(version_compare($version, '0.30.0', '<')) {
+ $app->log('remove_certificate: certbot is very old. Please update for proper certificate deletion.', LOGLEVEL_WARN);
+ } else {
+ $app->system->exec_safe($certbot_script . ' delete -n --cert-name ? 2>&1', $certificate['id']);
+ if($app->system->last_exec_retcode() != 0) {
+ $app->log('remove_certificate: certbot delete -n --cert-name ' . $certificate['id'] . ' failed: ' . $app->system->last_exec_out(), LOGLEVEL_WARN);
+ }
+ }
+ }
+ // if the conf file is still lingering around, we move it out of the way
+ if(is_file($certificate['conf'])) {
+ @rename($certificate['conf'], $certificate['conf'] . '.removed');
+ $app->log('remove_certificate: manually move renew conf ' . $certificate['conf'] . ' out of the way.', LOGLEVEL_DEBUG);
+ }
+ } else {
+ if(is_dir($certificate['conf'])) {
+ if($revoke_before_delete) {
+ $acme_script = $this->get_acme_script();
+ if($acme_script) {
+ $cert_selection = '';
+ $domain = $certificate['id'];
+ if(substr($domain, -4) == '_ecc') {
+ $cert_selection = '--ecc';
+ $domain = substr($domain, 0, -4);
+ }
+ // 5 = cessationOfOperation, see https://github.com/acmesh-official/acme.sh/wiki/revokecert
+ $app->system->exec_safe($acme_script . ' --revoke --revoke-reason 5 -d ? ' . $cert_selection . ' 2>&1', $domain);
+ if($app->system->last_exec_retcode() == 0) {
+ $app->log('remove_certificate: acme.sh revoked ' . $certificate['id'] . ' before deletion', LOGLEVEL_DEBUG);
+ } else {
+ $app->log('remove_certificate: acme.sh revoke ' . $certificate['id'] . ' before deletion failed: ' . $app->system->last_exec_out(), LOGLEVEL_WARN);
+ }
+ }
+ } else {
+ $app->log('remove_certificate: acme.sh skip revoke ' . $certificate['id'] . ' before deletion', LOGLEVEL_DEBUG);
+ }
+ if(!$app->system->rmdir($certificate['conf'], true)) {
+ $app->log('remove_certificate: could not delete config folder ' . $certificate['conf'], LOGLEVEL_WARN);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
- if(@is_link($crt_file)) $app->system->unlink($crt_file);
- if(@file_exists($crt_tmp_file))$app->system->exec_safe("ln -s ? ?", $crt_tmp_file, $crt_file);
-
- if(is_file($bundle_file)) {
- $app->system->copy($bundle_file, $bundle_file.'.old.'.$date);
- $app->system->chmod($bundle_file.'.old.'.$date, 0400);
- $app->system->unlink($bundle_file);
+ public function extract_x509($cert_file_or_contents, $chain_file = null) {
+ global $app;
+ if(!function_exists('openssl_x509_parse')) {
+ $app->log('extract_x509: openssl extension missing', LOGLEVEL_ERROR);
+ return false;
+ }
+ $cert_file = false;
+ if(strpos($cert_file_or_contents, '-----BEGIN CERTIFICATE-----') === false) {
+ $cert_file = $cert_file_or_contents;
+ $cert_file_or_contents = file_get_contents($cert_file_or_contents);
+ }
+ $info = openssl_x509_parse($cert_file_or_contents, true);
+ if(!$info) {
+ $app->log('extract_x509: ' . ($cert_file ?: 'inline certificate') . ' could not be parsed', LOGLEVEL_ERROR);
+ return false;
+ }
+ if(empty($info['subject']['CN']) || !$this->is_domain_name_or_wildcard($info['subject']['CN'])) {
+ $domains = [];
+ } else {
+ $domains = [$app->functions->idn_encode($info['subject']['CN'])];
+ }
+ if(!empty($info['extensions']) && !empty($info['extensions']['subjectAltName'])) {
+ $domains = array_filter(array_merge($domains, array_map(function($i) {
+ global $app;
+ $parts = explode(':', $i, 2);
+ if(count($parts) < 2) {
+ return false;
+ }
+ $maybe_domain = trim($parts[1]);
+ if(filter_var($maybe_domain, FILTER_VALIDATE_IP)) {
+ return $maybe_domain;
+ }
+ if($this->is_domain_name_or_wildcard($maybe_domain)) {
+ return $app->functions->idn_encode($maybe_domain);
+ }
+ return false;
+ }, explode(',', $info['extensions']['subjectAltName']))));
+ $domains = array_values(array_unique($domains));
+ }
+ if(empty($domains)) {
+ return false;
+ }
+ $valid_from = new DateTime('@' . $info['validFrom_time_t']);
+ $valid_to = new DateTime('@' . $info['validTo_time_t']);
+ $now = new DateTime();
+ $is_valid = $valid_from <= $now && $now <= $valid_to;
+ $is_revoked = null;
+ // only do online revokation check when cert is valid and we got the required chain
+ if($is_valid && $cert_file && $this->is_readable_link_or_file($chain_file)) {
+ $ocsp_uri = $app->system->exec_safe('openssl x509 -noout -ocsp_uri -in ? 2>&1', $cert_file);
+ $ocsp_host = parse_url($ocsp_uri ?: '', PHP_URL_HOST);
+ if($ocsp_uri && $ocsp_host) {
+ $ocsp_response = $app->system->system_safe('openssl ocsp -issuer ? -cert ? -text -url ? -header HOST=? 2>&1', $chain_file, $cert_file, $ocsp_uri, $ocsp_host);
+ if($app->system->last_exec_retcode() == 0) {
+ $is_revoked = strpos($ocsp_response, 'Cert Status: good') === false;
+ if($is_revoked) {
+ $is_valid = false;
+ }
+ } else {
+ $app->log('extract_x509: ' . $cert_file . ' getting OCSP response from ' . $ocsp_uri . ' failed: ' . $ocsp_response, LOGLEVEL_WARN);
+ }
}
+ }
+ $signature_type = 'RSA';
+ $long_type = strtolower(isset($info['signatureTypeLN']) ? $info['signatureTypeLN'] : '?');
+ if(strpos($long_type, 'ecdsa') !== false) {
+ $signature_type = 'ECDSA';
+ }
+ return [
+ 'serial_number' => $info['serialNumberHex'] ?: $info['serialNumber'],
+ 'signature_type' => $signature_type,
+ 'subject' => $info['subject'],
+ 'issuer' => $info['issuer'],
+ 'domains' => $domains,
+ 'is_valid' => $is_valid,
+ 'is_revoked' => $is_revoked,
+ 'valid_from' => $valid_from,
+ 'valid_to' => $valid_to,
+ ];
+ }
- if(@is_link($bundle_file)) $app->system->unlink($bundle_file);
- if(@file_exists($bundle_tmp_file)) $app->system->exec_safe("ln -s ? ?", $bundle_tmp_file, $bundle_file);
+ private function extract_first_certificate($file) {
+ if(!$this->is_readable_link_or_file($file)) {
+ return false;
+ }
+ $contents = file_get_contents($file);
+ if(!$contents) {
+ return false;
+ }
+ $matches = [];
+ if(!preg_match('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/ms', $contents, $matches)) {
+ return false;
+ }
+ return $matches[0];
+ }
- return true;
- } else {
- $app->log("Let's Encrypt Cert file: $crt_tmp_file does not exist.", LOGLEVEL_DEBUG);
+ private function is_domain_name_or_wildcard($input) {
+ $input = filter_var($input, FILTER_VALIDATE_DOMAIN);
+ if(!$input) {
return false;
}
+ // $input can still be something like "some. invalid . domain % name", so we check with a simple regex that no unusual things are in domain name
+ return preg_match("/^(\*\.)?[\w\p{L}0-9._-]+$/u", $input);
+ }
+
+ private function is_readable_link_or_file($path) {
+ return $path && (@is_link($path) || @is_file($path)) && @is_readable($path);
}
}
diff --git a/server/plugins-available/apache2_plugin.inc.php b/server/plugins-available/apache2_plugin.inc.php
index 59182185e7b99ada7f6a6097bf23ee4baba80655..b7e1de3654fc396be63ca5e6e7c5abccae066c26 100644
--- a/server/plugins-available/apache2_plugin.inc.php
+++ b/server/plugins-available/apache2_plugin.inc.php
@@ -2197,16 +2197,24 @@ class apache2_plugin {
} else {
$app->system->exec_safe('umount -l ? 2>/dev/null', $data['old']['document_root'].'/'.$log_folder);
}
+ }
- // remove letsencrypt if it exists (renew will always fail otherwise)
-
- $old_domain = $data['old']['domain'];
- if(substr($old_domain, 0, 2) === '*.') {
- // wildcard domain not yet supported by letsencrypt!
- $old_domain = substr($old_domain, 2);
+ // remove (and maybe revoke) Let's Encrypt certificate (renew will always fail otherwise)
+ if(!empty($web_config['le_delete_on_site_remove']) && $web_config['le_delete_on_site_remove'] == 'y'
+ && !empty($data['old']['document_root'])
+ && $data['old']['ssl_letsencrypt'] == 'y' && $data['old']['ssl'] == 'y') {
+ $app->uses('letsencrypt');
+ $paths = $app->letsencrypt->get_website_certificate_paths(['new' => $data['old']]);
+ $info = $app->letsencrypt->extract_x509($paths['crt']);
+ if($info) {
+ $certificates = $app->letsencrypt->get_certificate_list();
+ foreach($certificates as $certificate) {
+ if($certificate['serial_number'] == $info['serial_number']) {
+ $app->letsencrypt->remove_certificate($certificate);
+ break;
+ }
+ }
}
- $le_conf_file = '/etc/letsencrypt/renewal/' . $old_domain . '.conf';
- @rename('/etc/letsencrypt/renewal/' . $old_domain . '.conf', '/etc/letsencrypt/renewal/' . $old_domain . '.conf~backup');
}
//* remove mountpoint from fstab
diff --git a/server/plugins-available/nginx_plugin.inc.php b/server/plugins-available/nginx_plugin.inc.php
index b36012843cd76562d435378a01088eea21459dea..63bc139c9242aecc410b046b89ef507bb9177e18 100644
--- a/server/plugins-available/nginx_plugin.inc.php
+++ b/server/plugins-available/nginx_plugin.inc.php
@@ -2183,16 +2183,24 @@ class nginx_plugin {
} else {
$app->system->exec_safe('umount ? 2>/dev/null', $data['old']['document_root'].'/'.$log_folder);
}
+ }
- // remove letsencrypt if it exists (renew will always fail otherwise)
-
- $old_domain = $data['old']['domain'];
- if(substr($old_domain, 0, 2) === '*.') {
- // wildcard domain not yet supported by letsencrypt!
- $old_domain = substr($old_domain, 2);
+ // remove (and maybe revoke) Let's Encrypt certificate (renew will always fail otherwise)
+ if(!empty($web_config['le_delete_on_site_remove']) && $web_config['le_delete_on_site_remove'] == 'y'
+ && !empty($data['old']['document_root'])
+ && $data['old']['ssl_letsencrypt'] == 'y' && $data['old']['ssl'] == 'y') {
+ $app->uses('letsencrypt');
+ $paths = $app->letsencrypt->get_website_certificate_paths(['new' => $data['old']]);
+ $info = $app->letsencrypt->extract_x509($paths['crt']);
+ if($info) {
+ $certificates = $app->letsencrypt->get_certificate_list();
+ foreach($certificates as $certificate) {
+ if($certificate['serial_number'] == $info['serial_number']) {
+ $app->letsencrypt->remove_certificate($certificate);
+ break;
+ }
+ }
}
- $le_conf_file = '/etc/letsencrypt/renewal/' . $old_domain . '.conf';
- @rename('/etc/letsencrypt/renewal/' . $old_domain . '.conf', '/etc/letsencrypt/renewal/' . $old_domain . '.conf~backup');
}
//* remove mountpoint from fstab
diff --git a/server/scripts/letsencrypt_post_hook.sh b/server/scripts/letsencrypt_post_hook.sh
index caef1e2d210d2799a7765268994da39cc1226300..01a0955cce87481cd82746d7f2acac187876e585 100644
--- a/server/scripts/letsencrypt_post_hook.sh
+++ b/server/scripts/letsencrypt_post_hook.sh
@@ -1,14 +1,7 @@
#!/bin/bash
-### BEGIN INIT INFO
-# Provides: LETSENCRYPT POST HOOK SCRIPT
-# Required-Start: $local_fs $network
-# Required-Stop: $local_fs
-# Default-Start: 2 3 4 5
-# Default-Stop: 0 1 6
# Short-Description: LETSENCRYPT POST HOOK SCRIPT
# Description: To force close http port 80 if it is by default closed, to be used by letsencrypt client standlone command
-### END INIT INFO
## If you need a custom hook file, create a file with the same name in
## /usr/local/ispconfig/server/conf-custom/scripts/
@@ -17,44 +10,35 @@
##
## Eg. you can override the ispc_letsencrypt_firewall_disable() function then 'return 124'
## to customize the firewall setup.
-if [ -e "/usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_post_hook.sh" ] ; then
- . /usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_post_hook.sh
- ret=$?
- if [ $ret != 124 ]; then exit $ret; fi
+if [ -e "/usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_post_hook.sh" ]; then
+ . /usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_post_hook.sh
+ ret=$?
+ if [ $ret != 124 ]; then exit $ret; fi
fi
declare -F ispc_letsencrypt_firewall_disable &>/dev/null || ispc_letsencrypt_firewall_disable() {
- # delete 'ispc-letsencrypt' chain
- iptables -D INPUT -p tcp --dport 80 -j ispc-letsencrypt
- iptables -F ispc-letsencrypt
- iptables -X ispc-letsencrypt
+ # delete 'ispc-letsencrypt' chain
+ iptables -D INPUT -p tcp --dport 80 -j ispc-letsencrypt
+ iptables -F ispc-letsencrypt
+ iptables -X ispc-letsencrypt
}
ispc_letsencrypt_firewall_disable
-
# For RHEL, Centos or derivatives
-if which yum &> /dev/null 2>&1 ; then
- # Check if web server software is installed, start it if any
- if [ rpm -q nginx ]; then service nginx start
- elif [ rpm -q httpd ]; then service httpd start
-# # If using firewalld
-# elif [ rpm -q firewalld ] && [ `firewall-cmd --state` = running ]; then
-# firewall-cmd --zone=public --permanent --remove-service=http
-# firewall-cmd --reload
-# # If using UFW
-# elif [ rpm -q ufw ]; then ufw --force enable && ufw deny http
- fi
+if which yum &>/dev/null 2>&1; then
+ # Check if web server software is installed, start it if any
+ if rpm -q nginx; then
+ service nginx start
+ elif rpm -q httpd; then
+ service httpd start
+ fi
# For Debian, Ubuntu or derivatives
-elif apt-get -v >/dev/null 2>&1 ; then
- # Check if web server software is installed, stop it if any
- if [ $(dpkg-query -W -f='${Status}' nginx 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service nginx start
- elif [ $(dpkg-query -W -f='${Status}' apache2 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service apache2 start
-# # If using UFW
-# elif [ $(dpkg-query -W -f='${Status}' ufw 2>/dev/null | grep -c "ok installed") -eq 1 ]; then ufw --force enable && ufw deny http
- fi
-## Try iptables as a final attempt
-#else
-# iptables -D INPUT -p tcp --dport 80 -j ACCEPT
-# service iptables save
+elif apt-get -v >/dev/null 2>&1; then
+ # Check if web server software is installed, stop it if any
+ if [ "$(dpkg-query -W -f='${Status}' nginx 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then
+ service nginx start
+ elif [ "$(dpkg-query -W -f='${Status}' apache2 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then
+ service apache2 start
+ fi
fi
diff --git a/server/scripts/letsencrypt_pre_hook.sh b/server/scripts/letsencrypt_pre_hook.sh
index 41aa0184307cadd20a784c6a44c865d75052d0c4..6466b6623caace0b31c0cbf8e1757a9a5f8a30f4 100644
--- a/server/scripts/letsencrypt_pre_hook.sh
+++ b/server/scripts/letsencrypt_pre_hook.sh
@@ -1,14 +1,7 @@
#!/bin/bash
-### BEGIN INIT INFO
-# Provides: LETSENCRYPT PRE HOOK SCRIPT
-# Required-Start: $local_fs $network
-# Required-Stop: $local_fs
-# Default-Start: 2 3 4 5
-# Default-Stop: 0 1 6
# Short-Description: LETSENCRYPT PRE HOOK SCRIPT
# Description: To force open http port 80 to be used by letsencrypt client standlone command
-### END INIT INFO
## If you need a custom hook file, create a file with the same name in
## /usr/local/ispconfig/server/conf-custom/scripts/
@@ -17,45 +10,30 @@
##
## Eg. you can override the ispc_letsencrypt_firewall_enable() function then 'return 124'
## to customize the firewall setup.
-if [ -e "/usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_pre_hook.sh" ] ; then
- . /usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_pre_hook.sh
- ret=$?
- if [ $ret != 124 ]; then exit $ret; fi
+if [ -e "/usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_pre_hook.sh" ]; then
+ . /usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_pre_hook.sh
+ ret=$?
+ if [ $ret != 124 ]; then exit $ret; fi
fi
declare -F ispc_letsencrypt_firewall_enable &>/dev/null || ispc_letsencrypt_firewall_enable() {
- # create 'ispc-letsencrypt' chain with ACCEPT policy and send port 80 there
- iptables -N ispc-letsencrypt
- iptables -I ispc-letsencrypt -p tcp --dport 80 -j ACCEPT
- iptables -A ispc-letsencrypt -j RETURN
- iptables -I INPUT -p tcp --dport 80 -j ispc-letsencrypt
+ # create 'ispc-letsencrypt' chain with ACCEPT policy and send port 80 there
+ iptables -N ispc-letsencrypt
+ iptables -I ispc-letsencrypt -p tcp --dport 80 -j ACCEPT
+ iptables -A ispc-letsencrypt -j RETURN
+ iptables -I INPUT -p tcp --dport 80 -j ispc-letsencrypt
}
ispc_letsencrypt_firewall_enable
# For RHEL, Centos or derivatives
-if which yum &> /dev/null 2>&1 ; then
- # Check if web server software is installed, stop it if any
- if [ rpm -q nginx ]; then service nginx stop; fi
- if [ rpm -q httpd ]; then service httpd stop; fi
-# # If using firewalld
-# if [ rpm -q firewalld ] && [ `firewall-cmd --state` = running ]; then
-# firewall-cmd --zone=public --permanent --add-service=http
-# firewall-cmd --reload
-# fi
-# # If using UFW
-# if [ rpm -q ufw ]; then ufw --force enable && ufw allow http; fi
-
+if which yum &>/dev/null 2>&1; then
+ # Check if web server software is installed, stop it if any
+ if rpm -q nginx; then service nginx stop; fi
+ if rpm -q httpd; then service httpd stop; fi
# For Debian, Ubuntu or derivatives
-elif apt-get -v >/dev/null 2>&1 ; then
- # Check if web server software is installed, stop it if any
- if [ $(dpkg-query -W -f='${Status}' nginx 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service nginx stop; fi
- if [ $(dpkg-query -W -f='${Status}' apache2 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service apache2 stop; fi
-# # If using UFW
-# if [ $(dpkg-query -W -f='${Status}' ufw 2>/dev/null | grep -c "ok installed") -eq 1 ]; then ufw --force enable && ufw allow http; fi
-
-## Try iptables as a final attempt
-#else
-# iptables -I INPUT -p tcp --dport 80 -j ACCEPT
-# service iptables save
+elif apt-get -v >/dev/null 2>&1; then
+ # Check if web server software is installed, stop it if any
+ if [ "$(dpkg-query -W -f='${Status}' nginx 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service nginx stop; fi
+ if [ "$(dpkg-query -W -f='${Status}' apache2 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service apache2 stop; fi
fi
diff --git a/server/scripts/letsencrypt_renew_hook.sh b/server/scripts/letsencrypt_renew_hook.sh
index 5ec9912f59d87728b83cf0fcd233b78146d2f456..3bc70652ef6f730a82909ba12547a86e872d5e9b 100644
--- a/server/scripts/letsencrypt_renew_hook.sh
+++ b/server/scripts/letsencrypt_renew_hook.sh
@@ -1,59 +1,77 @@
#!/bin/bash
-### BEGIN INIT INFO
-# Provides: LETSENCRYPT RENEW HOOK SCRIPT
-# Required-Start: $local_fs $network
-# Required-Stop: $local_fs
-# Default-Start: 2 3 4 5
-# Default-Stop: 0 1 6
# Short-Description: LETSENCRYPT RENEW HOOK SCRIPT
# Description: Taken from LE4ISPC code. To be used to update ispserver.pem automatically after ISPConfig LE SSL certs are renewed and to reload / restart important ISPConfig server services
-### END INIT INFO
## If you need a custom hook file, create a file with the same name in
## /usr/local/ispconfig/server/conf-custom/scripts/
##
## End the file with 'return 124' to signal that this script should not terminate.
-if [ -e "/usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_renew_hook.sh" ] ; then
- . /usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_renew_hook.sh
- ret=$?
- if [ $ret != 124 ]; then exit $ret; fi
+if [ -e "/usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_renew_hook.sh" ]; then
+ . /usr/local/ispconfig/server/conf-custom/scripts/letsencrypt_renew_hook.sh
+ ret=$?
+ if [ $ret != 124 ]; then exit $ret; fi
fi
hostname=$(hostname -f)
-if [ -d "/usr/local/ispconfig/server/scripts/${hostname}" ] ; then
- lelive="/usr/local/ispconfig/server/scripts/${hostname}" ;
-elif [ -d "/root/.acme.sh/${hostname}" ] ; then
- lelive="/root/.acme.sh/${hostname}" ;
-else
- lelive="/etc/letsencrypt/live/${hostname}" ;
+
+# If you want to manually execute letsencrypt_renew_hook.sh, call it with the SUCCESS environment variable set.
+# E.g. like this: "SUCCESS=1 letsencrypt_renew_hook.sh"
+# Then we assume that the certificate is there and do the post-processing.
+SUCCESS=${SUCCESS:-}
+
+# acme.sh defines/exports the environment variables
+# CERT_PATH, CERT_KEY_PATH, CA_CERT_PATH, CERT_FULLCHAIN_PATH and Le_Domain (main cert domain)
+# for all hooks
+if [ -f "$CERT_KEY_PATH" ] && [[ "${Le_Domain:-}" == "$hostname" ]]; then
+ SUCCESS=acme.sh
+ echo "$(/bin/date)" "Reconfigure and reload services after $hostname certificate issuing/renewal via acme.sh" >>/var/log/ispconfig/ispconfig.log
+# certbot defines/exports the environment variables
+# RENEWED_DOMAINS (all cert domains space separated) and RENEWED_LINEAGE (directory in /etc/letsencrypt/live)
+# for the renew/deploy hook
+elif [ -d "$RENEWED_LINEAGE" ] && [[ "$RENEWED_DOMAINS " == "$hostname "* ]]; then
+ SUCCESS=certbot
+ echo "$(/bin/date)" "Reconfigure and reload services after $hostname certificate issuing/renewal via certbot" >>/var/log/ispconfig/ispconfig.log
fi
-if [ -d "$lelive" ]; then
- cd /usr/local/ispconfig/interface/ssl; ibak=ispserver.*.bak; ipem=ispserver.pem; icrt=ispserver.crt; ikey=ispserver.key
- if ls $ibak 1> /dev/null 2>&1; then rm $ibak; fi
- if [ -e "$ipem" ]; then mv $ipem $ipem-$(date +"%y%m%d%H%M%S").bak; cat $ikey $icrt > $ipem; chmod 600 $ipem; fi
- pureftpdpem=/etc/ssl/private/pure-ftpd.pem; if [ -e "$pureftpdpem" ]; then chmod 600 $pureftpdpem; fi
- # For Red Hat, Centos or derivatives
- if which yum &> /dev/null 2>&1 ; then
- if ( rpm -q pure-ftpd ); then service pure-ftpd restart; fi
- if ( rpm -q monit ); then service monit restart; fi
- if ( rpm -q postfix ); then service postfix restart; fi
- if ( rpm -q dovecot ); then service dovecot restart; fi
- if ( rpm -q mysql-server ); then service mysqld restart; fi
- if ( rpm -q mariadb-server ); then service mariadb restart; fi
- if ( rpm -q MariaDB-server ); then service mysql restart; fi
- if ( rpm -q nginx ); then service nginx restart; fi
- if ( rpm -q httpd ); then service httpd restart; fi
- # For Debian, Ubuntu or derivatives
- elif apt-get -v >/dev/null 2>&1 ; then
- if [ $(dpkg-query -W -f='${Status}' pure-ftpd-mysql 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service pure-ftpd-mysql restart; fi
- if [ $(dpkg-query -W -f='${Status}' monit 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service monit restart; fi
- if [ $(dpkg-query -W -f='${Status}' postfix 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service postfix restart; fi
- if [ $(dpkg-query -W -f='${Status}' dovecot-imapd 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service dovecot restart; fi
- if [ $(dpkg-query -W -f='${Status}' mysql 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service mysql restart; fi
- if [ $(dpkg-query -W -f='${Status}' mariadb 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service mysql restart; fi
- if [ $(dpkg-query -W -f='${Status}' nginx 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service nginx restart; fi
- if [ $(dpkg-query -W -f='${Status}' apache2 2>/dev/null | grep -c "ok installed") -eq 1 ]; then service apache2 restart; fi
+if [ -n "$SUCCESS" ]; then
+ if cd /usr/local/ispconfig/interface/ssl; then
+ ipem=ispserver.pem
+ icrt=ispserver.crt
+ ikey=ispserver.key
+ if ls ispserver.*.bak &>/dev/null; then
+ rm ispserver.*.bak
fi
-else echo `/bin/date` "Your Lets Encrypt SSL certs path for your ISPConfig server FQDN is missing.$line" >> /var/log/ispconfig/ispconfig.log; fi
+ if [ -e "$ipem" ]; then
+ mv $ipem "$ipem-$(date +"%y%m%d%H%M%S").bak"
+ cat $ikey $icrt >$ipem
+ chmod 600 $ipem
+ fi
+ fi
+ pureftpdpem=/etc/ssl/private/pure-ftpd.pem
+ if [ -e "$pureftpdpem" ]; then chmod 600 $pureftpdpem; fi
+ # For Red Hat, Centos or derivatives
+ if which yum &>/dev/null 2>&1; then
+ if rpm -q pure-ftpd; then service pure-ftpd restart; fi
+ if rpm -q monit; then service monit restart; fi
+ if rpm -q postfix; then service postfix restart; fi
+ if rpm -q dovecot; then service dovecot restart; fi
+ if rpm -q mysql-server; then service mysqld restart; fi
+ if rpm -q mariadb-server; then service mariadb restart; fi
+ if rpm -q MariaDB-server; then service mysql restart; fi
+ if rpm -q nginx; then service nginx restart; fi
+ if rpm -q httpd; then service httpd restart; fi
+ # For Debian, Ubuntu or derivatives
+ elif apt-get -v >/dev/null 2>&1; then
+ if [ "$(dpkg-query -W -f='${Status}' pure-ftpd-mysql 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service pure-ftpd-mysql restart; fi
+ if [ "$(dpkg-query -W -f='${Status}' monit 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service monit restart; fi
+ if [ "$(dpkg-query -W -f='${Status}' postfix 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service postfix restart; fi
+ if [ "$(dpkg-query -W -f='${Status}' dovecot-imapd 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service dovecot restart; fi
+ if [ "$(dpkg-query -W -f='${Status}' mysql 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service mysql restart; fi
+ if [ "$(dpkg-query -W -f='${Status}' mariadb 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service mysql restart; fi
+ if [ "$(dpkg-query -W -f='${Status}' nginx 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service nginx restart; fi
+ if [ "$(dpkg-query -W -f='${Status}' apache2 2>/dev/null | grep -c "ok installed")" -eq 1 ]; then service apache2 restart; fi
+ fi
+else
+ echo "$(/bin/date)" "Your Lets Encrypt SSL certs path for your ISPConfig server FQDN is missing." >>/var/log/ispconfig/ispconfig.log
+fi