diff --git a/admin/mailu/__init__.py b/admin/mailu/__init__.py index aeb4d89e..85b908c4 100644 --- a/admin/mailu/__init__.py +++ b/admin/mailu/__init__.py @@ -29,7 +29,8 @@ default_config = { 'DKIM_SELECTOR': 'dkim', 'BABEL_DEFAULT_LOCALE': 'en', 'BABEL_DEFAULT_TIMEZONE': 'UTC', - 'ENABLE_CERTBOT': False, + 'FRONTEND': 'none', + 'TLS_FLAVOR': 'cert', 'CERTS_PATH': '/certs', 'PASSWORD_SCHEME': 'SHA512-CRYPT' } @@ -57,16 +58,13 @@ manager.add_command('db', flask_migrate.MigrateCommand) # Task scheduling if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true': scheduler.start() + from mailu import tlstasks # Babel configuration @babel.localeselector def get_locale(): return flask.request.accept_languages.best_match(translations) -# Certbot configuration -if app.config['ENABLE_CERTBOT']: - from mailu import certbot - # Finally setup the blueprint and redirect / from mailu import admin app.register_blueprint(admin.app, url_prefix='/admin') diff --git a/admin/mailu/certbot.py b/admin/mailu/certbot.py deleted file mode 100644 index 71e0309c..00000000 --- a/admin/mailu/certbot.py +++ /dev/null @@ -1,71 +0,0 @@ -from mailu import app, scheduler, dockercli - -import subprocess -import os - - -def certbot_command(subcommand, *args): - """ Run a certbot command while specifying the standard parameters. - """ - command = [ - "certbot", subcommand, - "-n", - "--work-dir", "/tmp", - "--logs-dir", "/tmp", - "--config-dir", app.config["CERTS_PATH"], - *args - ] - result = subprocess.run(command, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - return result - - -def certbot_install(domain): - """ Install certificates for the given domain. Return True if a reload - is required. - """ - must_reload = False - path = app.config["CERTS_PATH"] - cert = os.path.join(path, "cert.pem") - key = os.path.join(path, "key.pem") - live_cert = os.path.join("live", domain, "fullchain.pem") - live_key = os.path.join("live", domain, "privkey.pem") - if not os.path.islink(cert) or os.readlink(cert) != live_cert: - must_reload = True - if os.path.exists(cert): - os.unlink(cert) - os.symlink(live_cert, cert) - if not os.path.islink(key) or os.readlink(key) != live_key: - must_reload = True - if os.path.exists(key): - os.unlink(key) - os.symlink(live_key, key) - return must_reload - - -@scheduler.scheduled_job('date') -@scheduler.scheduled_job('cron', day='*/4', hour=0, minute=0) -def generate_cert(): - print("Generating TLS certificates using Certbot") - hostname = app.config["HOSTNAME"] - email = "{}@{}".format(app.config["POSTMASTER"], app.config["DOMAIN"]) - result = certbot_command( - "certonly", - "--standalone", - "--agree-tos", - "--preferred-challenges", "http", - "--email", email, - "-d", hostname, - # The port is hardcoded in the nginx image as well, we should find - # a more suitable way to go but this will do until we have a proper - # daemon handling certbot stuff - "--http-01-port", "8081" - ) - if result.returncode: - print("Error while generating certificates:\n{}".format( - result.stdout.decode("utf8") + result.stderr.decode("utf8"))) - else: - print("Successfully generated or renewed TLS certificates") - if certbot_install(hostname): - print("Reloading TLS-dependant services") - dockercli.reload("http", "smtp", "imap") diff --git a/admin/mailu/tlstasks.py b/admin/mailu/tlstasks.py new file mode 100644 index 00000000..dc139c7e --- /dev/null +++ b/admin/mailu/tlstasks.py @@ -0,0 +1,68 @@ +from mailu import app, scheduler, dockercli + +import urllib3 +import json +import os +import base64 +import subprocess + + +def install_certs(domain): + """ Extract certificates from the given domain and install them + to the certificate path. + """ + path = app.config["CERTS_PATH"] + acme_path = os.path.join(path, "acme.json") + key_path = os.path.join(path, "key.pem") + cert_path = os.path.join(path, "cert.pem") + if not os.path.exists(acme_path): + print("Could not find traefik acme configuration") + return + with open(acme_path, "r") as handler: + data = json.loads(handler.read()) + for item in data["DomainsCertificate"]["Certs"]: + if domain == item["Domains"]["Main"]: + cert = base64.b64decode(item["Certificate"]["Certificate"]) + key = base64.b64decode(item["Certificate"]["PrivateKey"]) + break + else: + print("Could not find the proper certificate from traefik") + return + if os.path.join(cert_path): + with open(cert_path, "rb") as handler: + if handler.read() == cert: + return + print("Installing the new certificate from traefik") + with open(cert_path, "wb") as handler: + handler.write(cert) + with open(key_path, "wb") as handler: + handler.write(key) + + +def restart_services(): + print("Reloading services using TLS") + dockercli.reload("http", "smtp", "imap") + + +@scheduler.scheduled_job('date') +def create_dhparam(): + path = app.config["CERTS_PATH"] + dhparam_path = os.path.join(path, "dhparam.pem") + if not os.path.exists(dhparam_path): + print("Creating DH params") + subprocess.call(["openssl", "dhparam", "-out", dhparam_path, "2048"]) + restart_services() + + +@scheduler.scheduled_job('date') +@scheduler.scheduled_job('cron', day='*/4', hour=0, minute=0) +def refresh_certs(): + if not app.config["TLS_FLAVOR"] == "letsencrypt": + return + if not app.config["FRONTEND"] == "traefik": + print("Letsencrypt certificates are compatible with traefik only") + return + print("Requesting traefik to make sure the certificate is fresh") + hostname = app.config["HOSTNAME"] + urllib3.PoolManager().request("GET", "https://{}".format(hostname)) + install_certs(hostname)