1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2024-11-26 02:57:04 +00:00

provision and install free SSL certificates from Let's Encrypt

This commit is contained in:
Joshua Tauberer 2015-10-10 22:03:55 +00:00
parent 5033042b8c
commit b6933a73fa
7 changed files with 408 additions and 79 deletions

View File

@ -1,6 +1,8 @@
## $HOSTNAME ## $HOSTNAME
# Redirect all HTTP to HTTPS. # Redirect all HTTP to HTTPS *except* the ACME challenges (Let's Encrypt SSL certificate
# domain validation challenges) path, which must be served over HTTP per the ACME spec
# (due to some Apache vulnerability).
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
@ -12,10 +14,19 @@ server {
# error pages and in the "Server" HTTP-Header. # error pages and in the "Server" HTTP-Header.
server_tokens off; server_tokens off;
location / {
# Redirect using the 'return' directive and the built-in # Redirect using the 'return' directive and the built-in
# variable '$request_uri' to avoid any capturing, matching # variable '$request_uri' to avoid any capturing, matching
# or evaluation of regular expressions. # or evaluation of regular expressions.
return 301 https://$HOSTNAME$request_uri; return 301 https://$HOSTNAME$request_uri;
}
location /.well-known/acme-challenge/ {
# This path must be served over HTTP for ACME domain validation.
# We map this to a special path where our SSL cert provisioning
# tool knows to store challenge response files.
alias $STORAGE_ROOT/ssl/lets_encrypt/acme_challenges/;
}
} }
# The secure HTTPS server. # The secure HTTPS server.

View File

@ -4,5 +4,8 @@
# Take a backup. # Take a backup.
management/backup.py | management/email_administrator.py "Backup Status" management/backup.py | management/email_administrator.py "Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py --headless | management/email_administrator.py "Error Provisioning TLS Certificate"
# Run status checks and email the administrator if anything changed. # Run status checks and email the administrator if anything changed.
management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice" management/status_checks.py --show-changes | management/email_administrator.py "Status Checks Change Notice"

388
management/ssl_certificates.py Normal file → Executable file
View File

