mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-22 02:17:26 +00:00
Merge branch 'letsencrypt' for automatic provisioning of TLS certificates from Let's Encrypt
This commit is contained in:
commit
07f9228694
@ -10,11 +10,13 @@ Mail:
|
|||||||
|
|
||||||
Control Panel:
|
Control Panel:
|
||||||
|
|
||||||
|
* The SSL (now "TLS") certificates page now supports provisioning free SSL certificates from Let's Encrypt.
|
||||||
* Report free memory usage.
|
* Report free memory usage.
|
||||||
|
|
||||||
System:
|
System:
|
||||||
|
|
||||||
* The daily backup will now email the administrator if there is a problem.
|
* The daily backup will now email the administrator if there is a problem.
|
||||||
|
* Expiring TLS (SSL) certificates are now automatically renewed via Let's Encrypt.
|
||||||
|
|
||||||
v0.15a (January 9, 2016)
|
v0.15a (January 9, 2016)
|
||||||
------------------------
|
------------------------
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
## $HOSTNAME
|
## $HOSTNAME
|
||||||
|
|
||||||
# Redirect all HTTP to HTTPS.
|
# Redirect all HTTP to HTTPS *except* the ACME challenges (Let's Encrypt TLS 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,12 +14,21 @@ 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 TLS 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.
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
|
@ -327,6 +327,33 @@ def dns_get_dump():
|
|||||||
|
|
||||||
# SSL
|
# SSL
|
||||||
|
|
||||||
|
@app.route('/ssl/status')
|
||||||
|
@authorized_personnel_only
|
||||||
|
def ssl_get_status():
|
||||||
|
from ssl_certificates import get_certificates_to_provision
|
||||||
|
from web_update import get_web_domains_info, get_web_domains
|
||||||
|
|
||||||
|
# What domains can we provision certificates for? What unexpected problems do we have?
|
||||||
|
provision, cant_provision = get_certificates_to_provision(env, show_extended_problems=False)
|
||||||
|
|
||||||
|
# What's the current status of TLS certificates on all of the domain?
|
||||||
|
domains_status = get_web_domains_info(env)
|
||||||
|
domains_status = [{ "domain": d["domain"], "status": d["ssl_certificate"][0], "text": d["ssl_certificate"][1] } for d in domains_status ]
|
||||||
|
|
||||||
|
# Warn the user about domain names not hosted here because of other settings.
|
||||||
|
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
|
||||||
|
domains_status.append({
|
||||||
|
"domain": domain,
|
||||||
|
"status": "not-applicable",
|
||||||
|
"text": "The domain's website is hosted elsewhere.",
|
||||||
|
})
|
||||||
|
|
||||||
|
return json_response({
|
||||||
|
"can_provision": utils.sort_domains(provision, env),
|
||||||
|
"cant_provision": [{ "domain": domain, "problem": cant_provision[domain] } for domain in utils.sort_domains(cant_provision, env) ],
|
||||||
|
"status": domains_status,
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def ssl_get_csr(domain):
|
def ssl_get_csr(domain):
|
||||||
@ -346,6 +373,17 @@ def ssl_install_cert():
|
|||||||
return "Invalid domain name."
|
return "Invalid domain name."
|
||||||
return install_cert(domain, ssl_cert, ssl_chain, env)
|
return install_cert(domain, ssl_cert, ssl_chain, env)
|
||||||
|
|
||||||
|
@app.route('/ssl/provision', methods=['POST'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def ssl_provision_certs():
|
||||||
|
from ssl_certificates import provision_certificates
|
||||||
|
agree_to_tos_url = request.form.get('agree_to_tos_url')
|
||||||
|
status = provision_certificates(env,
|
||||||
|
agree_to_tos_url=agree_to_tos_url,
|
||||||
|
jsonable=True)
|
||||||
|
return json_response(status)
|
||||||
|
|
||||||
|
|
||||||
# WEB
|
# WEB
|
||||||
|
|
||||||
@app.route('/web/domains')
|
@app.route('/web/domains')
|
||||||
|
@ -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"
|
||||||
|
461
management/ssl_certificates.py
Normal file → Executable file
461
management/ssl_certificates.py
Normal file → Executable file
@ -1,8 +1,13 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
import idna
|
||||||
|
|
||||||
|
# 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 +22,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 +89,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 +120,417 @@ 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, show_extended_problems=True, force_domains=None):
|
||||||
|
# Get a set 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()
|
||||||
|
problems = { }
|
||||||
|
for domain in get_web_domains(env):
|
||||||
|
# If the user really wants a cert for certain domains, include it.
|
||||||
|
if force_domains:
|
||||||
|
if force_domains == "ALL" or (isinstance(force_domains, list) and domain in force_domains):
|
||||||
|
domains.add(domain)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Include this domain if its certificate is missing, self-signed, or expiring soon.
|
||||||
|
try:
|
||||||
|
cert = get_domain_ssl_files(domain, certs, env, allow_missing_cert=True)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
# system certificate is not present
|
||||||
|
problems[domain] = "Error: " + str(e)
|
||||||
|
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)
|
||||||
|
|
||||||
|
# It's valid. Should we report its validness?
|
||||||
|
if show_extended_problems:
|
||||||
|
problems[domain] = "The certificate is valid for at least another 30 days --- no need to replace."
|
||||||
|
|
||||||
|
# Warn the user about domains hosted elsewhere.
|
||||||
|
if not force_domains and show_extended_problems:
|
||||||
|
for domain in set(get_web_domains(env, exclude_dns_elsewhere=False)) - set(get_web_domains(env)):
|
||||||
|
problems[domain] = "The domain's DNS is pointed elsewhere, so there is no point to installing a TLS certificate here and we could not automatically provision one anyway because provisioning requires access to the website (which isn't here)."
|
||||||
|
|
||||||
|
# Filter out domains that we can't provision a certificate for.
|
||||||
|
def can_provision_for_domain(domain):
|
||||||
|
# Let's Encrypt doesn't yet support IDNA domains.
|
||||||
|
# We store domains in IDNA (ASCII). To see if this domain is IDNA,
|
||||||
|
# we'll see if its IDNA-decoded form is different.
|
||||||
|
if idna.decode(domain.encode("ascii")) != domain:
|
||||||
|
problems[domain] = "Let's Encrypt does not yet support provisioning certificates for internationalized domains."
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the domain resolve to this machine in public DNS? If not,
|
||||||
|
# we can't do domain control validation. For IPv6 is configured,
|
||||||
|
# make sure both IPv4 and IPv6 are correct because we don't know
|
||||||
|
# how Let's Encrypt will connect.
|
||||||
|
import dns.resolver
|
||||||
|
for rtype, value in [("A", env["PUBLIC_IP"]), ("AAAA", env.get("PUBLIC_IPV6"))]:
|
||||||
|
if not value: continue # IPv6 is not configured
|
||||||
|
try:
|
||||||
|
# Must make the qname absolute to prevent a fall-back lookup with a
|
||||||
|
# search domain appended, by adding a period to the end.
|
||||||
|
response = dns.resolver.query(domain + ".", rtype)
|
||||||
|
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
|
||||||
|
problems[domain] = "DNS isn't configured properly for this domain: DNS resolution failed (%s: %s)." % (rtype, str(e) or repr(e)) # NoAnswer's str is empty
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
problems[domain] = "DNS isn't configured properly for this domain: DNS lookup had an error: %s." % str(e)
|
||||||
|
return False
|
||||||
|
if len(response) != 1 or str(response[0]) != value:
|
||||||
|
problems[domain] = "Domain control validation cannot be performed for this domain because DNS points the domain to another machine (%s %s)." % (rtype, ", ".join(str(r) for r in response))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
domains = set(filter(can_provision_for_domain, domains))
|
||||||
|
|
||||||
|
# If there are any domains we definitely will provision for, add in
|
||||||
|
# additional domains to do at this time.
|
||||||
|
if len(domains) > 0:
|
||||||
|
domains |= set(filter(can_provision_for_domain, domains_if_any))
|
||||||
|
|
||||||
|
return (domains, problems)
|
||||||
|
|
||||||
|
def provision_certificates(env, agree_to_tos_url=None, logger=None, force_domains=None, jsonable=False):
|
||||||
|
import requests.exceptions
|
||||||
|
import acme.messages
|
||||||
|
|
||||||
|
from free_tls_certificates import client
|
||||||
|
|
||||||
|
# What domains should we provision certificates for? And what
|
||||||
|
# errors prevent provisioning for other domains.
|
||||||
|
domains, problems = get_certificates_to_provision(env, force_domains=force_domains)
|
||||||
|
|
||||||
|
# Exit fast if there is nothing to do.
|
||||||
|
if len(domains) == 0:
|
||||||
|
return {
|
||||||
|
"requests": [],
|
||||||
|
"problems": problems,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Break into groups of up to 100 certificates at a time, which is Let's Encrypt's
|
||||||
|
# limit for a single certificate. We'll sort to put related domains together.
|
||||||
|
domains = sort_domains(domains, env)
|
||||||
|
certs = []
|
||||||
|
while len(domains) > 0:
|
||||||
|
certs.append( domains[0:100] )
|
||||||
|
domains = domains[100:]
|
||||||
|
|
||||||
|
# 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 if not jsonable else 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,
|
||||||
|
"problems": problems,
|
||||||
|
}
|
||||||
|
|
||||||
|
def provision_certificates_cmdline():
|
||||||
|
import sys
|
||||||
|
from utils import load_environment, exclusive_process
|
||||||
|
|
||||||
|
exclusive_process("update_tls_certificates")
|
||||||
|
env = load_environment()
|
||||||
|
|
||||||
|
verbose = False
|
||||||
|
headless = False
|
||||||
|
force_domains = None
|
||||||
|
|
||||||
|
args = list(sys.argv)
|
||||||
|
args.pop(0) # program name
|
||||||
|
if args and args[0] == "-v":
|
||||||
|
verbose = True
|
||||||
|
args.pop(0)
|
||||||
|
if args and args[0] == "--headless":
|
||||||
|
headless = True
|
||||||
|
args.pop(0)
|
||||||
|
if args and args[0] == "--force":
|
||||||
|
force_domains = "ALL"
|
||||||
|
args.pop(0)
|
||||||
|
else:
|
||||||
|
force_domains = args
|
||||||
|
|
||||||
|
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 verbose:
|
||||||
|
print(">", message)
|
||||||
|
status = provision_certificates(env, agree_to_tos_url=agree_to_tos_url, logger=my_logger, force_domains=force_domains)
|
||||||
|
agree_to_tos_url = None # reset to prevent infinite looping
|
||||||
|
|
||||||
|
if not status["requests"]:
|
||||||
|
# No domains need certificates.
|
||||||
|
if not headless or verbose:
|
||||||
|
if len(status["problems"]) == 0:
|
||||||
|
print("No domains hosted on this box need a new TLS certificate at this time.")
|
||||||
|
elif len(status["problems"]) > 0:
|
||||||
|
print("No TLS certificates could be provisoned at this time:")
|
||||||
|
print()
|
||||||
|
for domain in sort_domains(status["problems"], env):
|
||||||
|
print("%s: %s" % (domain, status["problems"][domain]))
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# And finally show the domains with problems.
|
||||||
|
if len(status["problems"]) > 0:
|
||||||
|
print("TLS certificates could not be provisoned for:")
|
||||||
|
for domain in sort_domains(status["problems"], env):
|
||||||
|
print("%s: %s" % (domain, status["problems"][domain]))
|
||||||
|
|
||||||
|
# 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 +539,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 +598,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
|
||||||
@ -296,7 +693,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
|||||||
return ("The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)" % verifyoutput, None)
|
return ("The certificate is missing an intermediate chain or the intermediate chain is incorrect or incomplete. (%s)" % verifyoutput, None)
|
||||||
|
|
||||||
# There is some unknown problem. Return the `openssl verify` raw output.
|
# There is some unknown problem. Return the `openssl verify` raw output.
|
||||||
return ("There is a problem with the SSL certificate.", verifyoutput.strip())
|
return ("There is a problem with the certificate.", verifyoutput.strip())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# `openssl verify` returned a zero exit status so the cert is currently
|
# `openssl verify` returned a zero exit status so the cert is currently
|
||||||
@ -305,16 +702,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 +778,7 @@ def get_certificate_domains(cert):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
return names, cn
|
return names, cn
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Provision certificates.
|
||||||
|
provision_certificates_cmdline()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
#
|
#
|
||||||
# Checks that the upstream DNS has been set correctly and that
|
# Checks that the upstream DNS has been set correctly and that
|
||||||
# SSL certificates have been signed, etc., and if not tells the user
|
# TLS certificates have been signed, etc., and if not tells the user
|
||||||
# what to do next.
|
# what to do next.
|
||||||
|
|
||||||
import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
import sys, os, os.path, re, subprocess, datetime, multiprocessing.pool
|
||||||
@ -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'))
|
||||||
@ -600,15 +601,23 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output):
|
|||||||
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and
|
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and
|
||||||
# other domains, it is required to access its website.
|
# other domains, it is required to access its website.
|
||||||
if domain != env['PRIMARY_HOSTNAME']:
|
if domain != env['PRIMARY_HOSTNAME']:
|
||||||
ip = query_dns(domain, "A")
|
ok_values = []
|
||||||
if ip == env['PUBLIC_IP']:
|
for (rtype, expected) in (("A", env['PUBLIC_IP']), ("AAAA", env.get('PUBLIC_IPV6'))):
|
||||||
output.print_ok("Domain resolves to this box's IP address. [%s ↦ %s]" % (domain, env['PUBLIC_IP']))
|
if not expected: continue # IPv6 is not configured
|
||||||
|
value = query_dns(domain, rtype)
|
||||||
|
if value == expected:
|
||||||
|
ok_values.append(value)
|
||||||
else:
|
else:
|
||||||
output.print_error("""This domain should resolve to your box's IP address (%s) if you would like the box to serve
|
output.print_error("""This domain should resolve to your box's IP address (%s %s) if you would like the box to serve
|
||||||
webmail or a website on this domain. The domain currently resolves to %s in public DNS. It may take several hours for
|
webmail or a website on this domain. The domain currently resolves to %s in public DNS. It may take several hours for
|
||||||
public DNS to update after a change. This problem may result from other issues listed here.""" % (env['PUBLIC_IP'], ip))
|
public DNS to update after a change. This problem may result from other issues listed here.""" % (rtype, expected, value))
|
||||||
|
return
|
||||||
|
|
||||||
# We need a SSL certificate for PRIMARY_HOSTNAME because that's where the
|
# If both A and AAAA are correct...
|
||||||
|
output.print_ok("Domain resolves to this box's IP address. [%s ↦ %s]" % (domain, '; '.join(ok_values)))
|
||||||
|
|
||||||
|
|
||||||
|
# We need a TLS certificate for PRIMARY_HOSTNAME because that's where the
|
||||||
# user will log in with IMAP or webmail. Any other domain we serve a
|
# user will log in with IMAP or webmail. Any other domain we serve a
|
||||||
# website for also needs a signed certificate.
|
# website for also needs a signed certificate.
|
||||||
check_ssl_cert(domain, rounded_time, ssl_certificates, env, output)
|
check_ssl_cert(domain, rounded_time, ssl_certificates, env, output)
|
||||||
@ -650,56 +659,39 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None):
|
|||||||
return "; ".join(sorted(str(r).rstrip('.') for r in response))
|
return "; ".join(sorted(str(r).rstrip('.') for r in response))
|
||||||
|
|
||||||
def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
|
def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
|
||||||
# Check that SSL certificate is signed.
|
# Check that TLS certificate is signed.
|
||||||
|
|
||||||
# Skip the check if the A record is not pointed here.
|
# Skip the check if the A record is not pointed here.
|
||||||
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 certificate file 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 TLS (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 TLS Certificates page in the control panel to install a
|
||||||
SSL certificate.""")
|
TLS 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("TLS (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 TLS (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 TLS (SSL) certificate for this domain is self-signed.""")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
output.print_error("The SSL certificate has a problem: " + cert_status)
|
output.print_error("The TLS (SSL) certificate has a problem: " + cert_status)
|
||||||
if cert_status_details:
|
if cert_status_details:
|
||||||
output.print_line("")
|
output.print_line("")
|
||||||
output.print_line(cert_status_details)
|
output.print_line(cert_status_details)
|
||||||
@ -927,10 +919,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)
|
||||||
|
@ -87,7 +87,7 @@
|
|||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
|
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
|
||||||
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
|
<li><a href="#tls" onclick="return show_panel(this);">TLS (SSL) Certificates</a></li>
|
||||||
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Advanced Pages</li>
|
<li class="dropdown-header">Advanced Pages</li>
|
||||||
@ -155,7 +155,7 @@
|
|||||||
{% include "web.html" %}
|
{% include "web.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="panel_ssl" class="admin_panel">
|
<div id="panel_tls" class="admin_panel">
|
||||||
{% include "ssl.html" %}
|
{% include "ssl.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,12 +1,46 @@
|
|||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<h2>SSL Certificates</h2>
|
<h2>TLS (SSL) Certificates</h2>
|
||||||
|
|
||||||
|
<p>A TLS (formerly called SSL) certificate is a cryptographic file that proves to anyone connecting to a web address that the connection is secure between you and the owner of that address.</p>
|
||||||
|
|
||||||
|
<p>You need a TLS certificate for this box’s hostname ({{hostname}}) and every other domain name and subdomain that this box is hosting a website for (see the list below).</p>
|
||||||
|
|
||||||
|
<div id="ssl_provision">
|
||||||
|
<h3>Provision a Certificate</h3>
|
||||||
|
|
||||||
|
<div id="ssl_provision_p" style="display: none; margin-top: 1.5em">
|
||||||
|
<button onclick='return provision_tls_cert();' class='btn btn-primary' style="float: left; margin: 0 1.5em 1em 0;">Provision</button>
|
||||||
|
<p>A TLS certificate can be automatically provisioned from <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>, a free TLS certificate provider, for:<br>
|
||||||
|
<span class="text-primary"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clearfix"> </div>
|
||||||
|
|
||||||
|
<div id="ssl_provision_result"></div>
|
||||||
|
|
||||||
|
<div id="ssl_provision_problems_div" style="display: none;">
|
||||||
|
<p style="margin-bottom: .5em;">Certificates cannot be automatically provisioned for:</p>
|
||||||
|
<table id="ssl_provision_problems" style="margin-top: 0;" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Problem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>Use the <em>Install Certificate</em> button below for these domains.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Certificate Status</h3>
|
<h3>Certificate Status</h3>
|
||||||
|
|
||||||
|
<p style="margin-top: 1.5em">Certificates expire after a period of time. All certificates will be automatically renewed through <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a> 14 days prior to expiration.</p>
|
||||||
|
|
||||||
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto;">
|
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto; display: none">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Domain</th>
|
<th>Domain</th>
|
||||||
@ -18,17 +52,18 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for.</p>
|
|
||||||
|
|
||||||
<h3 id="ssl_install_header">Install SSL Certificate</h3>
|
<h3 id="ssl_install_header">Install Certificate</h3>
|
||||||
|
|
||||||
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a> or <a href="https://buy.wosign.com/free/">WoSign’s free SSL</a></a>.</p>
|
<p>There are many places where you can get a free or cheap certificate. We recommend <a href="https://www.namecheap.com/security/ssl-certificates/domain-validation.aspx">Namecheap’s $9 certificate</a>, <a href="https://www.startssl.com/">StartSSL’s free express lane</a> or <a href="https://buy.wosign.com/free/">WoSign’s free TLS</a></a>.</p>
|
||||||
|
|
||||||
<p>Which domain are you getting an SSL certificate for?</p>
|
<p>Which domain are you getting a certificate for?</p>
|
||||||
|
|
||||||
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
||||||
|
|
||||||
<p>What country are you in? This is required by some SSL certificate providers. You may leave this blank if you know your SSL certificate provider doesn't require it.</p>
|
<p>(A multi-domain or wildcard certificate will be automatically applied to any domains it is valid for besides the one you choose above.)</p>
|
||||||
|
|
||||||
|
<p>What country are you in? This is required by some TLS certificate providers. You may leave this blank if you know your TLS certificate provider doesn't require it.</p>
|
||||||
|
|
||||||
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
|
<p><select id="sslcc" onchange="show_csr()" class="form-control" style="width: auto">
|
||||||
<option value="">(Select)</option>
|
<option value="">(Select)</option>
|
||||||
@ -38,18 +73,18 @@
|
|||||||
</select></p>
|
</select></p>
|
||||||
|
|
||||||
<div id="csr_info" style="display: none">
|
<div id="csr_info" style="display: none">
|
||||||
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
|
<p>You will need to provide the certificate provider this Certificate Signing Request (CSR):</p>
|
||||||
|
|
||||||
<pre id="ssl_csr"></pre>
|
<pre id="ssl_csr"></pre>
|
||||||
|
|
||||||
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
|
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
|
||||||
|
|
||||||
<p>The SSL certificate provider will then provide you with an SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
|
<p>The certificate provider will then provide you with a TLS/SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
|
||||||
|
|
||||||
<p style="margin-bottom: .5em">SSL certificate:</p>
|
<p style="margin-bottom: .5em">TLS/SSL certificate:</p>
|
||||||
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----"></textarea></p>
|
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----"></textarea></p>
|
||||||
|
|
||||||
<p style="margin-bottom: .5em">SSL intermediate chain (if provided):</p>
|
<p style="margin-bottom: .5em">TLS/SSL intermediate chain (if provided):</p>
|
||||||
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
more stuff here
-----END CERTIFICATE-----"></textarea></p>
|
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
more stuff here
-----END CERTIFICATE-----"></textarea></p>
|
||||||
|
|
||||||
<p>After you paste in the information, click the install button.</p>
|
<p>After you paste in the information, click the install button.</p>
|
||||||
@ -58,26 +93,53 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function show_ssl() {
|
function show_tls(keep_provisioning_shown) {
|
||||||
api(
|
api(
|
||||||
"/web/domains",
|
"/ssl/status",
|
||||||
"GET",
|
"GET",
|
||||||
{
|
{
|
||||||
},
|
},
|
||||||
function(domains) {
|
function(res) {
|
||||||
|
// provisioning status
|
||||||
|
|
||||||
|
if (!keep_provisioning_shown)
|
||||||
|
$('#ssl_provision').toggle(res.can_provision.length + res.cant_provision.length > 0)
|
||||||
|
|
||||||
|
$('#ssl_provision_p').toggle(res.can_provision.length > 0);
|
||||||
|
if (res.can_provision.length > 0)
|
||||||
|
$('#ssl_provision_p span').text(res.can_provision.join(", "));
|
||||||
|
|
||||||
|
$('#ssl_provision_problems_div').toggle(res.cant_provision.length > 0);
|
||||||
|
$('#ssl_provision_problems tbody').text("");
|
||||||
|
for (var i = 0; i < res.cant_provision.length; i++) {
|
||||||
|
var domain = res.cant_provision[i];
|
||||||
|
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td></tr>");
|
||||||
|
$('#ssl_provision_problems tbody').append(row);
|
||||||
|
row.attr('data-domain', domain.domain);
|
||||||
|
row.find('.domain a').text(domain.domain);
|
||||||
|
row.find('.domain a').attr('href', 'https://' + domain.domain);
|
||||||
|
row.find('.status').text(domain.problem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificate status
|
||||||
|
var domains = res.status;
|
||||||
var tb = $('#ssl_domains tbody');
|
var tb = $('#ssl_domains tbody');
|
||||||
tb.text('');
|
tb.text('');
|
||||||
$('#ssldomain').html('<option value="">(select)</option>');
|
$('#ssldomain').html('<option value="">(select)</option>');
|
||||||
|
$('#ssl_domains').show();
|
||||||
for (var i = 0; i < domains.length; i++) {
|
for (var i = 0; i < domains.length; i++) {
|
||||||
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
|
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
|
||||||
tb.append(row);
|
tb.append(row);
|
||||||
row.attr('data-domain', domains[i].domain);
|
row.attr('data-domain', domains[i].domain);
|
||||||
row.find('.domain a').text(domains[i].domain);
|
row.find('.domain a').text(domains[i].domain);
|
||||||
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
|
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
|
||||||
row.addClass("text-" + domains[i].ssl_certificate[0]);
|
if (domains[i].status == "not-applicable") {
|
||||||
row.find('.status').text(domains[i].ssl_certificate[1]);
|
domains[i].status = "muted"; // text-muted css class
|
||||||
if (domains[i].ssl_certificate[0] == "success") {
|
row.find('.actions a').remove(); // no actions applicable
|
||||||
|
}
|
||||||
|
row.addClass("text-" + domains[i].status);
|
||||||
|
row.find('.status').text(domains[i].text);
|
||||||
|
if (domains[i].status == "success") {
|
||||||
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
|
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
|
||||||
} else {
|
} else {
|
||||||
row.find('.actions a').addClass('btn-primary').text('Install Certificate');
|
row.find('.actions a').addClass('btn-primary').text('Install Certificate');
|
||||||
@ -91,14 +153,15 @@ function show_ssl() {
|
|||||||
function ssl_install(elem) {
|
function ssl_install(elem) {
|
||||||
var domain = $(elem).parents('tr').attr('data-domain');
|
var domain = $(elem).parents('tr').attr('data-domain');
|
||||||
$('#ssldomain').val(domain);
|
$('#ssldomain').val(domain);
|
||||||
$('#csr_info').slideDown();
|
|
||||||
$('#ssl_csr').text('Loading...');
|
|
||||||
show_csr();
|
show_csr();
|
||||||
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
|
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top - $('.navbar-fixed-top').height() - 20 })
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function show_csr() {
|
function show_csr() {
|
||||||
|
if ($('#ssldomain').val() == "") return;
|
||||||
|
$('#csr_info').slideDown();
|
||||||
|
$('#ssl_csr').text('Loading...');
|
||||||
api(
|
api(
|
||||||
"/ssl/csr/" + $('#ssldomain').val(),
|
"/ssl/csr/" + $('#ssldomain').val(),
|
||||||
"POST",
|
"POST",
|
||||||
@ -122,10 +185,100 @@ function install_cert() {
|
|||||||
function(status) {
|
function(status) {
|
||||||
if (/^OK($|\n)/.test(status)) {
|
if (/^OK($|\n)/.test(status)) {
|
||||||
console.log(status)
|
console.log(status)
|
||||||
show_modal_error("SSL Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
|
show_modal_error("TLS Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
|
||||||
} else {
|
} else {
|
||||||
show_modal_error("SSL Certificate Installation", status);
|
show_modal_error("TLS Certificate Installation", status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var agree_to_tos_url_prompt = null;
|
||||||
|
var agree_to_tos_url = null;
|
||||||
|
function provision_tls_cert() {
|
||||||
|
// Automatically provision any certs.
|
||||||
|
$('#ssl_provision_p .btn').attr('disabled', '1'); // prevent double-clicks
|
||||||
|
api(
|
||||||
|
"/ssl/provision",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
agree_to_tos_url: agree_to_tos_url
|
||||||
|
},
|
||||||
|
function(status) {
|
||||||
|
// Clear last attempt.
|
||||||
|
agree_to_tos_url = null;
|
||||||
|
$('#ssl_provision_result').text("");
|
||||||
|
may_reenable_provision_button = true;
|
||||||
|
|
||||||
|
// Nothing was done. There might also be problem domains, but we've already displayed those.
|
||||||
|
if (status.requests.length == 0) {
|
||||||
|
show_modal_error("TLS Certificate Provisioning", "There were no domain names to provision certificates for.");
|
||||||
|
// don't return - haven't re-enabled the provision button
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each provisioning API call returns zero or more "requests" which represent
|
||||||
|
// a request to Let's Encrypt for a single certificate. Normally there is just
|
||||||
|
// one request (for a single multi-domain certificate).
|
||||||
|
for (var i = 0; i < status.requests.length; i++) {
|
||||||
|
var r = status.requests[i];
|
||||||
|
|
||||||
|
// create an HTML block to display the results of this request
|
||||||
|
var n = $("<div><h4/><p/></div>");
|
||||||
|
$('#ssl_provision_result').append(n);
|
||||||
|
|
||||||
|
// show a header only to disambiguate request blocks
|
||||||
|
if (status.requests.length > 0)
|
||||||
|
n.find("h4").text(r.domains.join(", "));
|
||||||
|
|
||||||
|
if (r.result == "agree-to-tos") {
|
||||||
|
// user needs to agree to Let's Encrypt's TOS
|
||||||
|
agree_to_tos_url_prompt = r.url;
|
||||||
|
$('#ssl_provision_p .btn').attr('disabled', '1');
|
||||||
|
n.find("p").html("Please open and review <a href='" + r.url + "' target='_blank'>Let's Encrypt's terms of service agreement</a>. You must agree to their terms for a certificate to be automatically provisioned from them.");
|
||||||
|
n.append($('<button onclick="agree_to_tos_url = agree_to_tos_url_prompt; return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Agree & Try Again</button>'));
|
||||||
|
|
||||||
|
// don't re-enable the Provision button -- user must use the Agree button
|
||||||
|
may_reenable_provision_button = false;
|
||||||
|
|
||||||
|
} else if (r.result == "error") {
|
||||||
|
n.find("p").addClass("text-danger").text(r.message);
|
||||||
|
|
||||||
|
} else if (r.result == "wait") {
|
||||||
|
// Show a button that counts down to zero, at which point it becomes enabled.
|
||||||
|
n.find("p").text("A certificate is now in the process of being provisioned, but it takes some time. Please wait until the Finish button is enabled, and then click it to acquire the certificate.");
|
||||||
|
var b = $('<button onclick="return provision_tls_cert();" class="btn btn-success" style="margin-left: 2em">Finish</button>');
|
||||||
|
b.attr("disabled", "1");
|
||||||
|
var now = new Date();
|
||||||
|
n.append(b);
|
||||||
|
function ready_to_finish() {
|
||||||
|
var remaining = r.seconds - Math.round((new Date() - now)/1000);
|
||||||
|
if (remaining > 0) {
|
||||||
|
setTimeout(ready_to_finish, 1000);
|
||||||
|
b.text("Finish (" + remaining + "...)")
|
||||||
|
} else {
|
||||||
|
b.text("Finish (ready)")
|
||||||
|
b.removeAttr("disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready_to_finish();
|
||||||
|
|
||||||
|
// don't re-enable the Provision button -- user must use the Retry button when it becomes enabled
|
||||||
|
may_reenable_provision_button = false;
|
||||||
|
|
||||||
|
} else if (r.result == "installed") {
|
||||||
|
n.find("p").addClass("text-success").text("The TLS certificate was provisioned and installed.");
|
||||||
|
setTimeout("show_tls(true)", 1); // update main table of certificate statuses, call with arg keep_provisioning_shown true so that we don't clear what we just outputted
|
||||||
|
}
|
||||||
|
|
||||||
|
// display the detailed log info in case of problems
|
||||||
|
var trace = $("<div class='small text-muted' style='margin-top: 1.5em'>Log:</div>");
|
||||||
|
n.append(trace);
|
||||||
|
for (var j = 0; j < r.log.length; j++)
|
||||||
|
trace.append($("<div/>").text(r.log[j]));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (may_reenable_provision_button)
|
||||||
|
$('#ssl_provision_p .btn').removeAttr("disabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -9,7 +9,7 @@ from dns_update import get_custom_dns_config, get_dns_zones
|
|||||||
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
||||||
from utils import shell, safe_domain_name, sort_domains
|
from utils import shell, safe_domain_name, sort_domains
|
||||||
|
|
||||||
def get_web_domains(env, include_www_redirects=True):
|
def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True):
|
||||||
# What domains should we serve HTTP(S) for?
|
# What domains should we serve HTTP(S) for?
|
||||||
domains = set()
|
domains = set()
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ def get_web_domains(env, include_www_redirects=True):
|
|||||||
# the topmost of each domain we serve.
|
# the topmost of each domain we serve.
|
||||||
domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env))
|
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
|
# ...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.
|
# IP address than this box. Remove those domains from our list.
|
||||||
domains -= get_domains_with_a_records(env)
|
domains -= get_domains_with_a_records(env)
|
||||||
@ -119,7 +120,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 +137,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 +178,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 +194,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:
|
||||||
|
@ -38,7 +38,7 @@ These services are protected by [TLS](https://en.wikipedia.org/wiki/Transport_La
|
|||||||
|
|
||||||
The services all follow these rules:
|
The services all follow these rules:
|
||||||
|
|
||||||
* SSL certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh))
|
* TLS certificates are generated with 2048-bit RSA keys and SHA-256 fingerprints. The box provides a self-signed certificate by default. The [setup guide](https://mailinabox.email/guide.html) explains how to verify the certificate fingerprint on first login. Users are encouraged to replace the certificate with a proper CA-signed one. ([source](setup/ssl.sh))
|
||||||
* Only TLSv1, TLSv1.1 and TLSv1.2 are offered (the older SSL protocols are not offered).
|
* Only TLSv1, TLSv1.1 and TLSv1.2 are offered (the older SSL protocols are not offered).
|
||||||
* Export-grade ciphers, the anonymous DH/ECDH algorithms (aNULL), and clear-text ciphers (eNULL) are not offered.
|
* Export-grade ciphers, the anonymous DH/ECDH algorithms (aNULL), and clear-text ciphers (eNULL) are not offered.
|
||||||
* The minimum cipher key length offered is 112 bits. The maximum is 256 bits. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy.
|
* The minimum cipher key length offered is 112 bits. The maximum is 256 bits. Diffie-Hellman ciphers use a 2048-bit key for forward secrecy.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user