diff --git a/.gitignore b/.gitignore index 14e6c4a79..6d7e391b9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ tools/__pycache__/ externals/ .env .vagrant -api/docs/api-docs.html \ No newline at end of file +api/docs/api-docs.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00e15ec7e..d3f33274e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,9 +4,10 @@ Mail-in-a-Box is an open source project. Your contributions and pull requests ar ## Development -To start developing Mail-in-a-Box, [clone the repository](https://github.com/mail-in-a-box/mailinabox) and familiarize yourself with the code. +To start developing Mail-in-a-Box, [clone the repository](https://github.com/mail-in-a-box/mailinabox) and familiarize yourself with the code. Then move to the cloned mailinabox directory. $ git clone https://github.com/mail-in-a-box/mailinabox + $ cd mailinabox ### Vagrant and VirtualBox @@ -15,9 +16,9 @@ We recommend you use [Vagrant](https://www.vagrantup.com/intro/getting-started/i With Vagrant set up, the following should boot up Mail-in-a-Box inside a virtual machine: $ vagrant up --provision - _If you're seeing an error message about your *IP address being listed in the Spamhaus Block List*, simply uncomment the `export SKIP_NETWORK_CHECKS=1` line in `Vagrantfile`. It's normal, you're probably using a dynamic IP address assigned by your Internet provider–they're almost all listed._ + ### Modifying your `hosts` file After a while, Mail-in-a-Box will be available at `192.168.50.4` (unless you changed that in your `Vagrantfile`). To be able to use the web-based bits, we recommend to add a hostname to your `hosts` file: diff --git a/management/auth.py b/management/auth.py index fd143c760..b97ea057a 100644 --- a/management/auth.py +++ b/management/auth.py @@ -6,7 +6,7 @@ from mailconfig import get_mail_password, get_mail_user_privileges from mfa import get_hash_mfa_state, validate_auth_mfa -DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key' +DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key' DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server' class KeyAuthService: diff --git a/management/daemon.py b/management/daemon.py index ffc6d5d5a..9aa4f36f6 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -378,6 +378,87 @@ def ssl_get_csr(domain): ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem')) return create_csr(domain, ssl_private_key, request.form.get('countrycode', ''), env) + +def return_message(item): + if item['result'] == 'skipped': + return { + "title": item["result"].capitalize(), + "log": "\n".join(item['log']), + } + elif item['result'] == 'installed': + return { + "title": item["result"].capitalize(), + "log": "Your certificate containing these domains " + ",".join( + item['domains']) + " have been renewed", + } + else: + return { + "title": item["result"].capitalize(), + "log": "\n".join(item['log']) + } + + +@app.route('/ssl/renew/<domain>', methods=['POST']) +@authorized_personnel_only +def ssl_renew(domain): + from utils import load_environment + from ssl_certificates import provision_certificates + existing_key = request.form.get('existing_key') + env = load_environment() + if existing_key == "yes": + status = provision_certificates(env, limit_domains=[], domain_to_be_renewed=domain) + app.logger.warning("renew without new key=", status) # TODO: remove this line after testing + elif existing_key == "no": + import glob + try: + # steps followed + # 1. take a backup of the current /home/user-data/ssl/ folder to be safe + # 2. renew all the existing certificates from CSR generated from the existing next_ssl_private_key + # 3. if the renew is successful, replace the current ssl_private_key with the next_ssl_private_key and + # 4. generate the next_ssl_private_key + # 5. if any error occurs, copy everything from the /home/user-data/ssl-backup folder to /home/user-data/ssl + + # step 1 + files = glob.glob(env["STORAGE_ROOT"] + "/ssl/*") + for file in files: + subprocess.check_output(["cp", "-r", file, env["STORAGE_ROOT"] + "/ssl-backup/"]) + + # step 2 + status = provision_certificates(env, limit_domains=[], new_key=True) + + # step 3 and 4 is in post_install_func method of ssl_certificates.py + app.logger.warning("renew with new key=", status) # TODO: remove this line after proper testing + except Exception as e: + import traceback + files = glob.glob(env["STORAGE_ROOT"] + "/ssl-backup/*") + for file in files: + subprocess.check_output(["cp", "-r", file, env["STORAGE_ROOT"] + "/ssl/"]) + app.logger.warning(traceback.print_exc()) # TODO: remove this line after proper testing + return json_response({ + "title": "Error", + "log": "Sorry, something is not right!", + }) + else: + return json_response({ + "title": "Error", + "log": "Sorry, something is not right!", + }) + + ret_message = {"title": "", "log": ""} + for item in status: + if isinstance(item, str): + continue + elif existing_key == "no": + message = return_message(item) + ret_message["title"] = message["title"] + ret_message["log"] += "\n" + message["log"] + elif existing_key == "yes" and domain in item["domains"]: + return json_response(return_message(item)) + return json_response(ret_message) + + + + @app.route('/ssl/install', methods=['POST']) @authorized_personnel_only def ssl_install_cert(): diff --git a/management/dns_update.py b/management/dns_update.py index 748f87f15..7b3b766ae 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -174,9 +174,11 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en # Add a DANE TLSA record for SMTP. records.append(("_25._tcp", "TLSA", build_tlsa_record(env), "Recommended when DNSSEC is enabled. Advertises to mail servers connecting to the box that mandatory encryption should be used.")) + records.append(("_25._tcp", "TLSA", build_tlsa_record(env, from_cert=False), "Recommended when DNSSEC is enabled. Advertises to mail servers connecting to the box that mandatory encryption should be used.")) # Add a DANE TLSA record for HTTPS, which some browser extensions might make use of. records.append(("_443._tcp", "TLSA", build_tlsa_record(env), "Optional. When DNSSEC is enabled, provides out-of-band HTTPS certificate validation for a few web clients that support it.")) + records.append(("_443._tcp", "TLSA", build_tlsa_record(env, from_cert=False), "Optional. When DNSSEC is enabled, provides out-of-band HTTPS certificate validation for a few web clients that support it.")) # Add a SSHFP records to help SSH key validation. One per available SSH key on this system. for value in build_sshfp_records(): @@ -367,7 +369,7 @@ def has_rec(qname, rtype, prefix=None): ######################################################################## -def build_tlsa_record(env): +def build_tlsa_record(env, from_cert=True): # A DANE TLSA record in DNS specifies that connections on a port # must use TLS and the certificate must match a particular criteria. # @@ -390,11 +392,16 @@ def build_tlsa_record(env): from ssl_certificates import load_cert_chain, load_pem from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat - fn = os.path.join(env["STORAGE_ROOT"], "ssl", "ssl_certificate.pem") - cert = load_pem(load_cert_chain(fn)[0]) - - subject_public_key = cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) - # We could have also loaded ssl_private_key.pem and called priv_key.public_key().public_bytes(...) + if from_cert: + fn = os.path.join(env["STORAGE_ROOT"], "ssl", "ssl_certificate.pem") + cert = load_pem(load_cert_chain(fn)[0]) + subject_public_key = cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + else: + # this is for Double TLSA scheme of key rollover. + # More details here (https://mail.sys4.de/pipermail/dane-users/2018-February/000440.html) + fn = os.path.join(env["STORAGE_ROOT"], "ssl", "next_ssl_private_key.pem") + private_key = load_pem(open(fn, 'rb').read()) + subject_public_key = private_key.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) pk_hash = hashlib.sha256(subject_public_key).hexdigest() @@ -862,6 +869,8 @@ def set_custom_dns_record(qname, rtype, value, action, env): if not re.search(DOMAIN_RE, value): raise ValueError("Invalid value.") + elif rtype == "TLSA": + pass elif rtype in ("CNAME", "TXT", "SRV", "MX", "SSHFP", "CAA"): # anything goes pass diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 3e1b58564..85ad5e244 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -45,7 +45,7 @@ def get_file_list(): # Remember stuff. private_keys = { } - certificates = [ ] + certificates = [] # Scan each of the files to find private keys and certificates. # We must load all of the private keys first before processing @@ -73,6 +73,7 @@ def get_file_list(): domains = { } for cert in certificates: # What domains is this certificate good for? + # cert_domains = cert common name + all SANs, primary_domain = cert common name cert_domains, primary_domain = get_certificate_domains(cert) cert._primary_domain = primary_domain @@ -186,8 +187,16 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True from web_update import get_web_domains from status_checks import query_dns, normalize_ip + # existing_certs = all valid certificates that are in /home/user-data/ssl or 1 level deep + # validity indicator -> private key exists for the cert public key?, not_expired? + # if multiple valid certs exist, then the one with the furthest expiry date and filename + # lexicographically smallest is returned existing_certs = get_ssl_certificates(env) + # this function returns the list of domain names of all the email addresses and adds + # autoconfig, www, mta-sts, autodiscover subdomain to each of these domains + # if exclude_dns_elsewhere flag is set, then all the domains having A/AAAA record not on this machine + # are excluded plausible_web_domains = get_web_domains(env, exclude_dns_elsewhere=False) actual_web_domains = get_web_domains(env) @@ -212,7 +221,8 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True # how Let's Encrypt will connect. bad_dns = [] for rtype, value in [("A", env["PUBLIC_IP"]), ("AAAA", env.get("PUBLIC_IPV6"))]: - if not value: continue # IPv6 is not configured + if not value: + continue # IPv6 is not configured response = query_dns(domain, rtype) if response != normalize_ip(value): bad_dns.append("%s (%s)" % (response, rtype)) @@ -226,6 +236,7 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True # DNS is all good. # Check for a good existing cert. + # existing_cert = existing cert for domain existing_cert = get_domain_ssl_files(domain, existing_certs, env, use_main_cert=False, allow_missing_cert=True) if existing_cert: existing_cert_check = check_certificate(domain, existing_cert['certificate'], existing_cert['private-key'], @@ -242,19 +253,30 @@ def get_certificates_to_provision(env, limit_domains=None, show_valid_certs=True return (domains_to_provision, domains_cant_provision) -def provision_certificates(env, limit_domains): +def provision_certificates(env, limit_domains, domain_to_be_renewed=None, new_key=False): # What domains should we provision certificates for? And what # errors prevent provisioning for other domains. - domains, domains_cant_provision = get_certificates_to_provision(env, limit_domains=limit_domains) - - # Build a list of what happened on each domain or domain-set. ret = [] - for domain, error in domains_cant_provision.items(): - ret.append({ - "domains": [domain], - "log": [error], - "result": "skipped", - }) + is_tlsa_update_required = False + if new_key: + from web_update import get_web_domains + domains = get_web_domains(env) + elif domain_to_be_renewed: + existing_certs = get_ssl_certificates(env) + existing_cert = get_domain_ssl_files(domain_to_be_renewed, existing_certs, env, use_main_cert=False, allow_missing_cert=True) + domains, primary_domain = get_certificate_domains(load_pem(load_cert_chain(existing_cert["certificate"])[0])) + else: + # domains = domains for which a certificate can be provisioned + # domains_cant_provision = domains for which a certificate can't be provisioned and the reason + domains, domains_cant_provision = get_certificates_to_provision(env, limit_domains=limit_domains) + + # Build a list of what happened on each domain or domain-set. + for domain, error in domains_cant_provision.items(): + ret.append({ + "domains": [domain], + "log": [error], + "result": "skipped", + }) # Break into groups by DNS zone: Group every domain with its parent domain, if # its parent domain is in the list of domains to request a certificate for. @@ -309,6 +331,8 @@ def provision_certificates(env, limit_domains): # Create a CSR file for our master private key so that certbot # uses our private key. key_file = os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem') + if new_key: + key_file = os.path.join(env['STORAGE_ROOT'], 'ssl', 'next_ssl_private_key.pem') with tempfile.NamedTemporaryFile() as csr_file: # We could use openssl, but certbot requires # that the CN domain and SAN domains match @@ -345,9 +369,9 @@ def provision_certificates(env, limit_domains): "certbot", "certonly", #"-v", # just enough to see ACME errors - "--non-interactive", # will fail if user hasn't registered during Mail-in-a-Box setup + "--non-interactive", # will fail if user hasn't registered during Mail-in-a-Box setup - "-d", ",".join(domain_list), # first will be main domain + "-d", ",".join(domain_list), # first will be main domain "--csr", csr_file.name, # use our private key; unfortunately this doesn't work with auto-renew so we need to save cert manually "--cert-path", os.path.join(d, 'cert'), # we only use the full chain @@ -363,6 +387,8 @@ def provision_certificates(env, limit_domains): ret[-1]["log"].append(certbotret) ret[-1]["result"] = "installed" + if new_key and env['PRIMARY_HOSTNAME'] in domains: + is_tlsa_update_required = True except subprocess.CalledProcessError as e: ret[-1]["log"].append(e.output.decode("utf8")) ret[-1]["result"] = "error" @@ -371,7 +397,7 @@ def provision_certificates(env, limit_domains): ret[-1]["result"] = "error" # Run post-install steps. - ret.extend(post_install_func(env)) + ret.extend(post_install_func(env, is_tlsa_update_required=is_tlsa_update_required)) # Return what happened with each certificate request. return ret @@ -466,7 +492,7 @@ def install_cert_copy_file(fn, env): shutil.move(fn, ssl_certificate) -def post_install_func(env): +def post_install_func(env, is_tlsa_update_required=False): ret = [] # Get the certificate to use for PRIMARY_HOSTNAME. @@ -496,6 +522,25 @@ def post_install_func(env): # The DANE TLSA record will remain valid so long as the private key # hasn't changed. We don't ever change the private key automatically. # If the user does it, they must manually update DNS. + if is_tlsa_update_required: + from dns_update import do_dns_update, set_custom_dns_record, build_tlsa_record + subprocess.check_output([ + "mv", env["STORAGE_ROOT"] + "/ssl/next_ssl_private_key.pem", + env["STORAGE_ROOT"] + "/ssl/ssl_private_key.pem" + ]) + subprocess.check_output([ + "openssl", "genrsa", + "-out", env["STORAGE_ROOT"] + "/ssl/next_ssl_private_key.pem", + "2048"]) + qname1 = "_25._tcp." + env['PRIMARY_HOSTNAME'] + qname2 = "_443._tcp." + env['PRIMARY_HOSTNAME'] + rtype = "TLSA" + value = build_tlsa_record(env, from_cert=False) + action = "add" + if set_custom_dns_record(qname1, rtype, value, action, env): + set_custom_dns_record(qname2, rtype, value, action, env) + ret.append(do_dns_update(env)) + # Update the web configuration so nginx picks up the new certificate file. from web_update import do_web_update diff --git a/management/status_checks.py b/management/status_checks.py index 36da034a1..7582a2e9d 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -454,9 +454,9 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): # Check the TLSA record. tlsa_qname = "_25._tcp." + domain - tlsa25 = query_dns(tlsa_qname, "TLSA", nxdomain=None) + tlsa25 = query_dns(tlsa_qname, "TLSA", nxdomain=None).split('; ') tlsa25_expected = build_tlsa_record(env) - if tlsa25 == tlsa25_expected: + if tlsa25_expected in tlsa25: output.print_ok("""The DANE TLSA record for incoming mail is correct (%s).""" % tlsa_qname,) elif tlsa25 is None: if has_dnssec: diff --git a/management/templates/index.html b/management/templates/index.html index 12f6ad8e0..6cd05e7e0 100644 --- a/management/templates/index.html +++ b/management/templates/index.html @@ -216,17 +216,17 @@ <h4 class="modal-title" id="errorModalTitle"> </h4> // so that we don't attempt to show another modal while this one // is closing. global_modal_state = 0; // OK - }) + }); $('#global_modal .btn-default').click(function() { global_modal_state = 1; // Cancel - }) + }); $('#global_modal').on('hidden.bs.modal', function (e) { // do the cancel function if (global_modal_state == null) global_modal_state = 1; // cancel if the user hit ESC or clicked outside of the modal if (global_modal_funcs && global_modal_funcs[global_modal_state]) global_modal_funcs[global_modal_state](); }) -}) +}); function show_modal_error(title, message, callback) { $('#global_modal h4').text(title); @@ -246,7 +246,7 @@ <h4 class="modal-title" id="errorModalTitle"> </h4> return false; // handy when called from onclick } -function show_modal_confirm(title, question, verb, yes_callback, cancel_callback) { +function show_modal_confirm(title, question, verb, yes_callback, cancel_callback, extra_callback=null) { $('#global_modal h4').text(title); if (typeof question == 'string') { $('#global_modal .modal-dialog').addClass("modal-sm"); @@ -263,7 +263,8 @@ <h4 class="modal-title" id="errorModalTitle"> </h4> $('#global_modal .btn-default').show().text(verb[1]); $('#global_modal .btn-danger').show().text(verb[0]); } - global_modal_funcs = [yes_callback, cancel_callback]; + if (extra_callback) global_modal_funcs = [yes_callback, cancel_callback, extra_callback]; + else global_modal_funcs = [yes_callback, cancel_callback]; global_modal_state = null; $('#global_modal').modal({}); return false; // handy when called from onclick diff --git a/management/templates/ssl.html b/management/templates/ssl.html index a6b913eed..d3db8edd9 100644 --- a/management/templates/ssl.html +++ b/management/templates/ssl.html @@ -40,7 +40,10 @@ <h3>Certificate status</h3> <h3 id="ssl_install_header">Install certificate</h3> -<p>If you don't want to use our automatic Let's Encrypt integration, you can give any other certificate provider a try. You can generate the needed CSR below.</p> +<p>If you don't want to use our automatic Let's Encrypt integration, you can give any other certificate provider a try. Click on install certificate button + if there is no certificate for your intended domain or + click on renew or replace certificate button and click replace if there is an existing certificate and you want to replace it with a new one from a different CA. + You can generate the needed CSR below.</p> <p>Which domain are you getting a certificate for?</p> @@ -101,7 +104,8 @@ <h3 id="ssl_install_header">Install certificate</h3> $('#ssldomain').html('<option value="">(select)</option>'); $('#ssl_domains').show(); for (var i = 0; i < domains.length; i++) { - var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>"); + var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> " + + "<td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>"); tb.append(row); row.attr('data-domain', domains[i].domain); row.find('.domain a').text(domains[i].domain); @@ -113,7 +117,10 @@ <h3 id="ssl_install_header">Install certificate</h3> row.addClass("text-" + domains[i].status); row.find('.status').text(domains[i].text); if (domains[i].status == "success") { - row.find('.actions a').addClass('btn-default').text('Replace Certificate'); + row.find('.actions a').addClass('btn-default').text('Renew or replace Certificate'); + row.find('.actions a').addClass('btn-default').on("click", function () { + ssl_renew_or_replace_modal(this); + }); } else { row.find('.actions a').addClass('btn-primary').text('Install Certificate'); } @@ -130,6 +137,87 @@ <h3 id="ssl_install_header">Install certificate</h3> $('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 }) return false; } +let flag = false; + +function ssl_renew_or_replace_modal(elem) { + if (!flag) { + $('#global_modal .modal-footer').append('<button type="button" class="btn btn-warning" data-dismiss="modal">Renew</button>'); + flag = true; + } + $('#global_modal .btn-warning').click(function() { + global_modal_state = 2; // Renew + }); + show_modal_confirm( + "Options", + "Do you want to replace the certificate with a new one or just renew this one?", + "Replace", + function () { + $('#global_modal .modal-footer .btn-warning').remove(); + flag = false; + ssl_install(elem); + }, function() {$('#global_modal .modal-footer .btn-warning').remove();flag = false;}, + function () { + $('#global_modal .modal-footer .btn-warning').remove(); + flag = false; + ssl_cert_renew(elem); + }); +} +function ssl_cert_renew(elem) { + if (!flag) { + $('#global_modal .modal-footer').append('<button type="button" class="btn btn-warning" data-dismiss="modal">Yes</button>'); + flag = true; + } + $('#global_modal .btn-warning').click(function() { + global_modal_state = 2; // Renew + }); + var domain = $(elem).parents('tr').attr('data-domain'); + show_modal_confirm( + "Renewing options", + "Do you want to renew with the existing key?", + "No", + function () { + $('#global_modal .modal-footer .btn-warning').remove();flag = false; + $('#ajax_loading_indicator').show(); + api( + "/ssl/renew/" + domain, + "POST", + { + existing_key: "no" + }, + function(data) { + $('#ajax_loading_indicator').stop(true).hide(); + show_modal_error(data["title"], data["log"]); + show_tls(true); + }, + function () { + $('#ajax_loading_indicator').stop(true).hide(); + show_modal_error("Error", "Something is not right, sorry!"); + show_tls(true); + } + ); + }, function() {$('#global_modal .modal-footer .btn-warning').remove();flag = false;}, + function () { + $('#global_modal .modal-footer .btn-warning').remove(); + flag = false; + $('#ajax_loading_indicator').show(); + api( + "/ssl/renew/" + domain, + "POST", + { + existing_key: "yes" + }, + function(data) { + $('#ajax_loading_indicator').stop(true).hide(); + show_modal_error(data["title"], data["log"]); + show_tls(true); + }, + function () { + $('#ajax_loading_indicator').stop(true).hide(); + show_modal_error("Error", "Something is not right, sorry!"); + show_tls(true); + }); + },); +} function show_csr() { // Can't show a CSR until both inputs are entered. diff --git a/setup/migrate.py b/setup/migrate.py index c52ac967b..28517b90c 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -208,7 +208,7 @@ def run_migrations(): migration_id = None if os.path.exists(migration_id_file): with open(migration_id_file) as f: - migration_id = f.read().strip(); + migration_id = f.read().strip() if migration_id is None: # Load the legacy location of the migration ID. We'll drop support diff --git a/setup/ssl.sh b/setup/ssl.sh index 61b0b9e5c..ef07c2741 100755 --- a/setup/ssl.sh +++ b/setup/ssl.sh @@ -19,7 +19,7 @@ # # The Diffie-Hellman cipher bits are used for SMTP and HTTPS, when a # Diffie-Hellman cipher is selected during TLS negotiation. Diffie-Hellman -# provides Perfect Forward Secrecy. +# provides Perfect Forward Secrecy. source setup/functions.sh # load our functions source /etc/mailinabox.conf # load global vars @@ -39,6 +39,7 @@ apt_install openssl # Create a directory to store TLS-related things like "SSL" certificates. mkdir -p $STORAGE_ROOT/ssl +mkdir -p $STORAGE_ROOT/ssl-backup # creating a backup directory for ssl certs just to be safe # Generate a new private key. # @@ -66,6 +67,13 @@ if [ ! -f $STORAGE_ROOT/ssl/ssl_private_key.pem ]; then openssl genrsa -out $STORAGE_ROOT/ssl/ssl_private_key.pem 2048) fi +# for Double TLSA scheme. More details here (https://mail.sys4.de/pipermail/dane-users/2018-February/000440.html) +if [ ! -f $STORAGE_ROOT/ssl/next_ssl_private_key.pem ]; then + # Set the umask so the key file is never world-readable. + (umask 077; hide_output \ + openssl genrsa -out $STORAGE_ROOT/ssl/next_ssl_private_key.pem 2048) +fi + # Generate a self-signed SSL certificate because things like nginx, dovecot, # etc. won't even start without some certificate in place, and we need nginx # so we can offer the user a control panel to install a better certificate.