@ -1,8 +1,11 @@
#!/usr/bin/python3
# Utilities for installing and selecting SSL certificates. # Utilities for installing and selecting SSL certificates.
import os, os.path, re, shutil import os, os.path, re, shutil
from utils import shell, safe_domain_name from utils import shell, safe_domain_name, sort_domains
# SELECTING SSL CERTIFICATES FOR USE IN WEB
def get_ssl_certificates(env): def get_ssl_certificates(env):
# Scan all of the installed SSL certificates and map every domain # Scan all of the installed SSL certificates and map every domain
@ -17,6 +20,8 @@ def get_ssl_certificates(env):
# List all of the files in the SSL directory and one level deep. # List all of the files in the SSL directory and one level deep.
def get_file_list(): def get_file_list():
if not os.path.exists(ssl_root):
return
for fn in os.listdir(ssl_root): for fn in os.listdir(ssl_root):
fn = os.path.join(ssl_root, fn) fn = os.path.join(ssl_root, fn)
if os.path.isfile(fn): if os.path.isfile(fn):
@ -82,10 +87,27 @@ def get_ssl_certificates(env):
# prefer one that is not self-signed # prefer one that is not self-signed
cert.issuer != cert.subject, cert.issuer != cert.subject,
###########################################################
# The above lines ensure that valid certificates are chosen
# over invalid certificates. The lines below choose between
# multiple valid certificates available for this domain.
###########################################################
# prefer one with the expiration furthest into the future so # prefer one with the expiration furthest into the future so
# that we can easily rotate to new certs as we get them # that we can easily rotate to new certs as we get them
cert.not_valid_after, cert.not_valid_after,
###########################################################
# We always choose the certificate that is good for the
# longest period of time. This is important for how we
# provision certificates for Let's Encrypt. To ensure that
# we don't re-provision every night, we have to ensure that
# if we choose to provison a certificate that it will
# *actually* be used so the provisioning logic knows it
# doesn't still need to provision a certificate for the
# domain.
###########################################################
# in case a certificate is installed in multiple paths, # in case a certificate is installed in multiple paths,
# prefer the... lexicographically last one? # prefer the... lexicographically last one?
cert._filename, cert._filename,
@ -96,46 +118,348 @@ def get_ssl_certificates(env):
"private-key": cert._private_key._filename, "private-key": cert._private_key._filename,
"certificate": cert._filename, "certificate": cert._filename,
"primary-domain": cert._primary_domain, "primary-domain": cert._primary_domain,
"certificate_object": cert,
} }
return ret return ret
def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False): def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False, raw=False):
# Get the default paths. # Get the system certificate info.
ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem')) ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem'))
ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
system_certificate = {
"private-key": ssl_private_key,
"certificate": ssl_certificate,
"primary-domain": env['PRIMARY_HOSTNAME'],
"certificate_object": load_pem(load_cert_chain(ssl_certificate)[0]),
}
if domain == env['PRIMARY_HOSTNAME']: if domain == env['PRIMARY_HOSTNAME']:
# The primary domain must use the server certificate because # The primary domain must use the server certificate because
# it is hard-coded in some service configuration files. # it is hard-coded in some service configuration files.
return ssl_private_key, ssl_certificate, None return system_certificate
wildcard_domain = re.sub("^[^\.]+", "*", domain) wildcard_domain = re.sub("^[^\.]+", "*", domain)
if domain in ssl_certificates: if domain in ssl_certificates:
cert_info = ssl_certificates[domain] return ssl_certificates[domain]
cert_type = "multi-domain"
elif wildcard_domain in ssl_certificates: elif wildcard_domain in ssl_certificates:
cert_info = ssl_certificates[wildcard_domain] return ssl_certificates[wildcard_domain]
cert_type = "wildcard"
elif not allow_missing_cert: elif not allow_missing_cert:
# No certificate is available for this domain! Return default files. # No valid certificate is available for this domain! Return default files.
ssl_via = "Using certificate for %s." % env['PRIMARY_HOSTNAME'] return system_certificate
return ssl_private_key, ssl_certificate, ssl_via
else: else:
# No certificate is available - and warn appropriately. # No valid certificate is available for this domain.
return None return None
# 'via' is a hint to the user about which certificate is in use for the domain
if cert_info['certificate'] == os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'):
# Using the server certificate.
via = "Using same %s certificate as for %s." % (cert_type, env['PRIMARY_HOSTNAME'])
elif cert_info['primary-domain'] != domain and cert_info['primary-domain'] in ssl_certificates and cert_info == ssl_certificates[cert_info['primary-domain']]:
via = "Using same %s certificate as for %s." % (cert_type, cert_info['primary-domain'])
else:
via = None # don't show a hint - show expiration info instead
return cert_info['private-key'], cert_info['certificate'], via # PROVISIONING CERTIFICATES FROM LETSENCRYPT
def get_certificates_to_provision(env):
# Get a list of domain names that we should now provision certificates
# for. Provision if a domain name has no valid certificate or if any
# certificate is expiring in 14 days. If provisioning anything, also
# provision certificates expiring within 30 days. The period between
# 14 and 30 days allows us to consolidate domains into multi-domain
# certificates for domains expiring around the same time.
from web_update import get_web_domains
import datetime
now = datetime.datetime.utcnow()
# Get domains with missing & expiring certificates.
certs = get_ssl_certificates(env)
domains = set()
domains_if_any = set()
for domain in get_web_domains(env):
try:
cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True)
except FileNotFoundError:
# system certificate is not present
continue
if cert is None:
# No valid certificate available.
domains.add(domain)
else:
cert = cert["certificate_object"]
if cert.issuer == cert.subject:
# This is self-signed. Get a real one.
domains.add(domain)
# Valid certificate today, but is it expiring soon?
elif cert.not_valid_after-now < datetime.timedelta(days=14):
domains.add(domain)
elif cert.not_valid_after-now < datetime.timedelta(days=30):
domains_if_any.add(domain)
# Filter out domains that don't have correct DNS, because then the CA
# won't be able to do DNS validation.
def is_domain_dns_correct(domain):
# Must make qname absolute to prevent a fall-back lookup with a
# search domain appended.
import dns.resolver
try:
response = dns.resolver.query(domain + ".", "A")
except:
return False
return len(response) == 1 and str(response[0]) == env["PUBLIC_IP"]
domains = set(d for d in domains if is_domain_dns_correct(d))
domains_if_any = set(d for d in domains_if_any if is_domain_dns_correct(d))
# If there are any domains we definitely will provision for, add in
# additional domains to do at this time.
if len(domains) > 0:
domains |= domains_if_any
# Sort, just to keep related domain names together in the next step.
domains = sort_domains(domains, env)
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
# limit for a single certificate.
cert_domains = []
while len(domains) > 0:
cert_domains.append( domains[0:100] )
domains = domains[100:]
# Return a list of lists of domain names.
return cert_domains
def provision_certificates(env, agree_to_tos_url=None, logger=None):
import requests.exceptions
import acme.messages
from free_tls_certificates import client
# What domains to provision certificates for? This is a list of
# lists of domains.
certs = get_certificates_to_provision(env)
if len(certs) == 0:
return {
"requests": [],
}
# 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)
# Where should we put ACME challenge files. This is mapped to /.well-known/acme_challenge
# by the nginx configuration.
challenges_path = os.path.join(account_path, 'acme_challenges')
if not os.path.exists(challenges_path):
os.mkdir(challenges_path)
# Read in the private key that we use for all TLS certificates. We'll need that
# to generate a CSR (done by free_tls_certificates).
with open(os.path.join(env['STORAGE_ROOT'], 'ssl/ssl_private_key.pem'), 'rb') as f:
private_key = f.read()
# Provision certificates.
ret = []
for domain_list in certs:
# For return.
ret_item = {
"domains": domain_list,
"log": [],
}
ret.append(ret_item)
# Logging for free_tls_certificates.
def my_logger(message):
if logger: logger(message)
ret_item["log"].append(message)
# Attempt to provision a certificate.
try:
try:
cert = client.issue_certificate(
domain_list,
account_path,
agree_to_tos_url=agree_to_tos_url,
private_key=private_key,
logger=my_logger)
except client.NeedToTakeAction as e:
# Write out the ACME challenge files.
for action in e.actions:
if isinstance(action, client.NeedToInstallFile):
fn = os.path.join(challenges_path, action.file_name)
with open(fn, 'w') as f:
f.write(action.contents)
else:
raise ValueError(str(action))
# Try to provision now that the challenge files are installed.
cert = client.issue_certificate(
domain_list,
account_path,
private_key=private_key,
logger=my_logger)
except client.NeedToAgreeToTOS as e:
# The user must agree to the Let's Encrypt terms of service agreement
# before any further action can be taken.
ret_item.update({
"result": "agree-to-tos",
"url": e.url,
})
except client.WaitABit as e:
# We need to hold on for a bit before querying again to see if we can
# acquire a provisioned certificate.
import time, datetime
ret_item.update({
"result": "wait",
"until": e.until_when, #.isoformat(),
"seconds": (e.until_when - datetime.datetime.now()).total_seconds()
})
except client.AccountDataIsCorrupt as e:
# This is an extremely rare condition.
ret_item.update({
"result": "error",
"message": "Something unexpected went wrong. It looks like your local Let's Encrypt account data is corrupted. There was a problem with the file " + e.account_file_path + ".",
})
except (client.InvalidDomainName, client.NeedToTakeAction, acme.messages.Error, requests.exceptions.RequestException) as e:
ret_item.update({
"result": "error",
"message": "Something unexpected went wrong: " + str(e),
})
else:
# A certificate was issued.
install_status = install_cert(domain_list[0], cert['cert'].decode("ascii"), b"\n".join(cert['chain']).decode("ascii"), env, raw=True)
# str indicates the certificate was not installed.
if isinstance(install_status, str):
ret_item.update({
"result": "error",
"message": "Something unexpected was wrong with the provisioned certificate: " + install_status,
})
else:
# A list indicates success and what happened next.
ret_item["log"].extend(install_status)
ret_item.update({
"result": "installed",
})
# Return what happened with each certificate request.
return {
"requests": ret
}
def provision_certificates_cmdline():
import sys
from utils import load_environment, exclusive_process
exclusive_process("update_tls_certificates")
env = load_environment()
agree_to_tos_url = None
while True:
# Run the provisioning script. This installs certificates. If there are
# a very large number of domains on this box, it issues separate
# certificates for groups of domains. We have to check the result for
# each group.
def my_logger(message):
if "-v" in sys.argv:
print(">", message)
status = provision_certificates(env, agree_to_tos_url=agree_to_tos_url, logger=my_logger)
agree_to_tos_url = None # reset to prevent infinite looping
if not status["requests"]:
# No domains need certificates.
if "--headless" not in sys.argv or "-v" in sys.argv:
print("No domains hosted on this box need a certificate at this time.")
sys.exit(0)
# What happened?
wait_until = None
wait_domains = []
for request in status["requests"]:
if request["result"] == "agree-to-tos":
# We may have asked already in a previous iteration.
if agree_to_tos_url is not None:
continue
# Can't ask the user a question in this mode.
if "--headless" in sys.argv:
print("Can't issue TLS certficate until user has agreed to Let's Encrypt TOS.")
sys.exit(1)
print("""
I'm going to provision a TLS certificate (formerly called a SSL certificate)
for you from Let's Encrypt (letsencrypt.org).
TLS certificates are cryptographic keys that ensure communication between
you and this box are secure when getting and sending mail and visiting
websites hosted on this box. Let's Encrypt is a free provider of TLS
certificates.
Please open this document in your web browser:
%s
It is Let's Encrypt's terms of service agreement. If you agree, I can
provision that TLS certificate. If you don't agree, you will have an
opportunity to install your own TLS certificate from the Mail-in-a-Box
control panel.
Do you agree to the agreement? Type Y or N and press <ENTER>: """
% request["url"], end='', flush=True)
if sys.stdin.readline().strip().upper() != "Y":
print("\nYou didn't agree. Quitting.")
sys.exit(1)
# Okay, indicate agreement on next iteration.
agree_to_tos_url = request["url"]
if request["result"] == "wait":
# Must wait. We'll record until when. The wait occurs below.
if wait_until is None:
wait_until = request["until"]
else:
wait_until = max(wait_until, request["until"])
wait_domains += request["domains"]
if request["result"] == "error":
print(", ".join(request["domains"]) + ":")
print(request["message"])
if request["result"] == "installed":
print("A TLS certificate was successfully installed for " + ", ".join(request["domains"]) + ".")
if wait_until:
# Wait, then loop.
import time, datetime
print()
print("A TLS certificate was requested for: " + ", ".join(wait_domains) + ".")
first = True
while wait_until > datetime.datetime.now():
if "--headless" not in sys.argv or first:
print ("We have to wait", int(round((wait_until - datetime.datetime.now()).total_seconds())), "seconds for the certificate to be issued...")
time.sleep(10)
first = False
continue # Loop!
if agree_to_tos_url:
# The user agrees to the TOS. Loop to try again by agreeing.
continue # Loop!
# Unless we were instructed to wait, or we just agreed to the TOS,
# we're done for now.
break
# INSTALLING A NEW CERTIFICATE FROM THE CONTROL PANEL
def create_csr(domain, ssl_key, country_code, env): def create_csr(domain, ssl_key, country_code, env):
return shell("check_output", [ return shell("check_output", [
@ -144,7 +468,7 @@ def create_csr(domain, ssl_key, country_code, env):
"-sha256", "-sha256",
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)]) "-subj", "/C=%s/ST=/L=/O=/CN=%s" % (country_code, domain)])
def install_cert(domain, ssl_cert, ssl_chain, env): 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. # Write the combined cert+chain to a temporary path and validate that it is OK.
# The certificate always goes above the chain. # The certificate always goes above the chain.
import tempfile import tempfile
@ -203,8 +527,10 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
# Update the web configuration so nginx picks up the new certificate file. # Update the web configuration so nginx picks up the new certificate file.
from web_update import do_web_update from web_update import do_web_update
ret.append( do_web_update(env) ) ret.append( do_web_update(env) )
if raw: return ret
return "\n".join(ret) return "\n".join(ret)
# VALIDATION OF CERTIFICATES
def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False): def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring_soon=True, rounded_time=False, just_check_domain=False):
# Check that the ssl_certificate & ssl_private_key files are good # Check that the ssl_certificate & ssl_private_key files are good
@ -305,16 +631,16 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# But is it expiring soon? # But is it expiring soon?
cert_expiration_date = cert.not_valid_after cert_expiration_date = cert.not_valid_after
ndays = (cert_expiration_date-now).days ndays = (cert_expiration_date-now).days
if not rounded_time or ndays < 7: if not rounded_time or ndays <= 10:
# Yikes better renew soon!
expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x")) expiry_info = "The certificate expires in %d days on %s." % (ndays, cert_expiration_date.strftime("%x"))
elif ndays <= 14:
expiry_info = "The certificate expires in less than two weeks, on %s." % cert_expiration_date.strftime("%x")
elif ndays <= 31:
expiry_info = "The certificate expires in less than a month, on %s." % cert_expiration_date.strftime("%x")
else: else:
# We'll renew it with Lets Encrypt.
expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x") expiry_info = "The certificate expires on %s." % cert_expiration_date.strftime("%x")
if ndays <= 31 and warn_if_expiring_soon: if ndays <= 10 and warn_if_expiring_soon:
# Warn on day 10 to give 4 days for us to automatically renew the
# certificate, which occurs on day 14.
return ("The certificate is expiring soon: " + expiry_info, None) return ("The certificate is expiring soon: " + expiry_info, None)
# Return the special OK code. # Return the special OK code.
@ -381,3 +707,7 @@ def get_certificate_domains(cert):
pass pass
return names, cn return names, cn
if __name__ == "__main__":
# Provision certificates.
provision_certificates_cmdline()

