def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True): # What domains should we serve HTTP(S) for? domains = set() # Serve web for all mail domains so that we might at least # provide auto-discover of email settings, and also a static website # if the user wants to make one. domains |= get_mail_domains(env) if include_www_redirects: # Add 'www.' subdomains that we want to provide default redirects # to the main domain for. We'll add 'www.' to any DNS zones, i.e. # the topmost of each domain we serve. domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env)) if exclude_dns_elsewhere: # ...Unless the domain has an A/AAAA record that maps it to a different # IP address than this box. Remove those domains from our list. domains -= get_domains_with_a_records(env) # Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail # as well as Z-Push for Exchange ActiveSync. This can't be removed # by a custom A/AAAA record and is never a 'www.' redirect. domains.add(env['PRIMARY_HOSTNAME']) # Sort the list so the nginx conf gets written in a stable order. domains = sort_domains(domains, env) return domains
def run_domain_checks(rounded_time, env, output, pool): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) domains_to_check = mail_domains | dns_domains | web_domains # Get the list of domains that we don't serve web for because of a custom CNAME/A record. domains_with_a_records = get_domains_with_a_records(env) ssl_certificates = get_ssl_certificates(env) # Serial version: #for domain in sort_domains(domains_to_check, env): # run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains) # Parallelize the checks across a worker pool. args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records, ssl_certificates) for domain in domains_to_check) ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1) ret = dict(ret) # (domain, output) => { domain: output } for domain in sort_domains(ret, env): ret[domain].playback(output)
def get_web_domains(env, include_www_redirects=True): # What domains should we serve HTTP(S) for? domains = set() # Serve web for all mail domains so that we might at least # provide auto-discover of email settings, and also a static website # if the user wants to make one. domains |= get_mail_domains(env) if include_www_redirects: # Add 'www.' subdomains that we want to provide default redirects # to the main domain for. We'll add 'www.' to any DNS zones, i.e. # the topmost of each domain we serve. domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env)) # ...Unless the domain has an A/AAAA record that maps it to a different # IP address than this box. Remove those domains from our list. domains -= get_domains_with_a_records(env) # Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail # as well as Z-Push for Exchange ActiveSync. This can't be removed # by a custom A/AAAA record and is never a 'www.' redirect. domains.add(env['PRIMARY_HOSTNAME']) # Sort the list so the nginx conf gets written in a stable order. domains = sort_domains(domains, env) return domains
def get_default_www_redirects(env): # Returns a list of www subdomains that we want to provide default redirects # for, i.e. any www's that aren't domains the user has actually configured # to serve for real. Which would be unusual. web_domains = set(get_web_domains(env)) www_domains = set('www.' + zone for zone, zonefile in get_dns_zones(env)) return sort_domains(www_domains - web_domains - get_domains_with_a_records(env), env)
def run_domain_checks(env): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) domains_to_check = mail_domains | dns_domains | web_domains # Serial version: #for domain in sort_domains(domains_to_check, env): # run_domain_checks_on_domain(domain, env, dns_domains, dns_zonefiles, mail_domains, web_domains) # Parallelize the checks across a worker pool. args = ((domain, env, dns_domains, dns_zonefiles, mail_domains, web_domains) for domain in domains_to_check) pool = multiprocessing.pool.Pool(processes=10) ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1) ret = dict(ret) # (domain, output) => { domain: output } output = BufferedOutput() for domain in sort_domains(ret, env): ret[domain].playback(output) return output
def run_domain_checks(env): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) # Check the domains. for domain in sort_domains(mail_domains | dns_domains | web_domains, env): env["out"].add_heading(domain) if domain == env["PRIMARY_HOSTNAME"]: check_primary_hostname_dns(domain, env, dns_domains, dns_zonefiles) if domain in dns_domains: check_dns_zone(domain, env, dns_zonefiles) if domain in mail_domains: check_mail_domain(domain, env) if domain in web_domains: check_web_domain(domain, env) if domain in dns_domains: check_dns_zone_suggestions(domain, env, dns_zonefiles)
def run_domain_checks(env): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) # Check the domains. for domain in sort_domains(mail_domains | dns_domains | web_domains, env): print(domain) print("=" * len(domain)) if domain == env["PRIMARY_HOSTNAME"]: check_primary_hostname_dns(domain, env) check_alias_exists("administrator@" + domain, env) if domain in dns_domains: check_dns_zone(domain, env, dns_zonefiles) if domain in mail_domains: check_mail_domain(domain, env) if domain == env["PRIMARY_HOSTNAME"] or domain in web_domains: # We need a SSL certificate for PRIMARY_HOSTNAME because that's where the # user will log in with IMAP or webmail. Any other domain we serve a # website for also needs a signed certificate. check_ssl_cert(domain, env) print()
def run_domain_checks(rounded_time, env, output, pool): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env) + get_default_www_redirects(env)) domains_to_check = mail_domains | dns_domains | web_domains # Serial version: # for domain in sort_domains(domains_to_check, env): # run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains) # Parallelize the checks across a worker pool. args = ( (domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains) for domain in domains_to_check ) ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1) ret = dict(ret) # (domain, output) => { domain: output } for domain in sort_domains(ret, env): ret[domain].playback(output)
def get_web_domain_flags(env): flags = dict() zones = get_dns_zones(env) email_domains = get_mail_domains(env) user_domains = get_mail_domains(env, users_only=True) external = get_domains_with_a_records(env) redirects = get_web_domains_with_root_overrides(env) for d in email_domains: flags[d] = flags.get(d, 0) flags[f"mta-sts.{d}"] = flags.get(d, 0) flags[f"openpgpkey.{d}"] = flags.get(d, 0) | DOMAIN_WKD for d in user_domains: flags[f"autoconfig.{d}"] = flags.get(d, 0) flags[f"autodiscover.{d}"] = flags.get(d, 0) for d, _ in zones: flags[f"www.{d}"] = flags.get(d, 0) | DOMAIN_WWW for d in redirects: flags[d] = flags.get(d, 0) | DOMAIN_REDIRECT flags[env["PRIMARY_HOSTNAME"]] |= DOMAIN_PRIMARY # Last check for websites hosted elsewhere for d in flags.keys(): if d in external: flags[d] = DOMAIN_EXTERNAL # -1 = All bits set to 1, assuming twos-complement return flags
def run_domain_checks(env): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) # Check the domains. for domain in sort_domains(mail_domains | dns_domains | web_domains, env): env["out"].add_heading(domain) if domain == env["PRIMARY_HOSTNAME"]: check_primary_hostname_dns(domain, env) if domain in dns_domains: check_dns_zone(domain, env, dns_zonefiles) if domain in mail_domains: check_mail_domain(domain, env) if domain in web_domains: check_web_domain(domain, env)
def run_domain_checks(rounded_time, env, output, pool): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) domains_to_check = mail_domains | dns_domains | web_domains # Get the list of domains that we don't serve web for because of a custom CNAME/A record. domains_with_a_records = get_domains_with_a_records(env) # Serial version: #for domain in sort_domains(domains_to_check, env): # run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains) # Parallelize the checks across a worker pool. args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records) for domain in domains_to_check) ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1) ret = dict(ret) # (domain, output) => { domain: output } for domain in sort_domains(ret, env): ret[domain].playback(output)
def get_default_www_redirects(env): # Returns a list of www subdomains that we want to provide default redirects # for, i.e. any www's that aren't domains the user has actually configured # to serve for real. Which would be unusual. web_domains = set(get_web_domains(env)) www_domains = set('www.' + zone for zone, zonefile in get_dns_zones(env)) return sort_domains( www_domains - web_domains - get_domains_with_a_records(env), env)
def get_web_domains(env, include_www_redirects=True, include_auto=True, exclude_dns_elsewhere=True, categories=['mail', 'ssl']): # What domains should we serve HTTP(S) for? domains = set() # Serve web for all mail domains so that we might at least # provide auto-discover of email settings, and also a static website # if the user wants to make one. for category in categories: domains |= get_mail_domains(env, category=category) if include_www_redirects and include_auto: # Add 'www.' subdomains that we want to provide default redirects # to the main domain for. We'll add 'www.' to any DNS zones, i.e. # the topmost of each domain we serve. domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env)) if 'mail' in categories and include_auto: # Add Autoconfiguration domains for domains that there are user accounts at: # 'autoconfig.' for Mozilla Thunderbird auto setup. # 'autodiscover.' for Activesync autodiscovery. domains |= set( 'autoconfig.' + maildomain for maildomain in get_mail_domains(env, users_only=True)) domains |= set( 'autodiscover.' + maildomain for maildomain in get_mail_domains(env, users_only=True)) # 'mta-sts.' for MTA-STS support for all domains that have email addresses. domains |= set('mta-sts.' + maildomain for maildomain in get_mail_domains(env)) if exclude_dns_elsewhere: # ...Unless the domain has an A/AAAA record that maps it to a different # IP address than this box. Remove those domains from our list. domains -= get_domains_with_a_records(env) # Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail # as well as Z-Push for Exchange ActiveSync. This can't be removed # by a custom A/AAAA record and is never a 'www.' redirect. domains.add(env['PRIMARY_HOSTNAME']) # Sort the list so the nginx conf gets written in a stable order. domains = sort_domains(domains, env) return domains
def dns_get_records(qname=None, rtype=None): # Get the current set of custom DNS records. from dns_update import get_custom_dns_config, get_dns_zones records = get_custom_dns_config(env, only_real_records=True) # Filter per the arguments for the more complex GET routes below. records = [ r for r in records if (not qname or r[0] == qname) and (not rtype or r[1] == rtype) ] # Make a better data structure. records = [{ "qname": r[0], "rtype": r[1], "value": r[2], "ttl": r[3], "sort-order": {}, } for r in records] # To help with grouping by zone in qname sorting, label each record with which zone it is in. # There's an inconsistency in how we handle zones in get_dns_zones and in sort_domains, so # do this first before sorting the domains within the zones. zones = utils.sort_domains([z[0] for z in get_dns_zones(env)], env) for r in records: for z in zones: if r["qname"] == z or r["qname"].endswith("." + z): r["zone"] = z break # Add sorting information. The 'created' order follows the order in the YAML file on disk, # which tracs the order entries were added in the control panel since we append to the end. # The 'qname' sort order sorts by our standard domain name sort (by zone then by qname), # then by rtype, and last by the original order in the YAML file (since sorting by value # may not make sense, unless we parse IP addresses, for example). for i, r in enumerate(records): r["sort-order"]["created"] = i domain_sort_order = utils.sort_domains([r["qname"] for r in records], env) for i, r in enumerate( sorted(records, key=lambda r: (zones.index(r["zone"]), domain_sort_order.index(r[ "qname"]), r["rtype"]))): r["sort-order"]["qname"] = i # Return. return json_response(records)
def run_domain_checks(rounded_time, env, output, pool): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) domains_to_check = mail_domains | dns_domains | web_domains # Get the list of domains that we don't serve web for because of a custom CNAME/A record. domains_with_a_records = get_domains_with_a_records(env) # Serial version: Other one currently dies... for domain in sort_domains(domains_to_check, env): run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records)
def run_domain_checks(rounded_time, env, output, pool): # Get the list of domains we handle mail for. mail_domains = get_mail_domains(env) # Get the list of domains we serve DNS zones for (i.e. does not include subdomains). dns_zonefiles = dict(get_dns_zones(env)) dns_domains = set(dns_zonefiles) # Get the list of domains we serve HTTPS for. web_domains = set(get_web_domains(env)) domains_to_check = mail_domains | dns_domains | web_domains # Remove "www", "autoconfig", "autodiscover", and "mta-sts" subdomains, which we group with their parent, # if their parent is in the domains to check list. domains_to_check = [ d for d in domains_to_check if not ( d.split(".", 1)[0] in ("www", "autoconfig", "autodiscover", "mta-sts") and len(d.split(".", 1)) == 2 and d.split(".", 1)[1] in domains_to_check ) ] # Get the list of domains that we don't serve web for because of a custom CNAME/A record. domains_with_a_records = get_domains_with_a_records(env) # Serial version: #for domain in sort_domains(domains_to_check, env): # run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains) # Parallelize the checks across a worker pool. args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records) for domain in domains_to_check) ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1) ret = dict(ret) # (domain, output) => { domain: output } for domain in sort_domains(ret, env): ret[domain].playback(output)
def dns_zones(): from dns_update import get_dns_zones return json_response([z[0] for z in get_dns_zones(env)])
def provision_certificates(env, limit_domains): # 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", }) # 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. # Start with the zones so that if the zone doesn't need a certificate itself, # its children will still be grouped together. Sort the provision domains to # put parents ahead of children. # Since Let's Encrypt requests are limited to 100 domains at a time, # we'll create a list of lists of domains where the inner lists have # at most 100 items. By sorting we also get the DNS zone domain as the first # entry in each list (unless we overflow beyond 100) which ends up as the # primary domain listed in each certificate. from dns_update import get_dns_zones certs = {} for zone, zonefile in get_dns_zones(env): certs[zone] = [[]] for domain in sort_domains(domains, env): # Does the domain end with any domain we've seen so far. for parent in certs.keys(): if domain.endswith("." + parent): # Add this to the parent's list of domains. # Start a new group if the list already has # 100 items. if len(certs[parent][-1]) == 100: certs[parent].append([]) certs[parent][-1].append(domain) break else: # This domain is not a child of any domain we've seen yet, so # start a new group. This shouldn't happen since every zone # was already added. certs[domain] = [[domain]] # Flatten to a list of lists of domains (from a mapping). Remove empty # lists (zones with no domains that need certs). certs = sum(certs.values(), []) certs = [_ for _ in certs if len(_) > 0] # Prepare to provision. # Where should we put our Let's Encrypt account info and state cache. account_path = os.path.join(env['STORAGE_ROOT'], 'ssl/lets_encrypt') if not os.path.exists(account_path): os.mkdir(account_path) # Provision certificates. for domain_list in certs: ret.append({ "domains": domain_list, "log": [], }) try: # 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') with tempfile.NamedTemporaryFile() as csr_file: # We could use openssl, but certbot requires # that the CN domain and SAN domains match # the domain list passed to certbot, and adding # SAN domains openssl req is ridiculously complicated. # subprocess.check_output([ # "openssl", "req", "-new", # "-key", key_file, # "-out", csr_file.name, # "-subj", "/CN=" + domain_list[0], # "-sha256" ]) from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives import hashes from cryptography.x509.oid import NameOID builder = x509.CertificateSigningRequestBuilder() builder = builder.subject_name( x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, domain_list[0]) ])) builder = builder.add_extension(x509.BasicConstraints( ca=False, path_length=None), critical=True) builder = builder.add_extension(x509.SubjectAlternativeName( [x509.DNSName(d) for d in domain_list]), critical=False) request = builder.sign(load_pem(load_cert_chain(key_file)[0]), hashes.SHA256(), default_backend()) with open(csr_file.name, "wb") as f: f.write(request.public_bytes(Encoding.PEM)) # Provision, writing to a temporary file. webroot = os.path.join(account_path, 'webroot') os.makedirs(webroot, exist_ok=True) with tempfile.TemporaryDirectory() as d: cert_file = os.path.join(d, 'cert_and_chain.pem') print("Provisioning TLS certificates for " + ", ".join(domain_list) + ".") certbotret = subprocess.check_output( [ "certbot", "certonly", #"-v", # just enough to see ACME errors "--non-interactive", # will fail if user hasn't registered during Mail-in-a-Box setup "-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 "--chain-path", os.path.join( d, 'chain'), # we only use the full chain "--fullchain-path", cert_file, "--webroot", "--webroot-path", webroot, "--config-dir", account_path, #"--staging", ], stderr=subprocess.STDOUT).decode("utf8") install_cert_copy_file(cert_file, env) ret[-1]["log"].append(certbotret) ret[-1]["result"] = "installed" except subprocess.CalledProcessError as e: ret[-1]["log"].append(e.output.decode("utf8")) ret[-1]["result"] = "error" except Exception as e: ret[-1]["log"].append(str(e)) ret[-1]["result"] = "error" # Run post-install steps. ret.extend(post_install_func(env)) # Return what happened with each certificate request. return ret