link to sloonz's script
[web.git] / ralf / _posts / 2017-12-26-lets-encrypt.md
diff --git a/ralf/_posts/2017-12-26-lets-encrypt.md b/ralf/_posts/2017-12-26-lets-encrypt.md
deleted file mode 100644 (file)
index 3f4fbee..0000000
+++ /dev/null
@@ -1,262 +0,0 @@
----
-title: Let's Encrypt Tiny
-categories: Sysadmin
----
-
-I think all HTTP communication on the internet should be encrypted -- and thanks
-to [Let's Encrypt](https://letsencrypt.org/), we are now much closer to this
-goal than we were two years ago.  However, when I set up Let's Encrypt on my
-server (which is more than a year ago by now), I was not very happy with the
-official client: The client manages multiple certificates with different sets of
-domains per certificate, but I found it entirely unclear which commands would
-replace existing certificates or create a new one.  Moreover, I have some
-special needs: I've set up DNSSEC with TLSA records containing hashes of my
-certificates, so replacing a certificate has to also update DNS and deal with
-the fact that DNS entries get cached.  Lucky enough, Let's Encrypt is based on
-open standards, so I was not forced to use their client!
-
-To make a long story short, I decided to write my own Let's Encrypt client,
-which I describe in this post.
-
-<!-- MORE -->
-
-## Let's Encrypt Tiny
-
-The client is based on [acme-tiny](https://github.com/diafygi/acme-tiny), a
-beautifully small Python library (<200 lines) speaking the ACME protocol.
-That's the protocol developed by Let's Encrypt to communicate with an automated
-CA.  I duly called my client "Let's Encrypt Tiny", and with less than 250 lines
-I think that name is still fair.  For now, Let's Encrypt Tiny resides in my
-[server-scripts](https://github.com/RalfJung/server-scripts) repository, and it
-will stay there until anyone else has an interesting in using it. ;)
-
-**Update:** Let's Encrypt Tiny now has its
-[own repository](https://github.com/RalfJung/lets-encrypt-tiny). **/Update**
-
-The central concept of Let's Encrypt Tiny is a "certificate line" -- a sequence
-of certificates, possibly for different private keys, that "belong together" in
-the sense that each is considered (by their owner) the successor of the
-previous.  Services like apache are configured to use a particular certificate
-line for a particular domain (well, in my case, it's the same line for all
-domains, but one could imagine different setups).  Each certificate line has a
-separate config file, whose most important job is to configure the set of
-domains in the latest certificate of the line.  The key operations on a
-certificate line are to create a *fresh* key and obtain a certificate for it,
-and to obtain a new certificate for the *existing* key.  Let's Encrypt
-certificates expire after 90 days, so we have to perform renewal at least that
-often, but there is no reason to always generate a fresh private key.  One
-reason to keep the previous key is that the TLSA record in DNS can be configured
-to be a hash of just the key, so a renewal for an existing key can be done
-without changing DNS.
-
-The other important concept in Let's Encrypt Tiny is the idea of a "staging
-key", as opposed to the "live key".  This is needed to properly support
-DNSSEC+TLSA.  Just briefly, the idea of TLSA is to not use certificate
-authorities to determine the correct certificate for a domain (CAs have proven
-untrustworthy again and again, and a single failing CA undermines the security
-of the entire system), but instead use DNS.  If DNS is secured with DNSSEC --
-which is much more resilient against a single entity failing than the CA system
--- then we can just put a hash of the certificate, or a hash of the public key,
-into the DNS, side-stepping the entire CA system.  Unfortunately, TLSA has not
-seen widespread adoption, though a
-[Firefox extension](https://addons.mozilla.org/en-US/firefox/addon/dnssec-validator/)
-is available (and extensions for some other browsers as well).  Still, I like
-this technology, so I have deployed it on my server.
-
-However, one consequence of TLSA records is that a freshly generated key cannot
-immediately be used (i.e., by the web server): The DNS still contains the old
-key's data, and that data gets cached!  In Let's Encrypt Tiny, this key first
-gets "staged".  Next, we have to update the DNS zone to contain TLSA records for
-*both* the old and the new key.  Then we have to wait until the TTL
-(time-to-live) of that record passes, to make sure that no caches still contain
-*only* the old key.  Finally, we "unstage" the key so all the servers (web
-server, jabber server, and so on) to use the new certificate and key, which are
-now "live".
-
-## Configuration
-
-Let's look at an example: Here is the configuration file for this server,
-ralfj.de, with comments explaining the purpose of the various options:
-
-```
-# The domains currently making up this certificate line:
-domains =
-  ralfj.de
-  www.ralfj.de
-  lists.ralfj.de
-  git.ralfj.de
-  ns.ralfj.de
-  ipv4.ns.ralfj.de
-  ipv6.ns.ralfj.de
-  jabber.ralfj.de
-  conference.jabber.ralfj.de
-# ... this list goes on.
-
-# The size of the RSA secret key (in bits).
-key-length = 4096
-
-[timing]
-# Max. age of the private key before we generate a new one.
-max-key-age-days = 256
-# How long a new private key is "staged" before it is used as the live key.
-# 0 disables staging.
-staging-hours = 0
-# How many days before the certificate expires should be request a new one?
-renew-cert-before-expiry-days = 15
-
-[hooks]
-# Script to execute after the certificate changed.
-post-certchange = /root/letsencrypt/cert-hook
-# Script to execute after the key changed.  Only needed when using DNSSEC+TLSA.
-#post-keychange = /root/letsencrypt/key-hook
-
-[acme]
-# File storing the ACME account private key (created if missing).
-account-key = /etc/ssl/private/letsencrypt/account.key
-# Directory to put the ACME challenges into.  Must be mapped to
-# /.well-known/acme-challenge on all domains listed above.  For example, in
-# apache, put the following directive somewhere global:
-#   Alias /.well-known/acme-challenge/ /srv/acme-challenge/
-challenge-dir = /srv/acme-challenge/
-
-[dirs]
-# Directory for certificates.
-certs = /etc/ssl/mycerts/letsencrypt
-# Directory for private keys.
-keys = /etc/ssl/private/letsencrypt
-# A place to put old certificates and keys.
-backups = /etc/ssl/old/letsencrypt
-
-[files]
-# Filename prefix (without extension) for the live key and certificate.
-live = live
-# Filename prefix (without extension) for the staging key and certificate.
-staging = staging
-```
-
-With this configuration, Let's Encrypt Tiny creates files
-`/etc/ssl/mycerts/letsencrypt/live.crt` and
-`/etc/ssl/private/letsencrypt/live.key`.  These files are always the "tip" of
-the certificate line and should be configured in the various servers -- however,
-most servers will need these files to be massaged a bit.  First of all, we also
-need a key chain, and the intermediate CA used by Let's Encrypt actually changes
-over time.  Moreover, some servers want certificate and key in one file, while
-others want the certificates to be bundled with the keychain and expect the
-private key in a separate file.  Sometimes, the Diffie-Hellman parameters are
-also expected in the same file as the certificate -- every SSL-supporting server
-seems to handle this slightly differently.
-
-This is all handled by the certificate hook, which creates the various derived files:
-```
-#!/bin/sh
-cd /etc/ssl/mycerts/letsencrypt
-export PATH="/usr/sbin/:/sbin/:$PATH"
-
-# Determine the intermediate CA used by this certificate.  We expect the
-# intermediate certificates to be stored in files in /etc/ssl/chains/,
-# e.g. /etc/ssl/chains/letsencrypt-X3.crt.
-ISSUER=$(openssl x509 -issuer -in live.crt -noout | sed 's/.*Authority \(X[0-9]\+\).*/\1/')
-ISSUER_FILE=/etc/ssl/chains/letsencrypt-"$ISSUER".crt
-if ! [ -f "$ISSUER_FILE" ]; then
-    echo "Cannot find certificate for issuer $(openssl x509 -issuer -in live.crt -noout)"
-    exit 1
-fi
-
-# Create derived files.  We expect /etc/ssl/dh2048.pem to contain the DH
-# parameters, generated with
-#   openssl dhparam -out /etc/ssl/dh2048.pem 2048
-cat "$ISSUER_FILE" > live.chain # just the chain
-cat live.crt /etc/ssl/dh2048.pem > live.crt+dh # Certificate plus DH parameters
-cat live.crt live.chain > live.crt+chain # Certificate plus chain
-
-# Fill in here:  the code to restart/reload all relevant services.
-```
-
-With this, the apache SSL configuration looks as follows:
-
-```
-# Certificate, Key, and DH parameters
-SSLCertificateFile      /etc/ssl/mycerts/live.crt+dh
-SSLCertificateKeyFile   /etc/ssl/private/live.key
-SSLCertificateChainFile /etc/ssl/mycerts/live.chain
-
-# configure SSL ciphers and protocols
-SSLProtocol All -SSLv2 -SSLv3
-# TODO: Once OpenSSL supports GMC with more than just AES, revisit this
-# NOTE: The reason we support non-FS ciphers is stupid middleboxes that don't support FS
-SSLCipherSuite 'kEECDH+AESGCM:kEDH+AESGCM:kEECDH:kEDH:AESGCM:ALL:!3DES:!EXPORT:!LOW:!MEDIUM:!aNULL:!eNULL'
-SSLHonorCipherOrder     on     
-```
-
-(My cipher suite is deliberately not the one from
-[bettercrypto.org](https://bettercrypto.org) because I prefer to not update it
-with every change in OpenSSL's supported ciphers.)
-
-## Obtaining the first certificate
-
-You can now run `letsencrypt-tiny -c letsencrypt.conf init` to perform the
-initial setup.
-
-In the future, to change the set of domains, first edit the config file and then
-run `letsencrypt-tiny -c letsencrypt.conf -k renew`.  The `-k` tells Let's
-Encrypt Tiny to also run the certificate hook.
-
-## Automation via cron
-
-Let's Encrypt certificates expire after 90 days, so we want renewal to be
-automated.  To this end, just make sure that `letsencrypt-tiny -c
-letsencrypt.conf -k cron` gets run regularly, like once a day.  I have the
-following in root's crontab (`sudo crontab -e`):
-
-```
-32  6  *   *   *     /root/server-scripts/letsencrypt-tiny -c /root/letsencrypt/conf -k cron
-```
-
-This will check the time intervals you configured above, and act accordingly.
-If any action is taken, the script will print that information on standard
-output; if you have email set up on your server, this means you will get an
-email notification.
-
-## DNSSEC and TLSA
-
-Everything described so far should give you a working SSL setup if you do not
-use DNSSEC+TLSA.  If you *do* use DNSSEC+TLSA, like I do on this server, you
-need to enable the `post-keychange` hook and have it regenerate your DNS zone,
-and you need to increase `staging-hours`.  The zone should always contain the
-hash of the live key, and, if a staging key exists, also the hash of the staging
-key.
-
-I am managing my DNS zones with
-[zonemaker](https://www.ralfj.de/projects/zonemaker/), and wrote some Python
-code to automatically generate TLSA records using the `tlsa` tool:
-
-```
-def TLSA_from_crt(protocol, port, crtfile):
-    crtfile = "/etc/ssl/mycerts/"+crtfile
-    open(crtfile).close() # check if the file exists
-    # make sure we match on *the key only*, so that we can renew the certificate without harm
-    zone_line = subprocess.check_output(["tlsa", "--selector", str(TLSA.Selector.SubjectPublicKeyInfo), "--certificate", crtfile, "example.org"]).decode("utf-8")
-    m = re.match("^[0-9a-zA-Z_.-]+ IN TLSA ([0-9]+) ([0-9]+) ([0-9]+) ([0-9a-zA-Z]+)$", zone_line)
-    assert m is not None
-    assert int(m.group(1)) == TLSA.Usage.EndEntity
-    assert int(m.group(2)) == TLSA.Selector.SubjectPublicKeyInfo
-    return TLSA(protocol, port, TLSA.Usage.EndEntity, TLSA.Selector.SubjectPublicKeyInfo, int(m.group(3)), m.group(4))
-
-def TLSA_for_LE(protocol = Protocol.TCP, port = 443):
-    # add both the live and (potentially) staging certificate to the letsencrypt TLSA record set
-    r = [TLSA_from_crt(protocol, port, "letsencrypt/live.crt")]
-    try:
-        r.append(TLSA_from_crt(protocol, port, "letsencrypt/staging.crt"))
-    except IOError:
-        pass
-    return r
-```
-
-Now I add `TLSA_for_LE(port = 443)` to the records of my domains.  Finally, the
-key hook just runs zonemaker and has bind reload the zone (it will automatically
-also be resigned).  Now, whenever a staging key is created, it is automatically
-added to my zone.  At least 25h later (I have the TTL set to 24h), the key gets
-unstaged, and the old TLSA record is removed from the zone.
-
-That's it!  If you have any questions, feel free to report
-[issues at GitHub](https://github.com/RalfJung/lets-encrypt-tiny/issues).