View File

@ -278,23 +278,24 @@ def run_domain_checks(rounded_time, env, output, pool):
# Get the list of domains that we don't serve web for because of a custom CNAME/A record. # 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) domains_with_a_records = get_domains_with_a_records(env)
ssl_certificates = get_ssl_certificates(env)
# Serial version: # Serial version:
#for domain in sort_domains(domains_to_check, env): #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) # run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains)
# Parallelize the checks across a worker pool. # 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) args = ((domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records)
for domain in domains_to_check) for domain in domains_to_check)
ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1) ret = pool.starmap(run_domain_checks_on_domain, args, chunksize=1)
ret = dict(ret) # (domain, output) => { domain: output } ret = dict(ret) # (domain, output) => { domain: output }
for domain in sort_domains(ret, env): for domain in sort_domains(ret, env):
ret[domain].playback(output) ret[domain].playback(output)
def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records, ssl_certificates): def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zonefiles, mail_domains, web_domains, domains_with_a_records):
output = BufferedOutput() output = BufferedOutput()
# we'd move this up, but this returns non-pickleable values
ssl_certificates = get_ssl_certificates(env)
# The domain is IDNA-encoded in the database, but for display use Unicode. # The domain is IDNA-encoded in the database, but for display use Unicode.
try: try:
domain_display = idna.decode(domain.encode('ascii')) domain_display = idna.decode(domain.encode('ascii'))
@ -656,45 +657,28 @@ def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return if query_dns(domain, "A", None) not in (env['PUBLIC_IP'], None): return
# Where is the SSL stored? # Where is the SSL stored?
x = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True) tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
if tls_cert is None:
if x is None:
output.print_warning("""No SSL certificate is installed for this domain. Visitors to a website on output.print_warning("""No SSL certificate is installed for this domain. Visitors to a website on
this domain will get a security warning. If you are not serving a website on this domain, you do this domain will get a security warning. If you are not serving a website on this domain, you do
not need to take any action. Use the SSL Certificates page in the control panel to install a not need to take any action. Use the SSL Certificates page in the control panel to install a
SSL certificate.""") SSL certificate.""")
return return
ssl_key, ssl_certificate, ssl_via = x
# Check that the certificate is good. # Check that the certificate is good.
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key, rounded_time=rounded_time) cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"], rounded_time=rounded_time)
if cert_status == "OK": if cert_status == "OK":
# The certificate is ok. The details has expiry info. # The certificate is ok. The details has expiry info.
output.print_ok("SSL certificate is signed & valid. %s %s" % (ssl_via if ssl_via else "", cert_status_details)) output.print_ok("SSL certificate is signed & valid. " + cert_status_details)
elif cert_status == "SELF-SIGNED": elif cert_status == "SELF-SIGNED":
# Offer instructions for purchasing a signed certificate. # Offer instructions for purchasing a signed certificate.
fingerprint = shell('check_output', [
"openssl",
"x509",
"-in", ssl_certificate,
"-noout",
"-fingerprint"
])
fingerprint = re.sub(".*Fingerprint=", "", fingerprint).strip()
if domain == env['PRIMARY_HOSTNAME']: if domain == env['PRIMARY_HOSTNAME']:
output.print_error("""The SSL certificate for this domain is currently self-signed. You will get a security output.print_error("""The SSL certificate for this domain is currently self-signed. You will get a security
warning when you check or send email and when visiting this domain in a web browser (for webmail or warning when you check or send email and when visiting this domain in a web browser (for webmail or
static site hosting). Use the SSL Certificates page in the control panel to install a signed SSL certificate. static site hosting).""")
You may choose to leave the self-signed certificate in place and confirm the security exception, but check that
the certificate fingerprint matches the following:""")
output.print_line("")
output.print_line(" " + fingerprint, monospace=True)
else: else:
output.print_error("""The SSL certificate for this domain is self-signed.""") output.print_error("""The SSL certificate for this domain is self-signed.""")
@ -927,10 +911,10 @@ if __name__ == "__main__":
if query_dns(domain, "A") != env['PUBLIC_IP']: if query_dns(domain, "A") != env['PUBLIC_IP']:
sys.exit(1) sys.exit(1)
ssl_certificates = get_ssl_certificates(env) ssl_certificates = get_ssl_certificates(env)
ssl_key, ssl_certificate, ssl_via = get_domain_ssl_files(domain, ssl_certificates, env) tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
if not os.path.exists(ssl_certificate): if not os.path.exists(tls_cert["certificate"]):
sys.exit(1) sys.exit(1)
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key, warn_if_expiring_soon=False) cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"], warn_if_expiring_soon=False)
if cert_status != "OK": if cert_status != "OK":
sys.exit(1) sys.exit(1)
sys.exit(0) sys.exit(0)

