(gdb) break *0x972

Debugging, GNU± Linux and WebHosting and ... and ...

Automatic SSL Certification with LetsEncrypt (and Bind9 zones)

Update: Letsencrypt is live!!! Since Fri. Dec 4th, 2015, I have valid SSL certificates :-) (Until 2016-03-03 apparently, then I'll just have to rerun the script at the bottom of this article.)

Since I launched this VPS hosting this blog, I've only used self-signed certificates. Partly because of the price of certificated certificates, partly because of the setup difficulty. But today, LetsEncrypt is about to go beta public (December 3, 2015), so it's time to get a valid HTTPS connection !

For quite a while, I thought that my setup required a "wildcard" certificate (*.0x972.info), to cover the different subdomains I manage, as well as the ones I'll add tomorrow. So I was very disappointed when I read that LetsEncrypt would not allow them! But then I checked how LetsEncrypt works: a simple shell command :-) That means that I can easily create a script that list my subdomains and pass it to letsencrypt. And tomorrow with new subdomains, I'll just relaunch the script.

letsencrypt -d example.com auth

Listing VPS subdomains

So, where can I get the list of the subdomains currently setup? (I use AlternC to manage my VPS) I should be able to query the database to that ... but I could find where AlternC stores that function. I found another quick and easy solution: with the DNS configuration files! It makes sense :-)

$ ls /var/lib/alternc/bind/zones/
pouget.me
0x972.info
$ sudo cat /var/lib/alternc/bind/zones/0x972.info
....
@ IN A 62.4.19.144
blog IN A 62.4.19.144
www IN A 62.4.19.144

With a bit of bash-script around it, we get:

get_subdomains() {
  domain=$1
  cat $BIND_ZONES/$domain \
           | grep "IN A"  \
           | cut -d" " -f1 \
           | while read subdomain
    do
       if [[ $subdomain == '@' ]]; then
         echo $domain
       else
         echo $subdomain.$domain
       fi
   done
}

Configuring LetsEncrypt

LetsEncrypt generates SSL certificates that authenticates the website you're communicating with, and encrypts the communication channel between the webserver and your computer. So the first step of the generation process is to make sure that the certificate is delivered to the website owner. To that purpose, LetsEncrypt uses a challenge/response protocol: the certification server tries to access an URL on the domain to certified: http://blog.0x972.info/.well-known/acme-challenge/$CHALLENGE, and the webserver should answer that request with the right answer, that it's the only one to know.

LetsEncrypt fully automates this process, but it needs help. The default method asks to stop any webserver listening on port :80, and starts its own server. This method means that you need to shutdown your webserver during the certificate generation. For a one-shot that may not be impossible, but LetsEncrypt certificates are only valid 90 days, so you have to renew it ~every two months. So we need to find a better way...

... and the solution already exists, it's called the webroot authenticator. Instead of letting LetsEncrypt starts is own webserver, you give it a path where it will store its challenge-response tokens, and you're in charge of putting it online. I did it this way:

$ cat /etc/apache2/conf.d/letsencrypt.conf 
Alias /.well-known /var/www/path/to/letsencrypt/.well-known
<Directory "/var/www/path/to/letsencrypt/.well-known">
    AllowOverride All
</Directory>

I tell Apache to create on every virtual host an alias directory, named /.well-known that points to /var/www/path/to/letsencrypt/.well-known. This directory has to be reachable and readable by Apache, and LetsEncrypt needs a read-write access (this part is easy, you run it as root !).

Then, just run LetsEncrypt with the following command (not that --webroot-path is not exactly the alias path):

sudo letsencrypt $DOMAINS auth --email $EMAIL  -a webroot --webroot-path /var/www/path/to/letsencrypt --renew-by-default

Configuring Apache

LetEncrypt put everything you need into /etc/letsencrypt/live/$DOMAIN:

$ sudo ls /etc/letsencrypt/live/0x972.info
cert.pem  chain.pem  fullchain.pem  privkey.pem

In Apache configuration, you'll need to add the following lines, either in the global configuration or in the virtual host parts:

SSLEngine on
SSLCaCertificatePath /etc/ssl/certs # not part of LetsEncrypt
SSLCertificateFile    /etc/letsencrypt/live/0x972.info/cert.pem
SSLCertificateKeyFile etc/letsencrypt/live/0x972.info/privkey.pem
SSLCertificateChainFile etc/letsencrypt/live/0x972.info/chain.pem

Finally reload Apache and your certificate should be live! (These certificates are not valid, don't forget that, you'll have to regenerate them after Dec, 3rd.)

Automating Everything

Finally, we need to script all of that for the automatic renewal. Nothing to to in Apache, the alias can stay here. I just have to skip some of the subdomains of the DNS that I don't use anymore:

#! /bin/bash

# Make sure only root can run our script
if [[ $EUID -ne 0 ]]; then
   echo "This script must be run as root" 1>&2
   exit 1
fi

TO_SKIP=("to_skip.0x972.info" "to_skip_2.0x972.info")
BIND_ZONES=/var/lib/alternc/bind/zones/
WEBROOT_PATH=/var/www/path/to/letsencrypt

letsencrypt() {
  letsencrypt $* auth --agree-dev-preview --renew-by-default -a webroot --webroot-path $WEBROOT_PATH
}

get_subdomains() {
  domain=$1
  cat $BIND_ZONES/$domain \
              | grep "IN A" \
              | cut -d" " -f1 \
              | while read subdomain
    do
       if [[ $subdomain == '@' ]]; then
         echo $domain
       else
         echo $subdomain.$domain
       fi
   done
}

get_all_subdomains() {
  for domain in $(ls $BIND_ZONES)
  do
    get_subdomains $domain | while read subdomain
    do
      echo $subdomain
    done
  done
}

subdomains_to_letsencrypt_opt() {
  while read subdom
  do
    if [[ " ${TO_SKIP[@]} " =~ " ${subdom} " ]]
    then
      continue
    fi
    echo "-d $subdom"
  done
}

letsencrypt $(get_all_subdomains | subdomains_to_letsencrypt_opt) 

Tested in Debian GNU/Linux 7 (wheezy).