From 7c22bbe5f08ba7e5dafd6e28339934e0828f77db Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 21 Sep 2016 17:30:04 +0200 Subject: [PATCH] Experimental support for Let's Encrypt. see #50 --- README.rst | 23 +++++++++++ installer.cfg | 8 ++++ modoboa_installer/ssl.py | 84 ++++++++++++++++++++++++++++++---------- run.py | 7 +++- 4 files changed, 100 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index c818524..be8a13f 100644 --- a/README.rst +++ b/README.rst @@ -39,3 +39,26 @@ By default, the following components are installed: If you want more information about the installation process, add the ``--debug`` option to your command line. + +Let's Encrypt certificate +------------------------- + +.. warning:: + + Please note this option requires the hostname you're using to be + valid (ie. it can be resolved with a DNS query) and to match the + server you're installing Modoboa on. + +If you want to generate a valid certificate using `Let's Encrypt +`_, edit the ``installer.cfg`` file and +modify the following settings:: + + [certificate] + generate = true + type = letsencrypt + + [letsencrypt] + email = admin@example.com + +Change the ``email`` setting to a valid value since it will be used +for account recovery. diff --git a/installer.cfg b/installer.cfg index cd324db..96de4a2 100644 --- a/installer.cfg +++ b/installer.cfg @@ -1,3 +1,11 @@ +[certificate] +generate = true +# Choose between self-signed or letsencrypt +type = letsencrypt + +[letsencrypt] +email = admin@example.com + [database] # Select database engine : postgres or mysql engine = postgres diff --git a/modoboa_installer/ssl.py b/modoboa_installer/ssl.py index a28bbdc..0e3bbe7 100644 --- a/modoboa_installer/ssl.py +++ b/modoboa_installer/ssl.py @@ -11,32 +11,42 @@ class CertificateBackend(object): def __init__(self, config): """Set path to certificates.""" self.config = config - if not config.has_option("general", "tls_key_file"): - for base_dir in ["/etc/pki/tls", "/etc/ssl"]: - if os.path.exists(base_dir): - self.config.set( - "general", "tls_key_file", - "{}/private/%(hostname)s.key".format(base_dir)) - self.config.set( - "general", "tls_cert_file", - "{}/certs/%(hostname)s.cert".format(base_dir)) - return - raise RuntimeError("Cannot find a directory to store certificate") - else: - return - -class SelfSignedCertificate(CertificateBackend): - """Create a self signed certificate.""" - - def create(self): - """Create a certificate.""" + def overwrite_existing_certificate(self): + """Check if certificate already exists.""" if os.path.exists(self.config.get("general", "tls_key_file")): if not self.config.getboolean("general", "force"): answer = utils.user_input( "Overwrite the existing SSL certificate? (y/N) ") if not answer.lower().startswith("y"): - return + return False + return True + + +class SelfSignedCertificate(CertificateBackend): + """Create a self signed certificate.""" + + def __init__(self, *args, **kwargs): + """Sanity checks.""" + super(SelfSignedCertificate, self).__init__(*args, **kwargs) + if self.config.has_option("general", "tls_key_file"): + # Compatibility + return + for base_dir in ["/etc/pki/tls", "/etc/ssl"]: + if os.path.exists(base_dir): + self.config.set( + "general", "tls_key_file", + "{}/private/%(hostname)s.key".format(base_dir)) + self.config.set( + "general", "tls_cert_file", + "{}/certs/%(hostname)s.cert".format(base_dir)) + return + raise RuntimeError("Cannot find a directory to store certificate") + + def create(self): + """Create a certificate.""" + if not self.overwrite_existing_certificate(): + return utils.printcolor( "Generating new self-signed certificate", utils.YELLOW) utils.exec_cmd( @@ -48,6 +58,40 @@ class SelfSignedCertificate(CertificateBackend): ) +class LetsEncryptCertificate(CertificateBackend): + """Create a certificate using letsencrypt.""" + + def create(self): + """Create a certificate.""" + utils.printcolor( + "Generating new certificate using letsencrypt", utils.YELLOW) + hostname = self.config.get("general", "hostname") + utils.exec_cmd( + "wget https://dl.eff.org/certbot-auto; chmod a+x certbot-auto", + cwd="/opt") + webroot = os.path.join( + self.config.get("modoboa", "instance_path"), + "sitestatic/.well-known") + utils.exec_cmd( + "/opt/certbot-auto certonly -n --standalone -d {} " + "-m {} --agree-tos".format( + webroot, hostname, self.config.get("letsencrypt", "email"))) + self.config.set("general", "tls_cert_file", ( + "/etc/letsencrypt/live/{}/fullchain.pem".format(hostname))) + self.config.set("general", "tls_key_file", ( + "/etc/letsencrypt/live/{}/privkey.pem".format(hostname))) + with open("/etc/cron.d/letsencrypt", "w") as fp: + fp.write("0 */12 * * * root /opt/certbot-auto renew " + "--quiet --no-self-upgrade && " + "service nginx reload && " + "service postfix reload && " + "service dovecot reload") + + def get_backend(config): """Return the appropriate backend.""" + if not config.getboolean("certificate", "generate"): + return None + if config.get("certificate", "type") == "letsencrypt": + return LetsEncryptCertificate(config) return SelfSignedCertificate(config) diff --git a/run.py b/run.py index 2909bd8..0fc99c7 100755 --- a/run.py +++ b/run.py @@ -39,7 +39,8 @@ def main(): .format(args.hostname), utils.BLUE) components = [] for section in config.sections(): - if section in ["general", "database", "mysql", "postgres"]: + if section in ["general", "database", "mysql", "postgres", + "certificate", "letsencrypt"]: continue if (config.has_option(section, "enabled") and not config.getboolean(section, "enabled")): @@ -56,7 +57,9 @@ def main(): "and come back later ;)", utils.BLUE) utils.printcolor("Starting...", utils.GREEN) package.backend.install("sudo") - ssl.get_backend(config).create() + ssl_backend = ssl.get_backend(config) + if ssl_backend: + ssl_backend.create() scripts.install("modoboa", config) scripts.install("postfix", config) scripts.install("amavis", config)