Merge pull request #254 from Mailu/feature-traefik
Switch to traefik for proxying and querying letsencryptmaster
						commit
						829e4a5e28
					
				@ -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")
 | 
			
		||||
@ -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.exists(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)
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
FROM traefik:alpine
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache bash
 | 
			
		||||
 | 
			
		||||
COPY conf /conf
 | 
			
		||||
COPY start.sh /start.sh
 | 
			
		||||
 | 
			
		||||
CMD /start.sh
 | 
			
		||||
@ -0,0 +1,31 @@
 | 
			
		||||
defaultEntryPoints = ["http", "https"]
 | 
			
		||||
logLevel = "ERROR"
 | 
			
		||||
accessLogsFile = "/dev/stdout"
 | 
			
		||||
 | 
			
		||||
[entryPoints]
 | 
			
		||||
  [entryPoints.http]
 | 
			
		||||
  address = ":80"
 | 
			
		||||
    [entryPoints.http.redirect]
 | 
			
		||||
    entryPoint = "https"
 | 
			
		||||
  [entryPoints.https]
 | 
			
		||||
  address = ":443"
 | 
			
		||||
    [entryPoints.https.tls]
 | 
			
		||||
      MinVersion = "VersionTLS11"
 | 
			
		||||
      CipherSuites = ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"]
 | 
			
		||||
    [[entryPoints.https.tls.certificates]]
 | 
			
		||||
    CertFile = "/certs/cert.pem"
 | 
			
		||||
    KeyFile = "/certs/key.pem"
 | 
			
		||||
 | 
			
		||||
[docker]
 | 
			
		||||
endpoint = "unix:///docker.sock"
 | 
			
		||||
domain = "{{ DOMAIN }}"
 | 
			
		||||
watch = true
 | 
			
		||||
exposedbydefault = false
 | 
			
		||||
 | 
			
		||||
[acme]
 | 
			
		||||
email = "{{ POSTMASTER }}@{{ DOMAIN }}"
 | 
			
		||||
storageFile = "/certs/acme.json"
 | 
			
		||||
onDemand = true
 | 
			
		||||
entryPoint = "https"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
defaultEntryPoints = ["http", "https"]
 | 
			
		||||
logLevel = "ERROR"
 | 
			
		||||
accessLogsFile = "/dev/stdout"
 | 
			
		||||
 | 
			
		||||
[entryPoints]
 | 
			
		||||
  [entryPoints.http]
 | 
			
		||||
  address = ":80"
 | 
			
		||||
    [entryPoints.http.redirect]
 | 
			
		||||
    entryPoint = "https"
 | 
			
		||||
  [entryPoints.https]
 | 
			
		||||
  address = ":443"
 | 
			
		||||
    [entryPoints.https.tls]
 | 
			
		||||
      MinVersion = "VersionTLS11"
 | 
			
		||||
      CipherSuites = ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"]
 | 
			
		||||
 | 
			
		||||
[docker]
 | 
			
		||||
endpoint = "unix:///docker.sock"
 | 
			
		||||
domain = "{{ DOMAIN }}"
 | 
			
		||||
watch = true
 | 
			
		||||
exposedbydefault = false
 | 
			
		||||
 | 
			
		||||
[acme]
 | 
			
		||||
email = "{{ POSTMASTER }}@{{ DOMAIN }}"
 | 
			
		||||
storageFile = "/certs/acme.json"
 | 
			
		||||
onDemand = true
 | 
			
		||||
entryPoint = "https"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
defaultEntryPoints = ["http"]
 | 
			
		||||
logLevel = "ERROR"
 | 
			
		||||
accessLogsFile = "/dev/stdout"
 | 
			
		||||
 | 
			
		||||
[entryPoints]
 | 
			
		||||
  [entryPoints.http]
 | 
			
		||||
  address = ":80"
 | 
			
		||||
 | 
			
		||||
[docker]
 | 
			
		||||
endpoint = "unix:///docker.sock"
 | 
			
		||||
domain = "{{ DOMAIN }}"
 | 
			
		||||
watch = true
 | 
			
		||||
exposedbydefault = false
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,12 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
# Substitute configuration
 | 
			
		||||
for VARIABLE in `env | cut -f1 -d=`; do
 | 
			
		||||
  sed -i "s={{ $VARIABLE }}=${!VARIABLE}=g" /conf/*.toml
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
# Select the proper configuration
 | 
			
		||||
cp /conf/$TLS_FLAVOR.toml /conf/traefik.toml
 | 
			
		||||
 | 
			
		||||
exec traefik -c /conf/traefik.toml
 | 
			
		||||
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue