Use certbot standalone to create Lets Encrypt SSL certs for ISPConfig Servers

Discussion in 'Developers' Forum' started by ahrasis, Oct 30, 2018.

  1. ahrasis

    ahrasis Active Member

    I submitted a WIP (work in progress) in the git at https://git.ispconfig.org/ispconfig/ispconfig3/merge_requests/834 for the update of installer_base.lib.php file to use certbot standalone in creating Lets Encrypt SSL certs for ISPConfig server(s). This is based on my LE4ISPC script.

    Basically the code are changed and/or inserted as follow:
    Code:
        public function make_ispconfig_ssl_cert() {
           global $conf, $autoinstall;
        
           // This hostname can be taken from user entry too
           // But I don't find a way for it yet so...
           // We use this for now ;D
           $hostname = exec('hostname -f');
    
           // Check if LE SSL folder for the hostname existed
           $le_live_dir = '/etc/letsencrypt/live/' . $hostname;
    
           // Check if this is web server
           $check_nginx = exec("dpkg-query -W -f='\${Status}' nginx 2>/dev/null | grep -c 'ok installed'");
           $check_apache = exec("dpkg-query -W -f='\${Status}' apache2 2>/dev/null | grep -c 'ok installed'");
    
           // We support certbot so create standalone LE SSL certs for this server
           if ([email protected]_dir($le_live_dir)) {
               // If it is nginx webserver
               if ($check_nginx == 1)
                   exec("certbot certonly --authenticator standalone -d $hostname --pre-hook 'service nginx stop' --post-hook 'service nginx start'");
               // If it is apache2 webserver
               elseif ($check_apache2 == 1)
                   exec("certbot certonly --authenticator standalone -d $hostname --pre-hook 'service apache2 stop' --post-hook 'service apache2 start'");
               // If it is not webserver
               else
                   exec("certbot certonly --authenticator standalone -d $hostname");
           }
        
           // If the LE SSL certs for this hostname exists
           if (is_dir($le_live_dir)) {
    
               // Define and check ISPConfig SSL folder
               $install_dir = $conf['ispconfig_install_dir'];
               if([email protected]_dir($install_dir.'/interface/ssl')) mkdir($install_dir.'/interface/ssl', 0755, true);
               $ssl_crt_file = $install_dir.'/interface/ssl/ispserver.crt';
               $ssl_csr_file = $install_dir.'/interface/ssl/ispserver.csr';
               $ssl_key_file = $install_dir.'/interface/ssl/ispserver.key';
               $ssl_pem_file = $install_dir.'/interface/ssl/ispserver.pem';
               $ssl_bak_file = $install_dir.'/interface/ssl/ispserver.*.bak';
            
               // Delete old then backup existing ispserver ssl files
               if (is_file($ssl_bak_file)) exec("rm $ssl_bak_file");
               if (is_file($ssl_crt_file)) exec("mv $ispccrt $ssl_crt_file-$(date +'%y%m%d%H%M%S).bak");
               if (is_file($ssl_key_file)) exec("mv $ispccrt $ssl_key_file-$(date +'%y%m%d%H%M%S).bak");
               if (is_file($ssl_pem_file)) exec("mv $ispccrt $ssl_pem_file-$(date +'%y%m%d%H%M%S).bak");
            
               // Create symlink to LE fullchain and key for ISPConfig
               exec("ln -s $le_live_dir/fullchain.pem $ssl_crt_file");
               exec("ln -s $le_live_dir/privkey.pem $ssl_key_file");
    
               // Build ispserver.pem file and chmod it
               exec("cat $ssl_key_file $ssl_crt_file > $ssl_pem_file");
               exec("chmod 600 $ssl_pem_file");
           }
    /*
           $ssl_pw = substr(md5(mt_rand()), 0, 6);
           exec("openssl genrsa -des3 -passout pass:$ssl_pw -out $ssl_key_file 4096");
           if(AUTOINSTALL){
               exec("openssl req -new -passin pass:$ssl_pw -passout pass:$ssl_pw -subj '/C=".escapeshellcmd($autoinstall['ssl_cert_country'])."/ST=".escapeshellcmd($autoinstall['ssl_cert_state'])."/L=".escapeshellcmd($autoinstall['ssl_cert_locality'])."/O=".escapeshellcmd($autoinstall['ssl_cert_organisation'])."/OU=".escapeshellcmd($autoinstall['ssl_cert_organisation_unit'])."/CN=".escapeshellcmd($autoinstall['ssl_cert_common_name'])."' -key $ssl_key_file -out $ssl_csr_file");
           } else {
               exec("openssl req -new -passin pass:$ssl_pw -passout pass:$ssl_pw -key $ssl_key_file -out $ssl_csr_file");
           }
           exec("openssl req -x509 -passin pass:$ssl_pw -passout pass:$ssl_pw -key $ssl_key_file -in $ssl_csr_file -out $ssl_crt_file -days 3650");
           exec("openssl rsa -passin pass:$ssl_pw -in $ssl_key_file -out $ssl_key_file.insecure");
           rename($ssl_key_file, $ssl_key_file.'.secure');
           rename($ssl_key_file.'.insecure', $ssl_key_file);
    */
           exec("chown -R root:root $install_dir/interface/ssl");
    
       }
    
    Comments and feedbacks are welcome. Thank you in advance.
     
    Last edited: Oct 30, 2018
    Jesse Norell and till like this.
  2. till

    till Super Moderator Staff Member ISPConfig Developer

    I guess you will have to offer a self-signed cert as an automatic fallback on LE failure, in the installer as LE will fail in many cases. For example:

    LE will fail when:

    - The hostname is no FQDN or not correctly configured yet.
    - The hostname has not been set up in DNS yet.
    - The hostname is a local / private name as the server is used in an intranet.
    - the hostname is a subdomain of the domain of a large hoster (this setup is used by most root server providers and LE will fail as the subdomain limit for the provider domain is exceeded).

    And in regard to standalone mode, does certbot saves the settings in a way that renewal will work later automatically? Having stopped apache/nginx on a production system on each renewal is not see god, but we might find a solution for that by using the same global alias that LE certs in ispconfig websites are using.
     
    ahrasis likes this.
  3. Jesse Norell

    Jesse Norell Well-Known Member

    That is of course needed for an initial installation, but not when upgrading ispconfig if there is an existing certificate. It wouldn't hurt to always check for an existing certificate; if not found (eg. new install) prompt to generate a self-signed or attempt letsencrypt setup; if found, ask if it should be updated and default to "no" (if "yes" is chosen, again ask the "letsencrypt vs self-signed" question).

    In other comments, it would be good to support numerous hostnames in the server's main certificate. I implemented that by having a config file (I'm using /usr/local/ispconfig/server/conf-custom/letsencrypt-domains.txt) list hostnames, one per line, and request the certificate with those names. It is pretty simple, and would be easy for the installer to prompt for a list of hostnames and create such a file if it didn't exist, and also easy for the gui to maintain such a file under server config; you could use the standard "conf" for automatically maintained and "conf-custom" for manual.

    I have the server setup to monitor this file and automatically update the server's certificate when it changes - it makes for less support calls for ssl certificate errors when migrating domains (ie. you can add mail.client.com to that file as you migrate the domain, then those clients get no certificate errors). Here's my current working script as an example; it does use webroot, so wouldn't work for the initial install; also it uses --cert-name which isn't available in older certbot installations; but it has the sed bit tested and working. :)

    Code:
    #!/bin/bash
    
    # letsencrypt-request-server-certificate.sh:
    # requests a letsencrypt certificate including
    # this server's fqdn and a list of additional domains
    
    domains_file=/usr/local/ispconfig/server/conf-custom/letsencrypt-domains.txt
    fqdn="$(hostname -f)"
    email="[email protected]$(hostname -d)"
    
    certbot auth --text --agree-tos --non-interactive \
            --cert-name "${fqdn}" --allow-subset-of-names --expand --keep-until-expiring --renew-with-new-domains \
            --webroot --webroot-path /usr/local/ispconfig/interface/acme/ \
            --email "${email}" -d "${fqdn}" \
            $( sed 's/#.*$//;s/^[ \t]*//;s/[ \t]*$//' "${domains_file}" | grep -Ev '^\s*$' | xargs -i{} echo -d "{}" )
    
     
    ahrasis likes this.
  4. Jesse Norell

    Jesse Norell Well-Known Member

    I suppose including the server fqdn should be optional, for some of the reasons @till posted.
     
  5. ahrasis

    ahrasis Active Member

    Agreed. I will adjust accordingly.
    Yes. In the hostname fqdn renewal conf.
    Agreed. If renewal is on time, this will happen once in 2 months which also means 6 times per year for an ISPConfig web server.
    It is designed to check for it and will back it up, if one existed but will delete any old backup before proceeding. I think I will remove this deletion part later. To note, the default currently there is no check implemented for the self-signed certs.
    I will prefer this to happen before running LE or self-signed request, so there will be choice for those who did not setup his hostname fqdn properly too, as per the catch listed by @till above.
    I like the idea but most probably will stick to make the basic idea to work first. We surely can expand once we finished this.
    Good stuff. I like it. Perhaps we can still try to use certbot webroot if we provide port 80 and 443 in ispconfig.vhost via its master in only ISPConfig web server? What do you think @till?
     
    Last edited: Oct 31, 2018
  6. ahrasis

    ahrasis Active Member

    I modify further as follows:
    Code:
        public function make_ispconfig_ssl_cert() {
           global $conf, $autoinstall;
    
           // Get hostname from user entry or shell command
           if($conf['hostname'] !== ('localhost' || '') ) $hostname = $conf['hostname'];
           else $hostname = exec('hostname -f');
          
           // Check dns a record exist and its ip equal to server public ip
           $svr_ip = file_get_contents('http://dynamicdns.park-your-domain.com/getip');
           if (checkdnsrr(idn_to_ascii($hostname), 'A')) {
               $dns_A=dns_get_record($hostname, DNS_A); $dns_ip=$dns_A[0][ip];
           }
    
           // Check if LE SSL folder for the hostname existed
           // Then create standalone LE SSL certs for this server
           $le_live_dir = '/etc/letsencrypt/live/' . $hostname;
           if ([email protected]_dir($le_live_dir) && ($svr_ip = $dns_ip)) {
    
               // If it is nginx webserver
               if($conf['nginx']['installed'] == true)
                   exec("certbot certonly --authenticator standalone -d $hostname --pre-hook 'service nginx stop' --post-hook 'service nginx start'");
    
               // If it is apache2 webserver
               elseif($conf['apache']['installed'] == true)
                   exec("certbot certonly --authenticator standalone -d $hostname --pre-hook 'service apache2 stop' --post-hook 'service apache2 start'");
    
               // If it is not webserver
               else
                   exec("certbot certonly --authenticator standalone -d $hostname");
           }
           // TODO: To implement webroot instead of standalone via
           // probably ispconfig vhost to provide for port 80 && 443
    
           // Define and check ISPConfig SSL folder
           $install_dir = $conf['ispconfig_install_dir'];
    
           $ssl_crt_file = $install_dir.'/interface/ssl/ispserver.crt';
           $ssl_csr_file = $install_dir.'/interface/ssl/ispserver.csr';
           $ssl_key_file = $install_dir.'/interface/ssl/ispserver.key';
           $ssl_pem_file = $install_dir.'/interface/ssl/ispserver.pem';
    
           if([email protected]_dir($install_dir.'/interface/ssl')) mkdir($install_dir.'/interface/ssl', 0755, true);
          
           // If the LE SSL certs for this hostname exists
           if (is_dir($le_live_dir) && ($svr_ip = $dns_ip)) {
              
               // Backup existing ispserver ssl files
               $date = new DateTime();
               if (file_exists($ssl_crt_file)) rename($ssl_crt_file, $ssl_crt_file . '-' .$date->format('YmdHis') . '.bak');
               if (file_exists($ssl_crt_file)) rename($ssl_key_file, $ssl_key_file . '-' .$date->format('YmdHis') . '.bak');
               if (file_exists($ssl_crt_file)) rename($ssl_pem_file, $ssl_pem_file . '-' .$date->format('YmdHis') . '.bak');
              
               // Create symlink to LE fullchain and key for ISPConfig
               symlink($le_live_dir.'/fullchain.pem', $ssl_crt_file);
               symlink($le_live_dir.'/privkey.pem', $ssl_key_file);
    
               // Build ispserver.pem file and chmod it
               exec("cat $ssl_key_file $ssl_crt_file > $ssl_pem_file; chmod 600 $ssl_pem_file");
           } else {
          
               // We can still use the old self-signed method
               $ssl_pw = substr(md5(mt_rand()), 0, 6);
               exec("openssl genrsa -des3 -passout pass:$ssl_pw -out $ssl_key_file 4096");
               if(AUTOINSTALL){
                   exec("openssl req -new -passin pass:$ssl_pw -passout pass:$ssl_pw -subj '/C=".escapeshellcmd($autoinstall['ssl_cert_country'])."/ST=".escapeshellcmd($autoinstall['ssl_cert_state'])."/L=".escapeshellcmd($autoinstall['ssl_cert_locality'])."/O=".escapeshellcmd($autoinstall['ssl_cert_organisation'])."/OU=".escapeshellcmd($autoinstall['ssl_cert_organisation_unit'])."/CN=".escapeshellcmd($autoinstall['ssl_cert_common_name'])."' -key $ssl_key_file -out $ssl_csr_file");
               } else {
                   exec("openssl req -new -passin pass:$ssl_pw -passout pass:$ssl_pw -key $ssl_key_file -out $ssl_csr_file");
               }
               exec("openssl req -x509 -passin pass:$ssl_pw -passout pass:$ssl_pw -key $ssl_key_file -in $ssl_csr_file -out $ssl_crt_file -days 3650");
               exec("openssl rsa -passin pass:$ssl_pw -in $ssl_key_file -out $ssl_key_file.insecure");
               rename($ssl_key_file, $ssl_key_file.'.secure');
               rename($ssl_key_file.'.insecure', $ssl_key_file);
           }
          
           exec("chown -R root:root $install_dir/interface/ssl");
    
       }
    
    Further comments and feedbacks are appreciated. Thank you in advance.
     
    Last edited: Oct 31, 2018
    till likes this.
  7. till

    till Super Moderator Staff Member ISPConfig Developer

    Looks good and I guess we can go for the standalone mode for now.

    For later: I guess we might be able to use webroot method and avoid using standalone mode during install when the ispconfig server has apache or nginx installed by temporarily configuring apache or nginx before we request the LE cert.

    For apache, we might just have to add a file /etc/apache2/sites-available/ispconfig.conf which contains:

    Code:
    Alias /.well-known/acme-challenge /usr/local/ispconfig/interface/acme/.well-known/acme-challenge
    <Directory /usr/local/ispconfig/interface/acme/.well-known/acme-challenge>
        <tmpl_if name='apache_version' op='>' value='2.2' format='version'>
        Require all granted
        <tmpl_else>
            Order allow,deny
            Allow from all
        </tmpl_if>
        <IfModule mpm_itk_module>
           AssignUserId www-data www-data
        </IfModule>
    </Directory>
    (variables and if's replaced of course), then enable the conf file in sites-enabled and reload apache. The file will get replaced in a later step of the installer by the 'complete' ispconfig.conf, so this should work without further changes in the ispconfig part.

    For nginx: We can check where requests will end up without configuration (default vhost, probably something like /var/www/html/) and then create a folder '.well-known' there and inside that folder a symlink 'acme-challenge' that points to /usr/local/ispconfig/interface/acme/.well-known/acme-challenge (we probably have to create /usr/local/ispconfig/interface/acme/.well-known/acme-challenge as full path too at that time as it normally gets created at a later stage during ispconfig install). This means that we can use the path /usr/local/ispconfig/interface/acme/.well-known/acme-challenge for webroot method at this early stage of installation already.

    The nginx method might work for apache as well, maybe it's even easier than what I first thought for apache.

    These are just some thoughts, we do not have to implement it like this. Feel free to comment what your thoughts on that matter are.
     
    ahrasis likes this.
  8. ahrasis

    ahrasis Active Member

    I just write but haven't tested the code on actual install yet. I will properly PR when it works on my test / actual install.
    I will have to prepare an apache server to test this as well. My idea is to add *:80 and *:443 in ispconfig.vhost but I will surely test all to determine which one works. If both works, we choose one that is better.
    My idea is the same as mentioned above i.e. adding to the top of ispconfig.vhost. Something like:
    Code:
    server {
            listen *:80;
            listen *:443 {ssl_on};
            {ssl_comment}ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
            {ssl_comment}ssl_certificate /usr/local/ispconfig/interface/ssl/ispserver.crt;
            {ssl_comment}ssl_certificate_key /usr/local/ispconfig/interface/ssl/ispserver.key;
            {ssl_comment}ssl_ciphers
    'TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
    
            server_name _;
    
            location ^~ /.well-known/acme-challenge/ {
                    access_log off;
                    log_not_found off;
                    root /usr/local/ispconfig/interface/acme/;
                    autoindex off;
                    index index.html;
                    try_files $uri $uri/ =404;
            }
    }
    
     
  9. till

    till Super Moderator Staff Member ISPConfig Developer

    That's not a good idea in my opinion, neither for apache nor nginx, as it will break the default vhost on all systems. So basically you break the default vhost functionality of all installed ispconfig servers by doing this.
     
    ahrasis likes this.
  10. Jesse Norell

    Jesse Norell Well-Known Member

    So this works with servers having multiple A records, loop through $dns_A and compare to that. Something like:
    Code:
    $dns_ips = array();
    foreach ($dns_A as $rec) {
        $dns_ips[] = $rec['ip'];
    }
    ...
    if ([email protected]_dir($le_live_dir) && in_array($srv_ip, $dns_ips)) {
    ...
    
    I suppose it should handle the server hostname being CNAME record and AAAA records as well.
     
    ahrasis likes this.
  11. ahrasis

    ahrasis Active Member

    I have tested the code and it is working as expected with the exception to the suggestion by @Jesse Norell above which I added later without further testing because I think it should be working just fine. You can proceed with the merge to master if everything is in order @till.
     
  12. ahrasis

    ahrasis Active Member

    I extended this to the other services, if the services is detected during the installation or update, the user shall have the option to extend it. The following covers postfix and pureftpd:
    Code:
               // Build ispserver.pem file and chmod it
               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 LE SSL certs to postfix?', array('y', 'n'), 'y')) == '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';
                   
                   // 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');
                   
                   // Create symlink to ISPConfig SSL files
                   symlink($ssl_crt_file, $smtpd_crt);
                   symlink($ssl_key_file, $smtpd_key);
               }
               
               // Extend LE SSL certs to pureftpd
               if ($conf['pureftpd']['installed'] == true && strtolower($this->simple_query('Symlink ISPConfig LE SSL certs to pureftpd? Creating dhparam file takes some times.', array('y', 'n'), 'y')) == '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';
                   
                   // Backup existing postfix ssl files
                   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"))
                       exec("cd $pureftpd_dir; openssl dhparam -out dhparam4096.pem 4096; ln -sf dhparam4096.pem pure-ftpd-dhparams.pem");
               }
    
     
    till likes this.
  13. Aeon

    Aeon New Member

    This works for Multiserver too? When no Webserver is Setup on the mailserver node?
     
    ahrasis likes this.
  14. ahrasis

    ahrasis Active Member

    Yes. The modifications intend to secure any type of ISPConfig servers which includes its standalone mail server.
     
  15. Jesse Norell

    Jesse Norell Well-Known Member

    Correct me if I'm wrong, but securing a mail server with the current setup would require port 80 to be open to the mail server, so certbot (in standalone mode) can handle the acme challenge and issue the certificate. Using DNS based auth for a mail server would be a nice addition later on, but is not in the initial implementation?
     
  16. ahrasis

    ahrasis Active Member

    Yes because it is still http challenge via certbot itself but no web server is needed to be installed.

    I don't think using dns challenge at this stage is wise or easy to implement but anybody can propose a framework on how it could work for the benefit of all.
     
  17. till

    till Super Moderator Staff Member ISPConfig Developer

    I agree that DNS challenge is probably too complicated for that install stage. But maybe we find a good way to implement it in future or make it an option for expert mode.
     
    ahrasis likes this.
  18. ahrasis

    ahrasis Active Member

    I drafted this to support webroot with mutli domain support as suggested by @till and @Jesse Norell above. Obviously, the multi domain code will only be executed during install or update that is if /usr/local/ispconfig/server/conf-custom/letsencrypt_domains.master exist.

    I also added email and key size 4096 but will have to see what other files that are needed to be created or amended or symlink to make this work:
    Code:
        public function make_ispconfig_ssl_cert() {
           global $conf, $autoinstall;
    
           //* Get hostname from user entry or shell command */
           if($conf['hostname'] !== ('localhost' || '')) $hostname = $conf['hostname'];
           else $hostname = exec('hostname -f');
    
           // Check dns a record exist and its ip equal to server public ip
           $svr_ip = file_get_contents('http://dynamicdns.park-your-domain.com/getip');
           if (checkdnsrr(idn_to_ascii($hostname, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46), 'A')) {
               $dnsa=dns_get_record($hostname, DNS_A);
               $dns_ips = array();
               foreach ($dnsa as $rec) {
                   $dns_ips[] = $rec['ip'];
               }
           }
    
           // Request for certs if no LE SSL folder for server fqdn exist
           $le_live_dir = '/etc/letsencrypt/live/' . $hostname;
           if ([email protected]_dir($le_live_dir) && in_array($svr_ip, $dns_ips)) {
    
               /* This won't work on non web server as it requires working vhost / conf for the added domains
               // Try to support for multi domain, if it is defined in letsencrypt_domains.master
               $domain_file = '/usr/local/ispconfig/server/conf-custom/letsencrypt_domains.master';
               $cli_domain_arg = '';
    
               // If file exist, get the unique domains but not more then 99
               if (file_exists($domain_file)) {
                   $extra_domains = file($domain_file, FILE_IGNORE_NEW_LINES);
                   $extra_domains = array_unique($extra_domains);
                   $le_domain_count = count($extra_domains);
                   if($le_domain_count > 99) {
                       $extra_domains = array_slice($extra_domains, 0, 99);
                       echo "\nExtra domains exceed limits. Only the first 99 will be expanded into the hostname FQDN cert.\n";
                   }
                   foreach($extra_domains as $le_domain) $cli_domain_arg .= (string) ' -d ' . $le_domain;
               } */
    
               // Get the default LE client name and version
               $le_client = explode("\n", shell_exec('which letsencrypt certbot /root/.local/share/letsencrypt/bin/letsencrypt /opt/eff.org/certbot/venv/bin/certbot'));
               $le_client = reset($le_client);
               $le_info = exec($le_client . ' --version  2>&1', $ret, $val);
               if(preg_match('/^(\S+|\w+)\s+(\d+(\.\d+)+)$/', $le_info, $matches)) { $le_name = $matches[1]; $le_version = $matches[2]; }
               $acme_version = '--server https://acme-v0' . (($le_version >=0.22) ? '2' : '1') . '.api.letsencrypt.org/directory';
               $certonly = 'certonly --agree-tos --non-interactive --expand --rsa-key-size 4096';
               $webroot = '--authenticator webroot --webroot-path /var/www/html';
               $standalone = '--authenticator standalone';
    
               // Only certbot is supported to prevent unknown failures
               if($le_name == 'certbot' && is_executable($le_client)) {
                   // If this is a webserver, we use webroot
                   if(($conf['nginx']['installed'] || $conf['apache']['installed']) == true) {
                       $well_known = '/var/www/html/.well-known';
                       $challenge = "$well_known/acme_challenge";
                       $acme_challenge = '/usr/local/ispconfig/interface/acme/.well-known/acme-challenge';
                       if (!is_dir($well_known)) mkdir($well_known, 0755, true);
                       if (!is_dir($challenge)) exec("ln -sf $acme_challenge $challenge");
                       exec("$le_client $certonly $acme_version $webroot --email [email protected]$hostname -d $hostname"); // $cli_domain_arg");
                   }
                   // Else, it is not webserver, so we use standalone
                   else
                       exec("$le_client $certonly $acme_version $standalone --email [email protected]$hostname -d $hostname"); // $cli_domain_arg");
               }
           }
    
           //* Define and check ISPConfig SSL folder */
           $install_dir = $conf['ispconfig_install_dir'];
    
           $ssl_crt_file = $install_dir.'/interface/ssl/ispserver.crt';
           $ssl_csr_file = $install_dir.'/interface/ssl/ispserver.csr';
           $ssl_key_file = $install_dir.'/interface/ssl/ispserver.key';
           $ssl_pem_file = $install_dir.'/interface/ssl/ispserver.pem';
    
           if([email protected]_dir($install_dir.'/interface/ssl')) mkdir($install_dir.'/interface/ssl', 0755, true);
    
           $date = new DateTime();
    
           // If the LE SSL certs for this hostname exists
           if (is_dir($le_live_dir) && in_array($svr_ip, $dns_ips)) {
    
               // Backup existing ispserver ssl files
               if (file_exists($ssl_crt_file)) rename($ssl_crt_file, $ssl_crt_file . '-' .$date->format('YmdHis') . '.bak');
               if (file_exists($ssl_crt_file)) rename($ssl_key_file, $ssl_key_file . '-' .$date->format('YmdHis') . '.bak');
               if (file_exists($ssl_crt_file)) rename($ssl_pem_file, $ssl_pem_file . '-' .$date->format('YmdHis') . '.bak');
    
               // Create symlink to LE fullchain and key for ISPConfig
               symlink($le_live_dir.'/fullchain.pem', $ssl_crt_file);
               symlink($le_live_dir.'/privkey.pem', $ssl_key_file);
    
               // Build ispserver.pem file and chmod it
               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 LE SSL certs to postfix?', array('y', 'n'), 'y')) == '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';
    
                   // 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');
    
                   // Create symlink to ISPConfig SSL files
                   symlink($ssl_crt_file, $smtpd_crt);
                   symlink($ssl_key_file, $smtpd_key);
               }
    
               // Extend LE SSL certs to pureftpd
               if ($conf['pureftpd']['installed'] == true && strtolower($this->simple_query('Symlink ISPConfig LE SSL certs to pureftpd? Creating dhparam file takes some times.', array('y', 'n'), 'y')) == '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';
    
                   // Backup existing postfix ssl files
                   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"))
                       exec("cd $pureftpd_dir; openssl dhparam -out dhparam4096.pem 4096; ln -sf dhparam4096.pem pure-ftpd-dhparams.pem");
               }
    
           } else {
    
               // We can still use the old self-signed method
               $ssl_pw = substr(md5(mt_rand()), 0, 6);
               exec("openssl genrsa -des3 -passout pass:$ssl_pw -out $ssl_key_file 4096");
               if(AUTOINSTALL){
                   exec("openssl req -new -passin pass:$ssl_pw -passout pass:$ssl_pw -subj '/C=".escapeshellcmd($autoinstall['ssl_cert_country'])."/ST=".escapeshellcmd($autoinstall['ssl_cert_state'])."/L=".escapeshellcmd($autoinstall['ssl_cert_locality'])."/O=".escapeshellcmd($autoinstall['ssl_cert_organisation'])."/OU=".escapeshellcmd($autoinstall['ssl_cert_organisation_unit'])."/CN=".escapeshellcmd($autoinstall['ssl_cert_common_name'])."' -key $ssl_key_file -out $ssl_csr_file");
               } else {
                   exec("openssl req -new -passin pass:$ssl_pw -passout pass:$ssl_pw -key $ssl_key_file -out $ssl_csr_file");
               }
               exec("openssl req -x509 -passin pass:$ssl_pw -passout pass:$ssl_pw -key $ssl_key_file -in $ssl_csr_file -out $ssl_crt_file -days 3650");
               exec("openssl rsa -passin pass:$ssl_pw -in $ssl_key_file -out $ssl_key_file.insecure");
               rename($ssl_key_file, $ssl_key_file.'.secure');
               rename($ssl_key_file.'.insecure', $ssl_key_file);
           }
    
           exec("chown -R root:root $install_dir/interface/ssl");
    
       }
    
    I will test the code as a whole later but I appreciate if anyone can give further comments and feedbacks. Thank you in advance.
     
    Last edited: Nov 2, 2018
    till likes this.
  19. ahrasis

    ahrasis Active Member

    I may have find a proper way to use certbot dns challenge for this but since is quite complexed, I think it is best to add support in ISPConfig control panel first and then extend it to ISPConfing installation, if needed.

    Therefore I will leave the PR related to this thread for the developers to check and merge it.

    I will continue writing the code to use dns challenge for ISPConfig bind server which involves modifying the database, adding TSIG key via dns setting page, allowing users to use it remotely.

    Yes, despite my doubt before TSIG key is actually needed in the database since multi server setup may require remote access to dns server to request for LE SSL certs.

    Passing LE validation token using php directly into ISPConfig database without creating TSIG key is another option but am not sure how to handle multiple requests and/or multiple domains.

    I think I better discuss this later part in another thread and PR since this has different modifications and features.
     
    till likes this.
  20. till

    till Super Moderator Staff Member ISPConfig Developer

    I think we should try to find a way to write the validation tokes trough ispconfig database or api (at least on the long run, or support both ways) as using TSIG key would require to establish an additional connection between the servers (from web server to dns server) which might be another possible point of failure.

    I'll try to review and test your other code this week and then merge it into master branch. Thank you for all the efforts and time that you invest here!
     
    ahrasis likes this.

Share This Page