diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e81731d..52a16ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Web: * Static websites now deny access to certain dot (.) files and directories which typically have sensitive info: .ht*, .svn*, .git*, .hg*, .bzr*. * The nginx server no longer reports its version and OS for better privacy. * The HTTP->HTTPS redirect is now more efficient. +* When serving a 'www.' domain, reuse the SSL certificate for the parent domain if it covers the 'www' subdomain too Control panel: diff --git a/management/buy_certificate.py b/management/buy_certificate.py deleted file mode 100755 index f6e3f4d9..00000000 --- a/management/buy_certificate.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/python3 - -# Helps you purchase a SSL certificate from Gandi.net using -# their API. -# -# Before you begin: -# 1) Create an account on Gandi.net. -# 2) Pre-pay $16 into your account at https://www.gandi.net/prepaid/operations. Wait until the payment goes through. -# 3) Activate your API key first on the test platform (wait a while, refresh the page) and then activate the production API at https://www.gandi.net/admin/api_key. - -import sys, re, os.path, urllib.request -import xmlrpc.client -import rtyaml - -from utils import load_environment, shell -from web_update import get_web_domains, get_domain_ssl_files, get_web_root -from status_checks import check_certificate - -def buy_ssl_certificate(api_key, domain, command, env): - if domain != env['PRIMARY_HOSTNAME'] \ - and domain not in get_web_domains(env): - raise ValueError("Domain is not %s or a domain we're serving a website for." % env['PRIMARY_HOSTNAME']) - - # Initialize. - - gandi = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/') - - try: - existing_certs = gandi.cert.list(api_key) - except Exception as e: - if "Invalid API key" in str(e): - print("Invalid API key. Check that you copied the API Key correctly from https://www.gandi.net/admin/api_key.") - sys.exit(1) - else: - raise - - # Where is the SSL cert stored? - - ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) - - # Have we already created a cert for this domain? - - for cert in existing_certs: - if cert['cn'] == domain: - break - else: - # No existing cert found. Purchase one. - if command != 'purchase': - print("No certificate or order found yet. If you haven't yet purchased a certificate, run ths script again with the 'purchase' command. Otherwise wait a moment and try again.") - sys.exit(1) - else: - # Start an order for a single standard SSL certificate. - # Use DNS validation. Web-based validation won't work because they - # require a file on HTTP but not HTTPS w/o redirects and we don't - # serve anything plainly over HTTP. Email might be another way but - # DNS is easier to automate. - op = gandi.cert.create(api_key, { - "csr": open(ssl_csr_path).read(), - "dcv_method": "dns", - "duration": 1, # year? - "package": "cert_std_1_0_0", - }) - print("An SSL certificate has been ordered.") - print() - print(op) - print() - print("In a moment please run this script again with the 'setup' command.") - - if cert['status'] == 'pending': - # Get the information we need to update our DNS with a code so that - # Gandi can verify that we own the domain. - - dcv = gandi.cert.get_dcv_params(api_key, { - "csr": open(ssl_csr_path).read(), - "cert_id": cert['id'], - "dcv_method": "dns", - "duration": 1, # year? - "package": "cert_std_1_0_0", - }) - if dcv["dcv_method"] != "dns": - raise Exception("Certificate ordered with an unknown validation method.") - - # Update our DNS data. - - dns_config = env['STORAGE_ROOT'] + '/dns/custom.yaml' - if os.path.exists(dns_config): - dns_records = rtyaml.load(open(dns_config)) - else: - dns_records = { } - - qname = dcv['md5'] + '.' + domain - value = dcv['sha1'] + '.comodoca.com.' - dns_records[qname] = { "CNAME": value } - - with open(dns_config, 'w') as f: - f.write(rtyaml.dump(dns_records)) - - shell('check_call', ['tools/dns_update']) - - # Okay, done with this step. - - print("DNS has been updated. Gandi will check within 60 minutes.") - print() - print("See https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id']) - - elif cert['status'] == 'valid': - # The certificate is ready. - - # Check before we overwrite something we shouldn't. - if os.path.exists(ssl_certificate): - cert_status, cert_status_details = check_certificate(None, ssl_certificate, None) - if cert_status != "SELF-SIGNED": - print("Please back up and delete the file %s so I can save your new certificate." % ssl_certificate) - sys.exit(1) - - # Form the certificate. - - # The certificate comes as a long base64-encoded string. Break in - # into lines in the usual way. - pem = "-----BEGIN CERTIFICATE-----\n" - pem += "\n".join(chunk for chunk in re.split(r"(.{64})", cert['cert']) if chunk != "") - pem += "\n-----END CERTIFICATE-----\n\n" - - # Append intermediary certificates. - pem += urllib.request.urlopen("https://www.gandi.net/static/CAs/GandiStandardSSLCA.pem").read().decode("ascii") - - # Write out. - - with open(ssl_certificate, "w") as f: - f.write(pem) - - print("The certificate has been installed in %s. Restarting services..." % ssl_certificate) - - # Restart dovecot and if this is for PRIMARY_HOSTNAME. - - if domain == env['PRIMARY_HOSTNAME']: - shell('check_call', ["/usr/sbin/service", "dovecot", "restart"]) - shell('check_call', ["/usr/sbin/service", "postfix", "restart"]) - - # Restart nginx in all cases. - - shell('check_call', ["/usr/sbin/service", "nginx", "restart"]) - - else: - print("The certificate has an unknown status. Please check https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id']) - -if __name__ == "__main__": - if len(sys.argv) < 4: - print("Usage: python management/buy_certificate.py gandi_api_key domain_name {purchase, setup}") - sys.exit(1) - api_key = sys.argv[1] - domain_name = sys.argv[2] - cmd = sys.argv[3] - buy_ssl_certificate(api_key, domain_name, cmd, load_environment()) - diff --git a/management/daemon.py b/management/daemon.py index bc2099b8..be617586 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -272,7 +272,7 @@ def dns_get_dump(): @authorized_personnel_only def ssl_get_csr(domain): from web_update import get_domain_ssl_files, create_csr - ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) + ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env) return create_csr(domain, ssl_key, env) @app.route('/ssl/install', methods=['POST']) diff --git a/management/status_checks.py b/management/status_checks.py index e20cbabd..f4ecf042 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -523,7 +523,7 @@ def check_ssl_cert(domain, env, output): if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return # Where is the SSL stored? - ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) + ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env) if not os.path.exists(ssl_certificate): output.print_error("The SSL certificate file for this domain is missing.") @@ -535,7 +535,7 @@ def check_ssl_cert(domain, env, output): if cert_status == "OK": # The certificate is ok. The details has expiry info. - output.print_ok("SSL certificate is signed & valid. " + cert_status_details) + output.print_ok("SSL certificate is signed & valid. %s %s" % (ssl_via if ssl_via else "", cert_status_details)) elif cert_status == "SELF-SIGNED": # Offer instructions for purchasing a signed certificate. @@ -788,7 +788,7 @@ if __name__ == "__main__": domain = env['PRIMARY_HOSTNAME'] if query_dns(domain, "A") != env['PUBLIC_IP']: sys.exit(1) - ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) + ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env) if not os.path.exists(ssl_certificate): sys.exit(1) cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key) diff --git a/management/web_update.py b/management/web_update.py index c4fc99e2..98fa8abf 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -74,7 +74,7 @@ def make_domain_config(domain, template, template_for_primaryhost, env): root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? - ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) + ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env) # For hostnames created after the initial setup, ensure we have an SSL certificate # available. Make a self-signed one now if one doesn't exist. @@ -148,6 +148,7 @@ def get_domain_ssl_files(domain, env, allow_shared_cert=True): # What SSL certificate will we use? ssl_certificate_primary = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') + ssl_via = None if domain == env['PRIMARY_HOSTNAME']: # For PRIMARY_HOSTNAME, use the one we generated at set-up time. ssl_certificate = ssl_certificate_primary @@ -162,8 +163,16 @@ def get_domain_ssl_files(domain, env, allow_shared_cert=True): from status_checks import check_certificate if check_certificate(domain, ssl_certificate_primary, None)[0] == "OK": ssl_certificate = ssl_certificate_primary + ssl_via = "Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME'] - return ssl_key, ssl_certificate + # For a 'www.' domain, see if we can reuse the cert of the parent. + elif domain.startswith('www.'): + ssl_certificate_parent = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain[4:])) + if os.path.exists(ssl_certificate_parent) and check_certificate(domain, ssl_certificate_parent, None)[0] == "OK": + ssl_certificate = ssl_certificate_parent + ssl_via = "Using multi/wildcard certificate of %s." % domain[4:] + + return ssl_key, ssl_certificate, ssl_via def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env): # For domains besides PRIMARY_HOSTNAME, generate a self-signed certificate if @@ -218,7 +227,7 @@ def install_cert(domain, ssl_cert, ssl_chain, env): # Do validation on the certificate before installing it. from status_checks import check_certificate - ssl_key, ssl_certificate = get_domain_ssl_files(domain, env, allow_shared_cert=False) + ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env, allow_shared_cert=False) cert_status, cert_status_details = check_certificate(domain, fn, ssl_key) if cert_status != "OK": if cert_status == "SELF-SIGNED": @@ -261,16 +270,16 @@ def get_web_domains_info(env): # for the SSL config panel, get cert status def check_cert(domain): from status_checks import check_certificate - ssl_key, ssl_certificate = get_domain_ssl_files(domain, env) + ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, env) if not os.path.exists(ssl_certificate): return ("danger", "No Certificate Installed") cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key) if cert_status == "OK": - if domain == env['PRIMARY_HOSTNAME'] or ssl_certificate != get_domain_ssl_files(env['PRIMARY_HOSTNAME'], env)[1]: + if not ssl_via: return ("success", "Signed & valid. " + cert_status_details) else: # This is an alternate domain but using the same cert as the primary domain. - return ("success", "Signed & valid. Using multi/wildcard certificate of %s." % env['PRIMARY_HOSTNAME']) + return ("success", "Signed & valid. " + ssl_via) elif cert_status == "SELF-SIGNED": return ("warning", "Self-signed. Get a signed certificate to stop warnings.") else: