From 779f1ca5081056e342d0a14e821f87bad181a1e8 Mon Sep 17 00:00:00 2001 From: Jan Thiel Date: Fri, 5 Feb 2021 07:26:30 +0100 Subject: [PATCH 1/4] WIP, Refactoring of Lets Encrypt Handling for Multiserver Setup support --- install/lib/installer_base.lib.php | 2 +- install/sql/incremental/upd_0092.sql | 3 + install/sql/ispconfig3.sql | 1 + server/lib/app.inc.php | 2 +- .../classes/cron.d/900-letsencrypt.inc.php | 5 +- server/lib/classes/letsencrypt.inc.php | 599 +++++++++++++----- server/lib/classes/openssl.inc.php | 78 +++ server/plugins-available/nginx_plugin.inc.php | 86 ++- 8 files changed, 574 insertions(+), 202 deletions(-) create mode 100644 install/sql/incremental/upd_0092.sql create mode 100644 server/lib/classes/openssl.inc.php diff --git a/install/lib/installer_base.lib.php b/install/lib/installer_base.lib.php index f73aefe2c5..2f4ed7807b 100644 --- a/install/lib/installer_base.lib.php +++ b/install/lib/installer_base.lib.php @@ -658,7 +658,7 @@ class installer_base { $this->warning('Unable to set rights of user in master database: '.$value['db']."\n Query: ".$query."\n Error: ".$this->dbmaster->errorMessage); } - $query = "GRANT SELECT, UPDATE (`ssl`, `ssl_letsencrypt`, `ssl_request`, `ssl_cert`, `ssl_action`, `ssl_key`) ON ?? TO ?@?"; + $query = "GRANT SELECT, UPDATE (`ssl`, `ssl_valid_until`, `ssl_letsencrypt`, `ssl_request`, `ssl_cert`, `ssl_action`, `ssl_key`) ON ?? TO ?@?"; if ($verbose){ echo $query ."\n"; } diff --git a/install/sql/incremental/upd_0092.sql b/install/sql/incremental/upd_0092.sql new file mode 100644 index 0000000000..20f8a82b79 --- /dev/null +++ b/install/sql/incremental/upd_0092.sql @@ -0,0 +1,3 @@ +-- add field for enhanced SSL handling +ALTER TABLE `web_domain` ADD `ssl_valid_until` timestamp NULL DEFAULT NULL AFTER `ssl`; +-- end of fixes \ No newline at end of file diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql index 07ca7b8642..d95de9b466 100644 --- a/install/sql/ispconfig3.sql +++ b/install/sql/ispconfig3.sql @@ -2033,6 +2033,7 @@ CREATE TABLE `web_domain` ( `seo_redirect` varchar(255) default NULL, `rewrite_to_https` ENUM('y','n') NOT NULL DEFAULT 'n', `ssl` enum('n','y') NOT NULL default 'n', + `ssl_valid_until` timestamp NULL DEFAULT NULL, `ssl_letsencrypt` enum('n','y') NOT NULL DEFAULT 'n', `ssl_letsencrypt_exclude` enum('n','y') NOT NULL DEFAULT 'n', `ssl_state` varchar(255) NULL, diff --git a/server/lib/app.inc.php b/server/lib/app.inc.php index e0e8c85db2..4a7e0bea65 100644 --- a/server/lib/app.inc.php +++ b/server/lib/app.inc.php @@ -74,7 +74,7 @@ class app { } public function __get($name) { - $valid_names = array('functions', 'getconf', 'letsencrypt', 'modules', 'plugins', 'services', 'system'); + $valid_names = array('functions', 'getconf', 'letsencrypt', 'openssl', 'modules', 'plugins', 'services', 'system'); if(!in_array($name, $valid_names)) { trigger_error('Undefined property ' . $name . ' of class app', E_USER_WARNING); } diff --git a/server/lib/classes/cron.d/900-letsencrypt.inc.php b/server/lib/classes/cron.d/900-letsencrypt.inc.php index b0f6f39c51..762c3d1dbf 100644 --- a/server/lib/classes/cron.d/900-letsencrypt.inc.php +++ b/server/lib/classes/cron.d/900-letsencrypt.inc.php @@ -48,7 +48,10 @@ class cronjob_letsencrypt extends cronjob { public function onRunJob() { global $app, $conf; - + + //TODO: Certbot: If this is a mirror server, do not run + //TODO: Certbot: On mirror server, check for new cert (validity in master-db vs local db) and re-install + $server_config = $app->getconf->get_server_config($conf['server_id'], 'server'); if(!isset($server_config['migration_mode']) || $server_config['migration_mode'] != 'y') { $acme = $app->letsencrypt->get_acme_script(); diff --git a/server/lib/classes/letsencrypt.inc.php b/server/lib/classes/letsencrypt.inc.php index 3923954e10..be554e1a4d 100644 --- a/server/lib/classes/letsencrypt.inc.php +++ b/server/lib/classes/letsencrypt.inc.php @@ -37,12 +37,22 @@ class letsencrypt { */ private $base_path = '/etc/letsencrypt'; private $renew_config_path = '/etc/letsencrypt/renewal'; - private $certbot_use_certcommand = false; + private $COMMAND_TYPE_CHECK = "CHECK"; + private $COMMAND_TYPE_REQUEST = "REQUEST"; + private $COMMAND_TYPE_INSTALL = "INSTALL"; public function __construct(){ } + /** + * acme.sh + * Searches for the acme.sh client scripts in known locations + * Returns false if no acme.sh executable is found + * Returns the path the the acme.sh script if found + * + * @return false|string + */ public function get_acme_script() { $acme = explode("\n", shell_exec('which /usr/local/ispconfig/server/scripts/acme.sh /root/.acme.sh/acme.sh')); $acme = reset($acme); @@ -53,42 +63,60 @@ class letsencrypt { } } - public function get_acme_command($domains, $key_file, $bundle_file, $cert_file, $server_type = 'apache') { + /** + * acme.sh + * Generates the shell commands to be used when acme.sh is the LetsEncrypt client + * + * @param COMMAND_TYPE $command_type One of the ENUMs telling which type of command should be generated + * @param array $domains Array of domains relevant for the certification + * @param string $key_file Path to the certificate key file + * @param string $bundle_file Path to the certificate bundle file + * @param string $cert_file Path to the certificate file + * @param string $server_type apache|nginx + * + * @return false|string + */ + public function get_acme_command($command_type, $domains, $key_file, $bundle_file, $cert_file, $server_type = 'apache') { global $app, $conf; $letsencrypt = $this->get_acme_script(); - $cmd = ''; + $domains_arg = ''; // generate cli format foreach($domains as $domain) { - $cmd .= (string) " -d " . $domain; + $domains_arg .= (string) " -d " . $domain; } - if($cmd == '') { + if($domains_arg == '') { return false; } + $log_arg = "--log " . escapeshellarg($conf['ispconfig_log_dir'].'/acme.log'). ""; + $reload_arg = "--reloadcmd " . escapeshellarg($this->get_reload_command()) . ""; + if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) { $cert_arg = '--fullchain-file ' . escapeshellarg($cert_file); } else { $cert_arg = '--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 || $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'; - - return $cmd; - } - - public function get_certbot_script() { - $letsencrypt = explode("\n", shell_exec('which letsencrypt certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot')); - $letsencrypt = reset($letsencrypt); - if(is_executable($letsencrypt)) { - return $letsencrypt; + if ( $this->COMMAND_TYPE_REQUEST == $command_type) { + return "{$letsencrypt} --issue {$domains_arg} -w /usr/local/ispconfig/interface/acme --always-force-new-domain-key --keylength 4096 {$log_arg}"; + } else if ( $this->COMMAND_TYPE_INSTALL == $command_type) { + return "{$letsencrypt} --install-cert {$domains_arg} --key-file ". escapeshellarg($key_file) ." {$cert_arg} {$reload_arg} {$log_arg}"; } else { - return false; + return ""; } + } + /** + * acme.sh + * + * Installs the acme.sh script locally + * + * @return bool true for successful install, false if failed + */ private function install_acme() { $install_cmd = 'wget -O - https://get.acme.sh | sh'; $ret = null; @@ -98,12 +126,37 @@ class letsencrypt { return ($val == 0 ? true : false); } + /** + * certbot + * + * Searches for the certbot client script in known locations + * Returns false if no certbot executable is found + * Returns the path the the certbot script if found + * + * @return false|string + */ + public function get_certbot_script() { + $letsencrypt = explode("\n", shell_exec('which letsencrypt certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot')); + $letsencrypt = reset($letsencrypt); + if(is_executable($letsencrypt)) { + return $letsencrypt; + } else { + return false; + } + } + + /** + * certbot | acme.sh + * + * Returns the reload command for the used http server + * + * @return string the reload command + */ 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']; @@ -122,52 +175,101 @@ class letsencrypt { return $cmd; } - public function get_certbot_command($domains) { + /** + * certbot + * + * Generates the shell commands to be used when certbot is the LetsEncrypt client + * + * @param COMMAND_TYPE $command_type One of the ENUMs telling which type of command should be generated + * @param array $domains Array of domains relevant for the certification + * + * @return string the shell command (empty in case of errors or missing parameters) + */ + public function get_certbot_command($command_type, $domains) { global $app; $letsencrypt = $this->get_certbot_script(); - $cmd = ''; - // generate cli format + // Map the domain array to a string containing the cli args + $domain_arg = ''; foreach($domains as $domain) { - $cmd .= (string) " --domains " . $domain; + $domain_arg .= (string) " --domains " . $domain; } - if($cmd == '') { - return false; + // Domains are required + if($domain_arg == '') { + return ''; } - $matches = array(); - $ret = null; - $val = 0; + $certbot_can_use_certcommand = false; - $letsencrypt_version = exec($letsencrypt . ' --version 2>&1', $ret, $val); - if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $letsencrypt_version, $matches)) { - $letsencrypt_version = $matches[2]; - } + $letsencrypt_version = $this->get_certbot_version(); if (version_compare($letsencrypt_version, '0.22', '>=')) { $acme_version = 'https://acme-v02.api.letsencrypt.org/directory'; } else { + $app->log("You are using an outdated Let's Encrypt client which is not able to use the acme V02 protocol. Please update! The old acme V01 protocol will be end of life in mid 2021. See https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430", LOGLEVEL_ERROR); $acme_version = 'https://acme-v01.api.letsencrypt.org/directory'; } + // Modern versions of certbot allow us for some more fancy options if (version_compare($letsencrypt_version, '0.30', '>=')) { $app->log("LE version is " . $letsencrypt_version . ", so using certificates command", LOGLEVEL_DEBUG); - $this->certbot_use_certcommand = true; + $certbot_can_use_certcommand = true; $webroot_map = array(); for($i = 0; $i < count($domains); $i++) { $webroot_map[$domains[$i]] = '/usr/local/ispconfig/interface/acme'; } $webroot_args = "--webroot-map " . escapeshellarg(str_replace(array("\r", "\n"), '', json_encode($webroot_map))); + // Domain list is not required with json webroot map, the domains will be implicitly used from the json + $domain_arg = ""; } else { - $webroot_args = "$cmd --webroot-path /usr/local/ispconfig/interface/acme"; + $webroot_args = "--webroot-path /usr/local/ispconfig/interface/acme"; } - $cmd = $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server $acme_version --rsa-key-size 4096 --email postmaster@$domain $cmd --webroot-path /usr/local/ispconfig/interface/acme"; + // Generate the required command based on the $command_type passed in + if ( $this->COMMAND_TYPE_REQUEST == $command_type) { + return $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server $acme_version --rsa-key-size 4096 --email postmaster@$domain $domain_arg $webroot_args"; + } else if ( $this->COMMAND_TYPE_CHECK == $command_type && $certbot_can_use_certcommand) { + return $letsencrypt . " certificates {$domain_arg}"; + } else { + return ''; + } - return $cmd; } - public function get_letsencrypt_certificate_paths($domains = array()) { + /** + * certbot + * + * Returns the certbot version + * + * @return string The certbot version + */ + public function get_certbot_version() { + $letsencrypt = $this->get_certbot_script(); + + $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)) { + return $matches[2]; + } + return $letsencrypt_version; + } + + /** + * certbot + * + * Searches the letsencrypt directory for the best matching existing certificates for all given domains. + * This is done searching and scoring the renewal config file and the containing domains based on a given domain list + * Returns false if none is found + * Returns an array of certificate paths if a matching cert is found + * + * @param array $domains The target domains to find a matching certificate for + * + * @return false|array False if none found. Array of the certificate paths if a matching cert is found + */ + public function find_matching_certificate_on_filesystem($domains = array()) { global $app; if($this->get_acme_script()) { @@ -186,6 +288,7 @@ class letsencrypt { sort($domains); $min_diff = false; + // Iterate over all renewal config files and create a score for each file while($file = readdir($dir)) { if($file === '.' || $file === '..' || substr($file, -5) !== '.conf') continue; $file_path = $this->renew_config_path . '/' . $file; @@ -243,10 +346,12 @@ class letsencrypt { } closedir($dir); + // We didn't find any matching certificate if($min_diff === false) return false; $cert_paths = false; $used_path = false; + // Select the config with the best matching score foreach($path_scores as $path => $data) { if($data['diff'] === $min_diff) { $used_path = $path; @@ -260,6 +365,14 @@ class letsencrypt { return $cert_paths; } + /** + * certbot | acme.sh + * Returns the cleaned SSL Domain. Removes invalid parts and maps empty ssl_domain configs + * + * @param $data + * + * @return string + */ private function get_ssl_domain($data) { global $app; @@ -280,6 +393,15 @@ class letsencrypt { return $domain; } + /** + * certbot | acme.sh + * + * Calculates the Paths, where the certificate files should be found within the webroot + * + * @param $data The website config array + * + * @return array The path array + */ public function get_website_certificate_paths($data) { $ssl_dir = $data['new']['document_root'].'/ssl'; $domain = $this->get_ssl_domain($data); @@ -306,30 +428,46 @@ class letsencrypt { 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'); + /** + * acme.sh + * + * Check if acme.sh is installed and use it prefered + * If neither acme.sh nor certbot are there, install acme.sh + * + * @return bool + */ + public function can_use_acmesh() { + global $app; - $use_acme = false; if($this->get_acme_script()) { - $use_acme = true; + return 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()) { + return true; + } + $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']; + /** + * certbot | acme.sh + * + * Returns an array of all domains required for the vhost including subdomains and alias domains + * + * @param $data The website config array + * + * @return array The domain list for the given website + */ + public function get_domains_for_certificate($data) { + global $app; + $domain = $this->get_ssl_domain($data); // default values $temp_domains = array($domain); - $cli_domain_arg = ''; $subdomains = null; $aliasdomains = null; @@ -358,89 +496,31 @@ class letsencrypt { } // prevent duplicate - $temp_domains = array_unique($temp_domains); - - // check if domains are reachable to avoid letsencrypt verification errors - $le_rnd_file = uniqid('le-') . '.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)); - if($le_hash_check == $le_rnd_hash) { - $le_domains[] = $temp_domain; - $app->log("Verified domain " . $temp_domain . " should be reachable for letsencrypt.", LOGLEVEL_DEBUG); - } else { - $app->log("Could not verify domain " . $temp_domain . ", so excluding it from letsencrypt request.", LOGLEVEL_WARN); - } - } - } - $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); - if($le_domain_count > 100) { - $temp_domains = array_slice($temp_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); - } - - // unset useless data - unset($subdomains); - unset($aliasdomains); - - $this->certbot_use_certcommand = false; - $letsencrypt_cmd = ''; - $allow_return_codes = null; - if($use_acme) { - $letsencrypt_cmd = $this->get_acme_command($temp_domains, $key_file, $bundle_file, $crt_file, $server_type); - $allow_return_codes = array(2); - } else { - $letsencrypt_cmd = $this->get_certbot_command($temp_domains); - } - - $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); - - $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; - } - } + return array_unique($temp_domains); + } - if($use_acme === true) { - if(!$success) { - $app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' could not be issued.', LOGLEVEL_WARN); - $app->log($letsencrypt_cmd, LOGLEVEL_WARN); - return false; - } else { - return true; - } - } + /** + * certbot + * + * Checks whether a certificate already exists and returns it using certbot certificate command + * Returns false if none is found + * Returns an array of the paths to the certificates if found + * + * @param $domains the domain list + * + * @return false|array False if none found. An array of the paths if a certificate is found + */ + public function get_existing_certificate_from_certbot($domains) { + global $app; - $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; - } + // There is no way to check this using acme.sh so always return false + if (!$this->can_use_acmesh()) { + $app->log("LE Certbot - Checking for existing certificates using the 'certificate' command", LOGLEVEL_DEBUG); - $letsencrypt_cmd = $this->get_certbot_script() . " certificates " . $cli_domain_arg; - $output = explode("\n", shell_exec($letsencrypt_cmd . " 2>/dev/null | grep -v '^\$'")); + $output = explode("\n", shell_exec($this->get_certbot_command($this->COMMAND_TYPE_CHECK, $domains) . " 2>/dev/null | grep -v '^\$'")); $le_path = ''; + $le_valid_until = 0; $skip_to_next = true; $matches = null; foreach($output as $outline) { @@ -452,12 +532,21 @@ class letsencrypt { } $skip_to_next = false; + // Check if the certificate is expired ("VALID: EXPIRED"). + // Skip all other checks 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; continue; } + // Get validity information + if(preg_match('/^\s*Expiry Date:\s?(.*)\s?\(VALID:\s+\d+/', $outline)) { + $app->log("Certificate valid until: " . $matches[1], LOGLEVEL_DEBUG); + $le_valid_until = strtotime(trim($matches[1])); + continue; + } + if(preg_match('/^\s*Certificate Path:\s*(\/.*?)\s*$/', $outline, $matches)) { $app->log("Found LE path: " . $matches[1], LOGLEVEL_DEBUG); $le_path = dirname($matches[1]); @@ -470,76 +559,246 @@ class letsencrypt { } if($le_path) { - $le_files = array( + return array( 'privkey' => $le_path . '/privkey.pem', 'chain' => $le_path . '/chain.pem', 'cert' => $le_path . '/cert.pem', - 'fullchain' => $le_path . '/fullchain.pem' + 'fullchain' => $le_path . '/fullchain.pem', + 'valid_until' => $le_valid_until ); } } - if(empty($le_files)) { - $le_files = $this->get_letsencrypt_certificate_paths($temp_domains); + return false; + } + + /** + * certbot + * + * Searches for an existing certbot certificate + * + * @param $domains the domains list for certification + * @return false|array false if none found, else an array with the paths to the certificate + */ + public function get_existing_certificate($domain) { + + $domains = $this->get_domains_for_certificate($domain); + + if($this->can_use_acmesh()) { + // TODO: Use the --install-cert Command of ACME and check the return code (1 = error | 0 = found) + + return false; + } else { + $letsencrypt_version = $this->get_certbot_version(); + + // Use Certbot certificate command + if (version_compare($letsencrypt_version, '0.30', '>=')) { + return $this->get_existing_certificate_from_certbot($domains); + } else { + // On older certbot versions, search on filesystem (legacy fallback) + return $this->find_matching_certificate_on_filesystem($domains); + } } - unset($temp_domains); + } - if($server_type != 'apache' || version_compare($app->system->getapacheversion(true), '2.4.8', '>=')) { - $crt_tmp_file = $le_files['fullchain']; + /** + * certbot | acme.sh + * + * Request a new certificate using one of the available LE backends. + * Returns the domains of the certificate or false in case of failure + * + * @param $data The website config + * @param string $server_type The http server type (apache | nginx) + * + * @return array|false False on failure. Else the array of domains the cert was requested for + */ + public function fetch_certificate_from_le($data, $server_type = 'apache') { + + global $app, $conf; + + $app->log("Trying to fetch new certificate from Letsencrypt", LOGLEVEL_DEBUG); + + $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 = $this->can_use_acmesh(); + + $tmp = $this->get_website_certificate_paths($data); + $domain = $tmp['domain']; + $key_file = $tmp['key']; + $crt_file = $tmp['crt']; + $bundle_file = $tmp['bundle']; + // Create the list of all wanted domains + $temp_domains = $this->get_domains_for_certificate($data); + // Validate all the domains and only return the validated ones + $temp_domains = $this->validate_certificate_domains($temp_domains, $web_config, $server_config); + + // LE only accepts 100 domains per single certificate, so cut overflow off and war about it + $le_domain_count = count($temp_domains); + if($le_domain_count > 100) { + $temp_domains = array_slice($temp_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); + } + + // Prepare the LE backend to use, get the command + $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($this->COMMAND_TYPE_REQUEST, $temp_domains, $key_file, $bundle_file, $crt_file, $server_type); + $allow_return_codes = array(2); } else { - $crt_tmp_file = $le_files['cert']; + $letsencrypt_cmd = $this->get_certbot_command($this->COMMAND_TYPE_REQUEST, $temp_domains); + umask($old_umask); } - $key_tmp_file = $le_files['privkey']; - $bundle_tmp_file = $le_files['chain']; + // Execute the LE Backend call and obtain the certificate + $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: ". implode(" ", $temp_domains), LOGLEVEL_DEBUG); + + $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(!$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); + return false; + } else { + $app->log('Let\'s Encrypt SSL Cert for: ' . $domain . ' successfully issued.', LOGLEVEL_DEBUG); + return $temp_domains; + } - // if cert already exists, dont remove it. Ex. expired/misstyped/noDnsYet alias domain, api down... - if(!file_exists($crt_tmp_file)) { - return false; + } + + /** + * certbot | acme.sh + * + * Checks whether a list of domains can be reached from the outside to prevalidate the Let's Encrypt requests + * + * @param $domains_to_validate List of domains to validate + * + * @return array The list of validated domains which can be accessed from the outside + */ + private function validate_certificate_domains($domains_to_validate, $web_config, $server_config) { + global $app; + // check if domains are reachable to avoid letsencrypt verification errors + $le_rnd_file = uniqid('le-') . '.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($domains_to_validate 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)); + if($le_hash_check == $le_rnd_hash) { + $le_domains[] = $temp_domain; + $app->log("Verified domain " . $temp_domain . " should be reachable for letsencrypt.", LOGLEVEL_DEBUG); + } else { + $app->log("Could not verify domain " . $temp_domain . ", so excluding it from letsencrypt request.", LOGLEVEL_WARN); + } } } + @unlink('/usr/local/ispconfig/interface/acme/.well-known/acme-challenge/' . $le_rnd_file); - //* 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"); + return $le_domains; + } - //* 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); - } + /** + * certbot | acme.sh + * + * Setup all required links, files and stuff on the server + * + * @param $data + * @param $domains + * @param string $server_type + * + * @return false | array False on setup failure. Else an array with the validity of the new certificate + */ + public function setup_certificate($data, $domains, $server_type = 'apache') { + global $app; - 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); + // Target paths + $tmp = $this->get_website_certificate_paths($data); + $domain = $tmp['domain']; + $key_file = $tmp['key']; + $crt_file = $tmp['crt']; + $bundle_file = $tmp['bundle']; - 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); - } + if ($this->can_use_acmesh()) { + // Install the certificate using acme.sh client + $cmd = $this->get_acme_command($this->COMMAND_TYPE_INSTALL, $domains, $key_file, $bundle_file, $crt_file, $server_type ); + $app->system->exec_safe($cmd); + // Return the validity of the setup certificate + return $app->openssl->get_cert_validity($crt_file); + } else { + // Certbot requires manually created symlinks - 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); + // Get the existing certificates + $le_files = $this->get_existing_certificate($domains); + if (!$le_files) { + return false; + } - 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); + // Chose the required files from let's encrypt to setup + // Apache requires different certificate types (cert only VS full chain) based on the Apache version + 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']; } + $key_tmp_file = $le_files['privkey']; + $bundle_tmp_file = $le_files['chain']; + + //* check is been correctly created + if(file_exists($crt_tmp_file)) { + $app->log("Let's Encrypt Cert file: $crt_tmp_file exists.", LOGLEVEL_DEBUG); + + // 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'); + $app->system->chmod($key_file.'.old', 0400); + $app->system->unlink($key_file); + } - 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); + 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); - return true; - } else { - $app->log("Let's Encrypt Cert file: $crt_tmp_file does not exist.", LOGLEVEL_DEBUG); - return false; + if(is_file($crt_file)) { + $app->system->copy($crt_file, $crt_file.'.old'); + $app->system->chmod($crt_file.'.old', 0400); + $app->system->unlink($crt_file); + } + + 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'); + $app->system->chmod($bundle_file.'.old', 0400); + $app->system->unlink($bundle_file); + } + + 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); + + // All done, return validity of the setup cert file + return $app->openssl->get_cert_validity($crt_file); + } else { + $app->log("Let's Encrypt Cert file: $crt_tmp_file does not exist.", LOGLEVEL_DEBUG); + return false; + } } } + } diff --git a/server/lib/classes/openssl.inc.php b/server/lib/classes/openssl.inc.php new file mode 100644 index 0000000000..6e9ddda989 --- /dev/null +++ b/server/lib/classes/openssl.inc.php @@ -0,0 +1,78 @@ +system->exec_safe("{$this->openssl_command} x509 -dates -noout -in ?", $crt_file) . " 2>/dev/null | grep -v '^\$'"); + $matches = null; + $validity = array(); + foreach($output as $outline) { + $outline = trim( $outline ); + $app->log( "CERT Validity output: " . $outline, LOGLEVEL_DEBUG ); + + if ( preg_match( '/^notBefore=(.*)$/', $outline, $matches ) ) { + $app->log( "Certificate valid from: " . $matches[1], LOGLEVEL_DEBUG ); + $validity['from'] = strtotime( $matches[1] ); + continue; + } + + if ( preg_match( '/^notAfter=(.*)$/', $outline, $matches ) ) { + $app->log( "Certificate valid until: " . $matches[1], LOGLEVEL_DEBUG ); + $validity['until'] = strtotime( $matches[1] ); + continue; + } + } + + return $validity; + + } + +} diff --git a/server/plugins-available/nginx_plugin.inc.php b/server/plugins-available/nginx_plugin.inc.php index 1fd2e536da..15418efb48 100644 --- a/server/plugins-available/nginx_plugin.inc.php +++ b/server/plugins-available/nginx_plugin.inc.php @@ -100,6 +100,8 @@ class nginx_plugin { // load the server configuration options $app->uses('getconf'); + // load openssl functions + $app->uses('openssl'); $web_config = $app->getconf->get_server_config($conf['server_id'], 'web'); if ($web_config['CA_path']!='' && !file_exists($web_config['CA_path'].'/openssl.cnf')) $app->log("CA path error, file does not exist:".$web_config['CA_path'].'/openssl.cnf', LOGLEVEL_ERROR); @@ -179,21 +181,24 @@ class nginx_plugin { $ssl_ext_file = $ssl_dir.'/v3.ext'; $app->system->file_put_contents($ssl_ext_file, $ext_cnf); - $rand_file = $rand_file; - $key_file2 = $key_file2; $openssl_cmd_key_file2 = $key_file2; - if(substr($domain, 0, 2) == '*.' && strpos($key_file2, '/ssl/\*.') !== false) $key_file2 = str_replace('/ssl/\*.', '/ssl/*.', $key_file2); // wildcard certificate - $key_file = $key_file; + if (substr($domain, 0, 2) == '*.' && strpos($key_file2, '/ssl/\*.') !== false) { + $key_file2 = str_replace('/ssl/\*.', '/ssl/*.', $key_file2); // wildcard certificate + } $openssl_cmd_key_file = $key_file; - if(substr($domain, 0, 2) == '*.' && strpos($key_file, '/ssl/\*.') !== false) $key_file = str_replace('/ssl/\*.', '/ssl/*.', $key_file); // wildcard certificate + if (substr($domain, 0, 2) == '*.' && strpos($key_file, '/ssl/\*.') !== false) { + $key_file = str_replace('/ssl/\*.', '/ssl/*.', $key_file); // wildcard certificate + } $ssl_days = 3650; - $csr_file = $csr_file; $openssl_cmd_csr_file = $csr_file; - if(substr($domain, 0, 2) == '*.' && strpos($csr_file, '/ssl/\*.') !== false) $csr_file = str_replace('/ssl/\*.', '/ssl/*.', $csr_file); // wildcard certificate + if (substr($domain, 0, 2) == '*.' && strpos($csr_file, '/ssl/\*.') !== false) { + $csr_file = str_replace('/ssl/\*.', '/ssl/*.', $csr_file); // wildcard certificate + } $config_file = $ssl_cnf_file; - $crt_file = $crt_file; $openssl_cmd_crt_file = $crt_file; - if(substr($domain, 0, 2) == '*.' && strpos($crt_file, '/ssl/\*.') !== false) $crt_file = str_replace('/ssl/\*.', '/ssl/*.', $crt_file); // wildcard certificate + if (substr($domain, 0, 2) == '*.' && strpos($crt_file, '/ssl/\*.') !== false) { + $crt_file = str_replace('/ssl/\*.', '/ssl/*.', $crt_file); // wildcard certificate + } if(is_file($ssl_cnf_file) && !is_link($ssl_cnf_file)) { @@ -206,11 +211,11 @@ class nginx_plugin { $app->system->exec_safe("openssl ca -batch -out ? -config ? -passin pass:? -in ? -extfile ?", $openssl_cmd_crt_file, $web_config['CA_path']."/openssl.cnf", $web_config['CA_pass'], $openssl_cmd_csr_file, $ssl_ext_file); $app->log("Creating CA-signed SSL Cert for: $domain", LOGLEVEL_DEBUG); if (filesize($crt_file)==0 || !file_exists($crt_file)) $app->log("CA-Certificate signing failed. openssl ca -out $openssl_cmd_crt_file -config ".$web_config['CA_path']."/openssl.cnf -passin pass:".$web_config['CA_pass']." -in $openssl_cmd_csr_file -extfile $ssl_ext_file", LOGLEVEL_ERROR); - }; + } if (@filesize($crt_file)==0 || !file_exists($crt_file)){ $app->system->exec_safe("openssl req -x509 -passin pass:? -passout pass:? -key ? -in ? -out ? -days ? -config ?", $ssl_password, $ssl_password, $openssl_cmd_key_file2, $openssl_cmd_csr_file, $openssl_cmd_crt_file, $ssl_days, $config_file); $app->log("Creating self-signed SSL Cert for: $domain", LOGLEVEL_DEBUG); - }; + } } @@ -222,12 +227,18 @@ class nginx_plugin { $ssl_request = $app->system->file_get_contents($csr_file); $ssl_cert = $app->system->file_get_contents($crt_file); $ssl_key = $app->system->file_get_contents($key_file); + $ssl_cert_validity = $app->openssl->get_cert_validity($crt_file); /* Update the DB of the (local) Server */ $app->db->query("UPDATE web_domain SET ssl_request = ?, ssl_cert = ?, ssl_key = ? WHERE domain = ?", $ssl_request, $ssl_cert, $ssl_key, $data['new']['domain']); $app->db->query("UPDATE web_domain SET ssl_action = '' WHERE domain = ?", $data['new']['domain']); /* Update also the master-DB of the Server-Farm */ $app->dbmaster->query("UPDATE web_domain SET ssl_request = ?, ssl_cert = ?, ssl_key = ? WHERE domain = ?", $ssl_request, $ssl_cert, $ssl_key, $data['new']['domain']); $app->dbmaster->query("UPDATE web_domain SET ssl_action = '' WHERE domain = ?", $data['new']['domain']); + /* Update the validity of the certificate */ + if (!empty($ssl_cert_validity) && $ssl_cert_validity['until'] != -1) { + $app->db->query("UPDATE web_domain SET ssl_valid_until = FROM_UNIXTIME(?) WHERE domain = ?", $ssl_cert_validity['until'], $data['new']['domain']); + $app->dbmaster->query("UPDATE web_domain SET ssl_valid_until = FROM_UNIXTIME(?) WHERE domain = ?", $ssl_cert_validity['until'], $data['new']['domain']); + } } //* Check that the SSL key is not password protected @@ -1348,7 +1359,7 @@ class nginx_plugin { $trans = array( '{DOCROOT}' => $vhost_data['web_document_root_www'], '{DOCROOT_CLIENT}' => $vhost_data['web_document_root'], - '{DOMAIN}' => $vhost_data['domain'], + '{DOMAIN}' => $vhost_data['domain'], '{FASTCGIPASS}' => 'fastcgi_pass '.($data['new']['php_fpm_use_socket'] == 'y'? 'unix:'.$fpm_socket : '127.0.0.1:'.$vhost_data['fpm_port']).';' ); foreach($nginx_directive_lines as $nginx_directive_line){ @@ -1375,30 +1386,47 @@ class nginx_plugin { $vhost_data['ssl_bundle_file'] = $bundle_file; //* Generate Let's Encrypt SSL certificat - if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y' && $conf['mirror_server_id'] == 0 && ( // ssl and let's encrypt is active and no mirror server + if($data['new']['ssl'] == 'y' && $data['new']['ssl_letsencrypt'] == 'y' && ( // ssl and let's encrypt is active ($data['old']['ssl'] == 'n' || $data['old']['ssl_letsencrypt'] == 'n') // we have new let's encrypt configuration || ($data['old']['domain'] != $data['new']['domain']) // we have domain update || ($data['old']['subdomain'] != $data['new']['subdomain']) // we have new or update on "auto" subdomain || $this->update_letsencrypt == true )) { - $success = $app->letsencrypt->request_certificates($data, 'nginx'); - if($success) { - /* we don't need to store it. - /* Update the DB of the (local) Server */ - $app->db->query("UPDATE web_domain SET ssl_request = '', ssl_cert = '', ssl_key = '' WHERE domain = ?", $data['new']['domain']); - $app->db->query("UPDATE web_domain SET ssl_action = '' WHERE domain = ?", $data['new']['domain']); - /* Update also the master-DB of the Server-Farm */ - $app->dbmaster->query("UPDATE web_domain SET ssl_request = '', ssl_cert = '', ssl_key = '' WHERE domain = ?", $data['new']['domain']); - $app->dbmaster->query("UPDATE web_domain SET ssl_action = '' WHERE domain = ?", $data['new']['domain']); - } else { - $data['new']['ssl_letsencrypt'] = 'n'; - if($data['old']['ssl'] == 'n') $data['new']['ssl'] = 'n'; - /* Update the DB of the (local) Server */ - $app->db->query("UPDATE web_domain SET `ssl` = ?, `ssl_letsencrypt` = ? WHERE `domain` = ? AND `server_id` = ?", $data['new']['ssl'], 'n', $data['new']['domain'], $conf['server_id']); - /* Update also the master-DB of the Server-Farm */ - $app->dbmaster->query("UPDATE web_domain SET `ssl` = ?, `ssl_letsencrypt` = ? WHERE `domain` = ?", $data['new']['ssl'], 'n', $data['new']['domain']); + $found_or_got_cert = $app->letsencrypt->get_existing_certificate($data['new']['ssl_domain']); + + // On mirror servers no certs should be requested. They should be only setup. We assume that the required folder (/etc/letsencrypt/ or /root/.acme.sh/) are live network shares + if ($conf['mirror_server_id'] == 0) { // Request certs only on the mirrored server + + // Didn't find an existing cert, fetch from LE + if(false === $found_or_got_cert) { + $found_or_got_cert = $app->letsencrypt->fetch_certificate_from_le( $data, 'nginx' ); + } + + if(false !== $found_or_got_cert) { + $cert_valid_until = $found_or_got_cert['until']; + /* we don't need to store it. + /* Update the DB of the (local) Server */ + $app->db->query("UPDATE web_domain SET ssl_request = '', ssl_cert = '', ssl_key = '', ssl_valid_until = ? WHERE domain = ?", $cert_valid_until, $data['new']['domain']); + $app->db->query("UPDATE web_domain SET ssl_action = '' WHERE domain = ?", $data['new']['domain']); + /* Update also the master-DB of the Server-Farm */ + $app->dbmaster->query("UPDATE web_domain SET ssl_request = '', ssl_cert = '', ssl_key = '', ssl_valid_until = ? WHERE domain = ?", $cert_valid_until, $data['new']['domain']); + $app->dbmaster->query("UPDATE web_domain SET ssl_action = '' WHERE domain = ?", $data['new']['domain']); + } else { + $data['new']['ssl_letsencrypt'] = 'n'; + if($data['old']['ssl'] == 'n') $data['new']['ssl'] = 'n'; + /* Update the DB of the (local) Server */ + $app->db->query("UPDATE web_domain SET `ssl` = ?, `ssl_letsencrypt` = ? WHERE `domain` = ? AND `server_id` = ?", $data['new']['ssl'], 'n', $data['new']['domain'], $conf['server_id']); + /* Update also the master-DB of the Server-Farm */ + $app->dbmaster->query("UPDATE web_domain SET `ssl` = ?, `ssl_letsencrypt` = ? WHERE `domain` = ?", $data['new']['ssl'], 'n', $data['new']['domain']); + } + } + + if (false !== $found_or_got_cert) { + //TODO: Configure certificates + } + } if($domain!='' && $data['new']['ssl'] == 'y' && @is_file($crt_file) && @is_file($key_file) && (@filesize($crt_file)>0) && (@filesize($key_file)>0)) { -- GitLab From e341c1efe01cd5c8963f949c53e3f7805e3d116e Mon Sep 17 00:00:00 2001 From: Jan Thiel Date: Mon, 15 Feb 2021 12:08:42 +0100 Subject: [PATCH 2/4] Add required SQL adjustments to DEV collection --- install/sql/incremental/upd_0093.sql | 14 -------------- install/sql/incremental/upd_dev_collection.sql | 4 ++++ 2 files changed, 4 insertions(+), 14 deletions(-) delete mode 100644 install/sql/incremental/upd_0093.sql diff --git a/install/sql/incremental/upd_0093.sql b/install/sql/incremental/upd_0093.sql deleted file mode 100644 index 2008b93c30..0000000000 --- a/install/sql/incremental/upd_0093.sql +++ /dev/null @@ -1,14 +0,0 @@ -<<<<<<< HEAD --- add field for enhanced SSL handling -ALTER TABLE `web_domain` ADD `ssl_valid_until` timestamp NULL DEFAULT NULL AFTER `ssl`; --- end of fixes -======= --- drop old php column because new installations don't have them (fails in multi-server) -ALTER TABLE `web_domain` DROP COLUMN `fastcgi_php_version`; - --- add php_fpm_socket_dir column to server_php -ALTER TABLE `server_php` ADD `php_fpm_socket_dir` varchar(255) DEFAULT NULL AFTER `php_fpm_pool_dir`; - --- fix #5939 -UPDATE `ftp_user` SET `expires` = NULL WHERE `expires` = '0000-00-00 00:00:00'; ->>>>>>> develop diff --git a/install/sql/incremental/upd_dev_collection.sql b/install/sql/incremental/upd_dev_collection.sql index 7d1ec43815..81b6c155fb 100644 --- a/install/sql/incremental/upd_dev_collection.sql +++ b/install/sql/incremental/upd_dev_collection.sql @@ -19,3 +19,7 @@ DROP TABLE 'software_update_inst'; -- Brexit UPDATE `country` SET `eu` = 'n' WHERE `iso` = 'GB'; + +-- add field for enhanced SSL handling +ALTER TABLE `web_domain` ADD `ssl_valid_until` timestamp NULL DEFAULT NULL AFTER `ssl`; +-- end of fixes \ No newline at end of file -- GitLab From d6824aff49c3e7eb11039bd5d829dcb4e534c466 Mon Sep 17 00:00:00 2001 From: Jan Thiel Date: Mon, 15 Feb 2021 15:28:51 +0100 Subject: [PATCH 3/4] Add --cert-name option to certbot calls to set primary domain --- server/lib/classes/letsencrypt.inc.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/lib/classes/letsencrypt.inc.php b/server/lib/classes/letsencrypt.inc.php index be554e1a4d..2f36efcfb6 100644 --- a/server/lib/classes/letsencrypt.inc.php +++ b/server/lib/classes/letsencrypt.inc.php @@ -201,9 +201,11 @@ class letsencrypt { return ''; } + $primary_domain = $domains[0]; $certbot_can_use_certcommand = false; $letsencrypt_version = $this->get_certbot_version(); + $app->log("LE version is " . $letsencrypt_version, LOGLEVEL_DEBUG); if (version_compare($letsencrypt_version, '0.22', '>=')) { $acme_version = 'https://acme-v02.api.letsencrypt.org/directory'; } else { @@ -212,7 +214,7 @@ class letsencrypt { } // Modern versions of certbot allow us for some more fancy options if (version_compare($letsencrypt_version, '0.30', '>=')) { - $app->log("LE version is " . $letsencrypt_version . ", so using certificates command", LOGLEVEL_DEBUG); + $app->log("using certificates command and --webroot-map", LOGLEVEL_DEBUG); $certbot_can_use_certcommand = true; $webroot_map = array(); for($i = 0; $i < count($domains); $i++) { @@ -227,7 +229,7 @@ class letsencrypt { // Generate the required command based on the $command_type passed in if ( $this->COMMAND_TYPE_REQUEST == $command_type) { - return $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server $acme_version --rsa-key-size 4096 --email postmaster@$domain $domain_arg $webroot_args"; + return $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server {$acme_version} --rsa-key-size 4096 --email postmaster@{$primary_domain} --cert-name {$primary_domain} {$webroot_args} {$domain_arg}"; } else if ( $this->COMMAND_TYPE_CHECK == $command_type && $certbot_can_use_certcommand) { return $letsencrypt . " certificates {$domain_arg}"; } else { -- GitLab From 54e933f9b09f2a1bcf592c7255275980b95469d5 Mon Sep 17 00:00:00 2001 From: Jan Thiel Date: Mon, 15 Feb 2021 16:27:14 +0100 Subject: [PATCH 4/4] Use backward compatible --cert-name implementation --- server/lib/classes/letsencrypt.inc.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/lib/classes/letsencrypt.inc.php b/server/lib/classes/letsencrypt.inc.php index 2f36efcfb6..1893a0524d 100644 --- a/server/lib/classes/letsencrypt.inc.php +++ b/server/lib/classes/letsencrypt.inc.php @@ -223,13 +223,15 @@ class letsencrypt { $webroot_args = "--webroot-map " . escapeshellarg(str_replace(array("\r", "\n"), '', json_encode($webroot_map))); // Domain list is not required with json webroot map, the domains will be implicitly used from the json $domain_arg = ""; + $cert_selection_command = "--cert-name $primary_domain"; } else { $webroot_args = "--webroot-path /usr/local/ispconfig/interface/acme"; + $cert_selection_command = "--expand"; } // Generate the required command based on the $command_type passed in if ( $this->COMMAND_TYPE_REQUEST == $command_type) { - return $letsencrypt . " certonly -n --text --agree-tos --expand --authenticator webroot --server {$acme_version} --rsa-key-size 4096 --email postmaster@{$primary_domain} --cert-name {$primary_domain} {$webroot_args} {$domain_arg}"; + return $letsencrypt . " certonly -n --text --agree-tos {$cert_selection_command} --authenticator webroot --server {$acme_version} --rsa-key-size 4096 --email postmaster@{$primary_domain} {$webroot_args} {$domain_arg}"; } else if ( $this->COMMAND_TYPE_CHECK == $command_type && $certbot_can_use_certcommand) { return $letsencrypt . " certificates {$domain_arg}"; } else { -- GitLab