diff --git a/admin/Dockerfile b/admin/Dockerfile index f078ea0d..86b090a1 100644 --- a/admin/Dockerfile +++ b/admin/Dockerfile @@ -10,6 +10,13 @@ COPY requirements.txt . COPY start.sh /start.sh RUN pip install -r requirements.txt + +# Temporarily install certbot from source while waiting for 0.10 +RUN git clone https://github.com/certbot/certbot /certbot \ + && cd /certbot \ + && pip install -e ./acme \ + && pip install -e ./ + RUN pybabel compile -d mailu/translations CMD ["/start.sh"] diff --git a/admin/mailu/__init__.py b/admin/mailu/__init__.py index b9787ddb..c352febd 100644 --- a/admin/mailu/__init__.py +++ b/admin/mailu/__init__.py @@ -28,7 +28,9 @@ default_config = { 'DKIM_PATH': '/dkim/{domain}.{selector}.key', 'DKIM_SELECTOR': 'dkim', 'BABEL_DEFAULT_LOCALE': 'en', - 'BABEL_DEFAULT_TIMEZONE': 'UTC' + 'BABEL_DEFAULT_TIMEZONE': 'UTC', + 'ENABLE_CERTBOT': False, + 'CERTS_PATH': '/certs' } # Load configuration from the environment if available @@ -61,6 +63,10 @@ if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true': 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 new file mode 100644 index 00000000..6b3cbdc7 --- /dev/null +++ b/admin/mailu/certbot.py @@ -0,0 +1,68 @@ +from mailu import app, scheduler + +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(path, "live", domain, "fullchain.pem") + live_key = os.path.join(path, "live", domain, "privkey.pem") + if not os.path.islink(cert) or os.readlink(cert) != live_cert: + must_reload = True + os.unlink(cert) + os.symlink(live_cert, cert) + if not os.path.islink(key) or os.readlink(key) != live_key: + must_reload = True + os.unlink(key) + os.symlink(live_key, key) + return must_reload + + +@scheduler.scheduled_job('date') +@scheduler.scheduled_job('cron', hour=2, minute=0) +def generate_cert(): + print("Generating TLS certificates using Certbot") + domain = app.config["DOMAIN"] + email = "{}@{}".format(app.config["POSTMASTER"], domain) + result = certbot_command( + "certonly", + "--standalone", + "--agree-tos", + "--preferred-challenges", "http", + "--email", email, + "-d", domain, + # 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.stdout.decode("utf8"))) + else: + print("Successfully generated or renewed TLS certificates") + must_reload = certbot_install(domain) + diff --git a/admin/run.py b/admin/run.py index 74f32e57..a55ac1e2 100644 --- a/admin/run.py +++ b/admin/run.py @@ -2,6 +2,6 @@ import os if __name__ == "__main__": - os.environ["DEBUG"] = "true" + os.environ["DEBUG"] = "True" from mailu import app app.run() diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 2a7b13c0..ec4db23f 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -76,5 +76,9 @@ http { return 403; } } + + location /.well-known/acme-challenge { + proxy_pass http://admin:8081; + } } }