def get_domain_ssl_files(domain, env): # What SSL private key will we use? Allow the user to override this, but # in many cases using the same private key for all domains would be fine. # Don't allow the user to override the key for PRIMARY_HOSTNAME because # that's what's in the main file. ssl_key = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_private_key.pem') alt_key = os.path.join(env["STORAGE_ROOT"], 'ssl/domains/%s_private_key.pem' % safe_domain_name(domain)) if domain != env['PRIMARY_HOSTNAME'] and os.path.exists(alt_key): ssl_key = alt_key # What SSL certificate will we use? This has to be differnet for each # domain name. For PRIMARY_HOSTNAME, use the one we generated at set-up # time. if domain == env['PRIMARY_HOSTNAME']: ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') else: ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/domains/%s_certifiate.pem' % safe_domain_name(domain)) # Where would the CSR go? As with the SSL cert itself, the CSR must be # different for each domain name. if domain == env['PRIMARY_HOSTNAME']: csr_path = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_cert_sign_req.csr') else: csr_path = os.path.join(env["STORAGE_ROOT"], 'ssl/domains/%s_cert_sign_req.csr' % safe_domain_name(domain)) return ssl_key, ssl_certificate, csr_path
def get_domain_ssl_files(domain, env, allow_shared_cert=True): # What SSL private key will we use? Allow the user to override this, but # in many cases using the same private key for all domains would be fine. # Don't allow the user to override the key for PRIMARY_HOSTNAME because # that's what's in the main file. ssl_key = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_private_key.pem') ssl_key_is_alt = False alt_key = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/private_key.pem' % safe_domain_name(domain)) if domain != env['PRIMARY_HOSTNAME'] and os.path.exists(alt_key): ssl_key = alt_key ssl_key_is_alt = True # What SSL certificate will we use? ssl_certificate_primary = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') if domain == env['PRIMARY_HOSTNAME']: # For PRIMARY_HOSTNAME, use the one we generated at set-up time. ssl_certificate = ssl_certificate_primary else: # For other domains, we'll probably use a certificate in a different path. ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain)) # But we can be smart and reuse the main SSL certificate if is has # a Subject Alternative Name matching this domain. Don't do this if # the user has uploaded a different private key for this domain. if not ssl_key_is_alt and allow_shared_cert: from status_checks import check_certificate if check_certificate(domain, ssl_certificate_primary, None)[0] == "OK": ssl_certificate = ssl_certificate_primary return ssl_key, ssl_certificate
def make_domain_config(domain, template, template_for_primaryhost, env): # How will we configure this domain. # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? ssl_key, ssl_certificate, csr_path = 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. ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, env) # Put pieces together. nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template) nginx_conf = nginx_conf_parts[0] + "\n" if domain == env['PRIMARY_HOSTNAME']: nginx_conf += template_for_primaryhost + "\n" # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain.encode("idna").decode("ascii")) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) # Because the certificate may change, we should recognize this so we # can trigger an nginx update. def hashfile(filepath): import hashlib sha1 = hashlib.sha1() f = open(filepath, 'rb') try: sha1.update(f.read()) finally: f.close() return sha1.hexdigest() nginx_conf += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate)) # Add in any user customizations in YAML format. nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] for path, url in yaml.get("proxies", {}).items(): nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url) for path, url in yaml.get("redirects", {}).items(): nginx_conf += "\trewrite %s %s permanent;\n" % (path, url) # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include) # Ending. nginx_conf += nginx_conf_parts[1] return nginx_conf
def get_web_root(domain, env, test_exists=True): # Try STORAGE_ROOT/web/domain_name if it exists, but fall back to STORAGE_ROOT/web/default. for test_domain in (domain, "default"): root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain)) if os.path.exists(root) or not test_exists: break return root
def get_dns_zones(env): # What domains should we create DNS zones for? Never create a zone for # a domain & a subdomain of that domain. domains = get_dns_domains(env) # Exclude domains that are subdomains of other domains we know. Proceed # by looking at shorter domains first. zone_domains = set() for domain in sorted(domains, key=lambda d: len(d)): for d in zone_domains: if domain.endswith("." + d): # We found a parent domain already in the list. break else: # 'break' did not occur: there is no parent domain. zone_domains.add(domain) # Make a nice and safe filename for each domain. zonefiles = [] for domain in zone_domains: zonefiles.append([domain, safe_domain_name(domain) + ".txt"]) # Sort the list so that the order is nice and so that nsd.conf has a # stable order so we don't rewrite the file & restart the service # meaninglessly. zone_order = sort_domains([zone[0] for zone in zonefiles], env) zonefiles.sort(key=lambda zone: zone_order.index(zone[0])) return zonefiles
def make_domain_config(domain, template, template_for_primaryhost, env): # How will we configure this domain. # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? 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. ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env) # Put pieces together. nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template) nginx_conf = nginx_conf_parts[0] + "\n" if domain == env['PRIMARY_HOSTNAME']: nginx_conf += template_for_primaryhost + "\n" # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) # Because the certificate may change, we should recognize this so we # can trigger an nginx update. def hashfile(filepath): import hashlib sha1 = hashlib.sha1() f = open(filepath, 'rb') try: sha1.update(f.read()) finally: f.close() return sha1.hexdigest() nginx_conf += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate)) # Add in any user customizations in YAML format. nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] for path, url in yaml.get("proxies", {}).items(): nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url) for path, url in yaml.get("redirects", {}).items(): nginx_conf += "\trewrite %s %s permanent;\n" % (path, url) # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include) # Ending. nginx_conf += nginx_conf_parts[1] return nginx_conf
def get_web_root(domain, env, test_exists=True): # Try STORAGE_ROOT/web/domain_name if it exists, but fall back to STORAGE_ROOT/web/default. for test_domain in (domain, 'default'): root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain)) if os.path.exists(root) or not test_exists: break return root
def install_cert(domain, ssl_cert, ssl_chain, env): # Write the combined cert+chain to a temporary path and validate that it is OK. # The certificate always goes above the chain. import tempfile fd, fn = tempfile.mkstemp('.pem') os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii")) os.close(fd) # Do validation on the certificate before installing it. ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem')) cert_status, cert_status_details = check_certificate(domain, fn, ssl_private_key) if cert_status != "OK": if cert_status == "SELF-SIGNED": cert_status = "This is a self-signed certificate. I can't install that." os.unlink(fn) if cert_status_details is not None: cert_status += " " + cert_status_details return cert_status # Where to put it? # Make a unique path for the certificate. from cryptography.hazmat.primitives import hashes from binascii import hexlify cert = load_pem(load_cert_chain(fn)[0]) all_domains, cn = get_certificate_domains(cert) path = "%s-%s-%s.pem" % ( safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix ) ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', path)) # Install the certificate. os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True) shutil.move(fn, ssl_certificate) ret = ["OK"] # When updating the cert for PRIMARY_HOSTNAME, symlink it from the system # certificate path, which is hard-coded for various purposes, and then # restart postfix and dovecot. if domain == env['PRIMARY_HOSTNAME']: # Update symlink. system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) os.unlink(system_ssl_certificate) os.symlink(ssl_certificate, system_ssl_certificate) # Restart postfix and dovecot so they pick up the new file. shell('check_call', ["/usr/sbin/service", "postfix", "restart"]) shell('check_call', ["/usr/sbin/service", "dovecot", "restart"]) ret.append("mail services restarted") # 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. # Update the web configuration so nginx picks up the new certificate file. from web_update import do_web_update ret.append( do_web_update(env) ) return "\n".join(ret)
def get_dns_zones(env): # What domains should we create DNS zones for? Never create a zone for # a domain & a subdomain of that domain. domains = get_dns_domains(env) # Exclude domains that are subdomains of other domains we know. Proceed # by looking at shorter domains first. zone_domains = set() for domain in sorted(domains, key=lambda d : len(d)): for d in zone_domains: if domain.endswith("." + d): # We found a parent domain already in the list. break else: # 'break' did not occur: there is no parent domain. zone_domains.add(domain) # Make a nice and safe filename for each domain. zonefiles = [] for domain in zone_domains: zonefiles.append([domain, safe_domain_name(domain) + ".txt"]) # Sort the list so that the order is nice and so that nsd.conf has a # stable order so we don't rewrite the file & restart the service # meaninglessly. zone_order = sort_domains([ zone[0] for zone in zonefiles ], env) zonefiles.sort(key = lambda zone : zone_order.index(zone[0]) ) return zonefiles
def get_domain_ssl_files(domain, env): # What SSL private key will we use? Allow the user to override this, but # in many cases using the same private key for all domains would be fine. # Don't allow the user to override the key for PRIMARY_HOSTNAME because # that's what's in the main file. ssl_key = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_private_key.pem') ssl_key_is_alt = False alt_key = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/private_key.pem' % safe_domain_name(domain)) if domain != env['PRIMARY_HOSTNAME'] and os.path.exists(alt_key): ssl_key = alt_key ssl_key_is_alt = True # What SSL certificate will we use? ssl_certificate_primary = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') if domain == env['PRIMARY_HOSTNAME']: # For PRIMARY_HOSTNAME, use the one we generated at set-up time. ssl_certificate = ssl_certificate_primary else: # For other domains, we'll probably use a certificate in a different path. ssl_certificate = os.path.join( env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain)) # But we can be smart and reuse the main SSL certificate if is has # a Subject Alternative Name matching this domain. Don't do this if # the user has uploaded a different private key for this domain. if not ssl_key_is_alt: from whats_next import check_certificate if check_certificate(domain, ssl_certificate_primary, None) == "OK": ssl_certificate = ssl_certificate_primary # Where would the CSR go? As with the SSL cert itself, the CSR must be # different for each domain name. if domain == env['PRIMARY_HOSTNAME']: csr_path = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_cert_sign_req.csr') else: csr_path = os.path.join( env["STORAGE_ROOT"], 'ssl/%s/certificate_signing_request.csr' % safe_domain_name(domain)) return ssl_key, ssl_certificate, csr_path
def get_domain_ssl_files(domain, env, allow_shared_cert=True): # What SSL private key will we use? Allow the user to override this, but # in many cases using the same private key for all domains would be fine. # Don't allow the user to override the key for PRIMARY_HOSTNAME because # that's what's in the main file. ssl_key = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_private_key.pem') ssl_key_is_alt = False alt_key = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/private_key.pem' % safe_domain_name(domain)) if domain != env['PRIMARY_HOSTNAME'] and os.path.exists(alt_key): ssl_key = alt_key ssl_key_is_alt = 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 else: # For other domains, we'll probably use a certificate in a different path. ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain)) # But we can be smart and reuse the main SSL certificate if is has # a Subject Alternative Name matching this domain. Don't do this if # the user has uploaded a different private key for this domain. if not ssl_key_is_alt and allow_shared_cert: 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'] # 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 get_domain_ssl_files(domain, env): # What SSL private key will we use? Allow the user to override this, but # in many cases using the same private key for all domains would be fine. # Don't allow the user to override the key for PRIMARY_HOSTNAME because # that's what's in the main file. ssl_key = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_private_key.pem') ssl_key_is_alt = False alt_key = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/private_key.pem' % safe_domain_name(domain)) if domain != env['PRIMARY_HOSTNAME'] and os.path.exists(alt_key): ssl_key = alt_key ssl_key_is_alt = True # What SSL certificate will we use? ssl_certificate_primary = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') if domain == env['PRIMARY_HOSTNAME']: # For PRIMARY_HOSTNAME, use the one we generated at set-up time. ssl_certificate = ssl_certificate_primary else: # For other domains, we'll probably use a certificate in a different path. ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/ssl_certificate.pem' % safe_domain_name(domain)) # But we can be smart and reuse the main SSL certificate if is has # a Subject Alternative Name matching this domain. Don't do this if # the user has uploaded a different private key for this domain. if not ssl_key_is_alt: from status_checks import check_certificate if check_certificate(domain, ssl_certificate_primary, None)[0] == "OK": ssl_certificate = ssl_certificate_primary # Where would the CSR go? As with the SSL cert itself, the CSR must be # different for each domain name. if domain == env['PRIMARY_HOSTNAME']: csr_path = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_cert_sign_req.csr') else: csr_path = os.path.join(env["STORAGE_ROOT"], 'ssl/%s/certificate_signing_request.csr' % safe_domain_name(domain)) return ssl_key, ssl_certificate, csr_path
def make_domain_config(domain, template, template_for_primaryhost, env): # How will we configure this domain. # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? ssl_key, ssl_certificate, csr_path = 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. ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, env) # Put pieces together. nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template) nginx_conf = nginx_conf_parts[0] + "\n" if domain == env['PRIMARY_HOSTNAME']: nginx_conf += template_for_primaryhost + "\n" # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) # Add in any user customizations in YAML format. nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] for path, url in yaml.get("proxies", {}).items(): nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % ( path, url) # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join( env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include) # Ending. nginx_conf += nginx_conf_parts[1] return nginx_conf
def install_cert_copy_file(fn, env): # Where to put it? # Make a unique path for the certificate. from cryptography.hazmat.primitives import hashes from binascii import hexlify cert = load_pem(load_cert_chain(fn)[0]) all_domains, cn = get_certificate_domains(cert) path = "%s-%s-%s.pem" % ( safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix ) ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', path)) # Install the certificate. os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True) shutil.move(fn, ssl_certificate)
def make_domain_config(domain, template, template_for_primaryhost, env): # How will we configure this domain. # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? ssl_key, ssl_certificate, csr_path = 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. ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, env) # Put pieces together. nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template) nginx_conf = nginx_conf_parts[0] + "\n" if domain == env['PRIMARY_HOSTNAME']: nginx_conf += template_for_primaryhost + "\n" # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) # Add in any user customizations in YAML format. nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] for path, url in yaml.get("proxies", {}).items(): nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url) # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include) # Ending. nginx_conf += nginx_conf_parts[1] return nginx_conf
def make_domain_config(domain, templates, ssl_certificates, env): # GET SOME VARIABLES # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? tls_cert = get_domain_ssl_files(domain, ssl_certificates, env) # ADDITIONAL DIRECTIVES. nginx_conf_extra = "" # Because the certificate may change, we should recognize this so we # can trigger an nginx update. def hashfile(filepath): import hashlib sha1 = hashlib.sha1() f = open(filepath, 'rb') try: sha1.update(f.read()) finally: f.close() return sha1.hexdigest() nginx_conf_extra += "# ssl files sha1: %s / %s\n" % ( hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"])) # Add in any user customizations in YAML format. hsts = "yes" nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] # any proxy or redirect here? for path, url in yaml.get("proxies", {}).items(): nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url) for path, url in yaml.get("redirects", {}).items(): nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url) # override the HSTS directive type hsts = yaml.get("hsts", hsts) # Add the HSTS header. if hsts == "yes": nginx_conf_extra += "add_header Strict-Transport-Security max-age=31536000;\n" elif hsts == "preload": nginx_conf_extra += "add_header Strict-Transport-Security \"max-age=10886400; includeSubDomains; preload\";\n" # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include) # PUT IT ALL TOGETHER # Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder # of the previous template. nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n" for t in templates + [nginx_conf_extra]: nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf) # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"]) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", tls_cert["certificate"]) nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain return nginx_conf
def is_default_web_root(domain, env): root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain)) return not os.path.exists(root)
def make_domain_config(domain, templates, ssl_certificates, env): # GET SOME VARIABLES # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, ssl_certificates, env) # ADDITIONAL DIRECTIVES. nginx_conf_extra = "" # Because the certificate may change, we should recognize this so we # can trigger an nginx update. def hashfile(filepath): import hashlib sha1 = hashlib.sha1() f = open(filepath, 'rb') try: sha1.update(f.read()) finally: f.close() return sha1.hexdigest() nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate)) # Add in any user customizations in YAML format. hsts = "yes" nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] # any proxy or redirect here? for path, url in yaml.get("proxies", {}).items(): nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url) for path, url in yaml.get("redirects", {}).items(): nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url) # override the HSTS directive type hsts = yaml.get("hsts", hsts) # Add the HSTS header. if hsts == "yes": nginx_conf_extra += "add_header Strict-Transport-Security max-age=31536000;\n" elif hsts == "preload": nginx_conf_extra += "add_header Strict-Transport-Security \"max-age=10886400; includeSubDomains; preload\";\n" # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include) # PUT IT ALL TOGETHER # Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder # of the previous template. nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n" for t in templates + [nginx_conf_extra]: nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf) # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain return nginx_conf
def make_domain_config(domain, templates, env): # GET SOME VARIABLES # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? 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. ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env) # ADDITIONAL DIRECTIVES. nginx_conf_extra = "" # Because the certificate may change, we should recognize this so we # can trigger an nginx update. def hashfile(filepath): import hashlib sha1 = hashlib.sha1() f = open(filepath, 'rb') try: sha1.update(f.read()) finally: f.close() return sha1.hexdigest() nginx_conf_extra += "# ssl files sha1: %s / %s\n" % ( hashfile(ssl_key), hashfile(ssl_certificate)) # Add in any user customizations in YAML format. nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] for path, url in yaml.get("proxies", {}).items(): nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % ( path, url) for path, url in yaml.get("redirects", {}).items(): nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url) # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join( env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include) # PUT IT ALL TOGETHER # Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder # of the previous template. nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n" for t in templates + [nginx_conf_extra]: nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf) # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) nginx_conf = nginx_conf.replace( "$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain return nginx_conf
def make_domain_config(domain, templates, env): # GET SOME VARIABLES # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? 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. ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env) # ADDITIONAL DIRECTIVES. nginx_conf_extra = "" # Because the certificate may change, we should recognize this so we # can trigger an nginx update. def hashfile(filepath): import hashlib sha1 = hashlib.sha1() f = open(filepath, 'rb') try: sha1.update(f.read()) finally: f.close() return sha1.hexdigest() nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate)) # Add in any user customizations in YAML format. nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] for path, url in yaml.get("proxies", {}).items(): nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url) for path, url in yaml.get("redirects", {}).items(): nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url) # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if os.path.exists(nginx_conf_custom_include): nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include) # PUT IT ALL TOGETHER # Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder # of the previous template. nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n" for t in templates + [nginx_conf_extra]: nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf) # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain return nginx_conf
def make_domain_config(domain, templates, ssl_certificates, env): # GET SOME VARIABLES # Where will its root directory be for static files? root = get_web_root(domain, env) # What private key and SSL certificate will we use for this domain? tls_cert = get_domain_ssl_files(domain, ssl_certificates, env) # ADDITIONAL DIRECTIVES. nginx_conf_extra = "" # Because the certificate may change, we should recognize this so we # can trigger an nginx update. def hashfile(filepath): import hashlib sha1 = hashlib.sha1() f = open(filepath, 'rb') try: sha1.update(f.read()) finally: f.close() return sha1.hexdigest() nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"])) # Add in any user customizations in YAML format. hsts = "yes" nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml") if os.path.exists(nginx_conf_custom_fn): yaml = rtyaml.load(open(nginx_conf_custom_fn)) if domain in yaml: yaml = yaml[domain] # any proxy or redirect here? for path, url in yaml.get("proxies", {}).items(): # Parse some flags in the fragment of the URL. pass_http_host_header = False proxy_redirect_off = False frame_options_header_sameorigin = False m = re.search("#(.*)$", url) if m: for flag in m.group(1).split(","): if flag == "pass-http-host": pass_http_host_header = True elif flag == "no-proxy-redirect": proxy_redirect_off = True elif flag == "frame-options-sameorigin": frame_options_header_sameorigin = True url = re.sub("#(.*)$", "", url) nginx_conf_extra += "\tlocation %s {" % path nginx_conf_extra += "\n\t\tproxy_pass %s;" % url if proxy_redirect_off: nginx_conf_extra += "\n\t\tproxy_redirect off;" if pass_http_host_header: nginx_conf_extra += "\n\t\tproxy_set_header Host $http_host;" if frame_options_header_sameorigin: nginx_conf_extra += "\n\t\tproxy_set_header X-Frame-Options SAMEORIGIN;" nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;" nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Host $http_host;" nginx_conf_extra += "\n\t\tproxy_set_header X-Forwarded-Proto $scheme;" nginx_conf_extra += "\n\t\tproxy_set_header X-Real-IP $remote_addr;" nginx_conf_extra += "\n\t}\n" for path, alias in yaml.get("aliases", {}).items(): nginx_conf_extra += "\tlocation %s {" % path nginx_conf_extra += "\n\t\talias %s;" % alias nginx_conf_extra += "\n\t}\n" for path, url in yaml.get("redirects", {}).items(): nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url) # override the HSTS directive type hsts = yaml.get("hsts", hsts) # Add the HSTS header. if hsts == "yes": nginx_conf_extra += "\tadd_header Strict-Transport-Security \"max-age=15768000\" always;\n" elif hsts == "preload": nginx_conf_extra += "\tadd_header Strict-Transport-Security \"max-age=15768000; includeSubDomains; preload\" always;\n" # Add in any user customizations in the includes/ folder. nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf") if not os.path.exists(nginx_conf_custom_include): with open(nginx_conf_custom_include, "a+") as f: f.writelines([ f"# Custom configurations for {domain} go here\n", "# To use php: use the \"php-fpm\" alias\n\n", "index index.html index.htm;\n" ]) nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include) # PUT IT ALL TOGETHER # Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder # of the previous template. nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n" for t in templates + [nginx_conf_extra]: nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf) # Replace substitution strings in the template & return. nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"]) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", tls_cert["certificate"]) nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain return nginx_conf
def install_cert(domain, ssl_cert, ssl_chain, env, raw=False): # Write the combined cert+chain to a temporary path and validate that it is OK. # The certificate always goes above the chain. import tempfile fd, fn = tempfile.mkstemp('.pem') os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii")) os.close(fd) # Do validation on the certificate before installing it. ssl_private_key = os.path.join( os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem')) cert_status, cert_status_details = check_certificate( domain, fn, ssl_private_key) if cert_status != "OK": if cert_status == "SELF-SIGNED": cert_status = "This is a self-signed certificate. I can't install that." os.unlink(fn) if cert_status_details is not None: cert_status += " " + cert_status_details return cert_status # Where to put it? # Make a unique path for the certificate. from cryptography.hazmat.primitives import hashes from binascii import hexlify cert = load_pem(load_cert_chain(fn)[0]) all_domains, cn = get_certificate_domains(cert) path = "%s-%s-%s.pem" % ( safe_domain_name( cn ), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date hexlify(cert.fingerprint( hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix ) ssl_certificate = os.path.join( os.path.join(env["STORAGE_ROOT"], 'ssl', path)) # Install the certificate. os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True) shutil.move(fn, ssl_certificate) ret = ["OK"] # When updating the cert for PRIMARY_HOSTNAME, symlink it from the system # certificate path, which is hard-coded for various purposes, and then # restart postfix and dovecot. if domain == env['PRIMARY_HOSTNAME']: # Update symlink. system_ssl_certificate = os.path.join( os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) os.unlink(system_ssl_certificate) os.symlink(ssl_certificate, system_ssl_certificate) # Restart postfix and dovecot so they pick up the new file. shell('check_call', ["/usr/sbin/service", "postfix", "restart"]) shell('check_call', ["/usr/sbin/service", "dovecot", "restart"]) ret.append("mail services restarted") # 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. # Update the web configuration so nginx picks up the new certificate file. from web_update import do_web_update ret.append(do_web_update(env)) if raw: return ret return "\n".join(ret)