View File

@ -119,7 +119,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
root = get_web_root(domain, env) root = get_web_root(domain, env)
# What private key and SSL certificate will we use for this domain? # 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) tls_cert = get_domain_ssl_files(domain, ssl_certificates, env)
# ADDITIONAL DIRECTIVES. # ADDITIONAL DIRECTIVES.
@ -136,7 +136,7 @@ def make_domain_config(domain, templates, ssl_certificates, env):
finally: finally:
f.close() f.close()
return sha1.hexdigest() return sha1.hexdigest()
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate)) 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. # Add in any user customizations in YAML format.
hsts = "yes" hsts = "yes"
@ -177,8 +177,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT']) nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
nginx_conf = nginx_conf.replace("$HOSTNAME", domain) nginx_conf = nginx_conf.replace("$HOSTNAME", domain)
nginx_conf = nginx_conf.replace("$ROOT", root) nginx_conf = nginx_conf.replace("$ROOT", root)
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) nginx_conf = nginx_conf.replace("$SSL_KEY", tls_cert["private-key"])
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) 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 nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
return nginx_conf return nginx_conf
@ -193,20 +193,15 @@ def get_web_root(domain, env, test_exists=True):
def get_web_domains_info(env): def get_web_domains_info(env):
www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False)) www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False))
has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env)) has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env))
ssl_certificates = get_ssl_certificates(env)
# for the SSL config panel, get cert status # for the SSL config panel, get cert status
def check_cert(domain): def check_cert(domain):
ssl_certificates = get_ssl_certificates(env) tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
x = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True) if tls_cert is None: return ("danger", "No Certificate Installed")
if x is None: return ("danger", "No Certificate Installed") cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"])
ssl_key, ssl_certificate, ssl_via = x
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
if cert_status == "OK": if cert_status == "OK":
if not ssl_via:
return ("success", "Signed & valid. " + cert_status_details) 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. " + ssl_via)
elif cert_status == "SELF-SIGNED": elif cert_status == "SELF-SIGNED":
return ("warning", "Self-signed. Get a signed certificate to stop warnings.") return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
else: else:

