From c422543fdd52e13d9c0d93b621e153c697b85250 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sun, 29 Nov 2015 01:27:03 +0000 Subject: [PATCH] make the system SSL certificate a symlink so we never have to replace a certificate file, and flatten the directory structure of user-installed certificates --- management/web_update.py | 43 +++++++++++++++++++++------------------- setup/migrate.py | 26 ++++++++++++++++++++++++ setup/ssl.sh | 11 +++++++--- 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/management/web_update.py b/management/web_update.py index c9c93f60..18fd27f8 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -351,21 +351,18 @@ def install_cert(domain, ssl_cert, ssl_chain, env): return cert_status # Where to put it? - if domain == env['PRIMARY_HOSTNAME']: - ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) - else: - # Make a unique path for the certificate. - from status_checks import load_cert_chain, load_pem, get_certificate_domains - from cryptography.hazmat.primitives import hashes - from binascii import hexlify - cert = load_pem(load_cert_chain(fn)[0]) - all_domains, cn = get_certificate_domains(cert) - path = "%s-%s-%s" % ( - cn, # common name - cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date - hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix - ) - ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', path, 'ssl_certificate.pem')) + # Make a unique path for the certificate. + from status_checks import load_cert_chain, load_pem, get_certificate_domains + from cryptography.hazmat.primitives import hashes + from binascii import hexlify + cert = load_pem(load_cert_chain(fn)[0]) + all_domains, cn = get_certificate_domains(cert) + path = "%s-%s-%s.pem" % ( + cn, # common name + cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date + hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix + ) + ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', path)) # Install the certificate. os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True) @@ -373,17 +370,23 @@ def install_cert(domain, ssl_cert, ssl_chain, env): ret = ["OK"] - # When updating the cert for PRIMARY_HOSTNAME, also update DNS because it is - # used in the DANE TLSA record and restart postfix and dovecot which use - # that certificate. + # When updating the cert for PRIMARY_HOSTNAME, symlink it from the system + # certificate path, which is hard-coded for various purposes, and then + # update DNS (because of the DANE TLSA record), postfix, and dovecot, + # which all use the file. if domain == env['PRIMARY_HOSTNAME']: - ret.append( do_dns_update(env) ) + # Update symlink. + system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) + os.unlink(system_ssl_certificate) + os.symlink(ssl_certificate, system_ssl_certificate) + # Update DNS & restart postfix and dovecot so they pick up the new file. + ret.append( do_dns_update(env) ) shell('check_call', ["/usr/sbin/service", "postfix", "restart"]) shell('check_call', ["/usr/sbin/service", "dovecot", "restart"]) ret.append("mail services restarted") - # Kick nginx so it sees the cert. + # Update the web configuration so nginx picks up the new certificate file. ret.append( do_web_update(env) ) return "\n".join(ret) diff --git a/setup/migrate.py b/setup/migrate.py index 6acd0edc..45f748bc 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -111,6 +111,32 @@ def migration_9(env): db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD permitted_senders TEXT"]) +def migration_10(env): + # Clean up the SSL certificates directory. + + # Move the primary certificate to a new name and then + # symlink it to the system certificate path. + import datetime + system_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') + if not os.path.islink(system_certificate): # not already a symlink + new_path = os.path.join(env["STORAGE_ROOT"], 'ssl', env['PRIMARY_HOSTNAME'] + "-" + datetime.datetime.now().date().isoformat().replace("-", "") + ".pem") + print("Renamed", system_certificate, "to", new_path, "and created a symlink for the original location.") + shutil.move(system_certificate, new_path) + os.symlink(new_path, system_certificate) + + # Flatten the directory structure. For any directory + # that contains a single file named ssl_certificate.pem, + # move the file out and name it the same as the directory, + # and remove the directory. + for sslcert in glob.glob(os.path.join( env["STORAGE_ROOT"], 'ssl/*/ssl_certificate.pem' )): + d = os.path.dirname(sslcert) + if len(os.listdir(d)) == 1: + # This certificate is the only file in that directory. + newname = os.path.join(env["STORAGE_ROOT"], 'ssl', os.path.basename(d) + '.pem') + if not os.path.exists(newname): + shutil.move(sslcert, newname) + os.rmdir(d) + def get_current_migration(): ver = 0 while True: diff --git a/setup/ssl.sh b/setup/ssl.sh index 5d6143f5..fa29a211 100755 --- a/setup/ssl.sh +++ b/setup/ssl.sh @@ -77,12 +77,17 @@ if [ ! -f $STORAGE_ROOT/ssl/ssl_certificate.pem ]; then -sha256 -subj "/C=$CSR_COUNTRY/ST=/L=/O=/CN=$PRIMARY_HOSTNAME" # Generate the self-signed certificate. + CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem hide_output \ openssl x509 -req -days 365 \ - -in $CSR -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $STORAGE_ROOT/ssl/ssl_certificate.pem + -in $CSR -signkey $STORAGE_ROOT/ssl/ssl_private_key.pem -out $CERT - # Delete the certificate signing request because it has no other purpose. - rm -f $CSR + # Delete the certificate signing request because it has no other purpose. + rm -f $CSR + + # Symlink the certificate into the system certificate path, so system services + # can find it. + ln -s $CERT $STORAGE_ROOT/ssl/ssl_certificate.pem fi # Generate some Diffie-Hellman cipher bits.