View File

@ -11,8 +11,11 @@ if [ -f /usr/local/lib/python2.7/dist-packages/boto/__init__.py ]; then hide_out
# build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography. # build-essential libssl-dev libffi-dev python3-dev: Required to pip install cryptography.
apt_install python3-flask links duplicity python-boto libyaml-dev python3-dnspython python3-dateutil \ apt_install python3-flask links duplicity python-boto libyaml-dev python3-dnspython python3-dateutil \
build-essential libssl-dev libffi-dev python3-dev python-pip build-essential libssl-dev libffi-dev python3-dev python-pip
hide_output pip3 install --upgrade rtyaml "email_validator>=1.0.0" "idna>=2.0.0" "cryptography>=1.0.2" boto psutil
# Install other Python packages. The first line is the packages that Josh maintains himself!
hide_output pip3 install --upgrade \
rtyaml "email_validator>=1.0.0" free_tls_certificates \
"idna>=2.0.0" "cryptography>=1.0.2" boto psutil
# email_validator is repeated in setup/questions.sh # email_validator is repeated in setup/questions.sh
# Create a backup directory and a random key for encrypting backups. # Create a backup directory and a random key for encrypting backups.
@ -44,5 +47,5 @@ cat > /etc/cron.d/mailinabox-nightly << EOF;
0 3 * * * root (cd `pwd` && management/daily_tasks.sh) 0 3 * * * root (cd `pwd` && management/daily_tasks.sh)
EOF EOF
# Start it. # Start the management server.
restart_service mailinabox restart_service mailinabox

View File

@ -116,6 +116,9 @@ done
tools/dns_update tools/dns_update
tools/web_update tools/web_update
# If DNS is already working, try to provision TLS certficates from Let's Encrypt.
management/ssl_certificates.py
# If there aren't any mail users yet, create one. # If there aren't any mail users yet, create one.
source setup/firstuser.sh source setup/firstuser.sh