Let's Encrypt

Noah Petherbridge
kirsle
Posted by Noah Petherbridge on Wednesday, December 30 2015 @ 11:36:02 AM

The free SSL certificate authority Let's Encrypt went into public beta earlier this month, and I updated all of my sites to use SSL now. I still had several more months before kirsle.net's old certificate from Namecheap expired, but I switched to the Let's Encrypt certificate because I could include all my subdomains instead of only the www one.

Check out their website and get free SSL certificates for your sites, too. I'm just writing this blog with some personal tips for how I configured my nginx server and a Python script I wrote to automate the process of renewing my certificates every month (Let's Encrypt certs expire every 90 days).

nginx config

Because all of my sites now use SSL encryption, and nginx has a handful of options for configuring the cipher suites and other settings (you need to play around with these to get a good score on the SSL Labs security test), I created a file named /etc/nginx/ssl_params to put all of the common parameters. Then, each individual site includes that file.

/etc/nginx/ssl_params

# Common SSL security settings
ssl_session_timeout 5m;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_dhparam /etc/ssl/kirsle.net-2015/dhparams.pem;

# So the Acme client can use the htdocs method
location /.well-known {
    alias /var/www/html/.well-known;
}

Notice how I also made a global alias to point the /.well-known URI to the /var/www/html/.well-known path. This is useful for running the ACME client (I use the standard letsencrypt-auto client); I can use the "web root" method and then I don't need to stop my nginx web server (I don't trust a program to automatically edit my nginx config, and I don't want to bring down all my sites in order to run letsencrypt-auto in stand-alone mode, where it runs its own server temporarily).

And then, for my websites that use SSL certs, I include the ssl_params file so that I don't need to manage redundant settings in a dozen different places. Here's an example for my rpm.kirsle.net subdomain, which is where I put random Linux packages:

server {
    server_name rpm.kirsle.net;
    listen [2604:a880:1:20::46:5004]:443 ipv6only=on;
    listen 443 ssl;

    ssl on;
    ssl_certificate /etc/letsencrypt/live/www.kirsle.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.kirsle.net/privkey.pem;
    include ssl_params;

    root /home/kirsle/subdomains/rpm;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
        autoindex on;
    }
}

# Forward port 80 to SSL
server {
    server_name rpm.kirsle.net;
    listen [2604:a880:1:20::46:5004]:80 ipv6only=on;
    listen 80;
    return 301 https://rpm.kirsle.net$request_uri;
}

Get a certificate without stopping your web server

As touched on before, I didn't want to have to do service nginx stop in order to run the letsencrypt-auto client in standalone mode. (The reason I would have to is that many of my sites are Python web apps, so they don't have a traditional document root directory like some simpler sites would, such as many sites that would be hosted by an Apache server).

So for example, on Kirsle.net the majority of requests are handled directly by the Python app, so there is no root document directory (I make aliases for certain subdirectories, though, for serving static files because this is better handled by nginx than by Python). So, a workaround is to configure your nginx server to alias the /.well-known URI to a document root on disk, because the ACME client will want to create files underneath the /.well-known/acme-challenge/ directory to prove ownership of your domain name.

To keep things simple and DRY, I put that URI alias in the shared SSL parameters file.

Automatically renew certificates

Let's Encrypt is still in beta at the time of writing, and they haven't yet implemented a way to automatically renew certificates before they expire; and Let's Encrypt certificates expire every 90 days, unlike most traditional certificate authorities that give you 12 month leases at a time.

So in the mean time, I wrote a simple Python script for myself that renews all my certificates regularly. I installed it in my root user's crontab on my server, to run the script on the first of every month. The script checks all the existing certificates to see if any should be renewed (if they're at least a month old), and runs the letsencrypt-auto client to update them and restart my web server.

Here's the source code. For the raw plain text version, check https://sh.kirsle.net/renew-certs.py. There's a configurable section at the top of the script that can be edited to put in your own domain names.

#!/usr/bin/env python3

# Cron script to renew LetsEncrypt certificates.
#
# --Kirsle
# https://sh.kirsle.net/

import os
import subprocess
import time

################################################################################
# Configuration Section Begins                                                 #
################################################################################

# Let's Encrypt directories
LE_APPDIR = "/opt/letsencrypt"       # Where `letsencrypt-auto` lives
LE_CERTS  = "/etc/letsencrypt/live"  # Where live certificates go

# Common arguments to letsencrypt-auto
COMMON = ["./letsencrypt-auto", "certonly", "--renew",
          "--webroot", "-w", "/var/www/html"]

# Domains and their subdomains; one array element per certificate, with each
# array element being a list of domains to include in the same cert.
CERTS = [
    [ "www.kirsle.net", "kirsle.net", "www.kirsle.com", "kirsle.com",
      "www.kirsle.org", "kirsle.org", "sh.kirsle.net", "rpm.kirsle.net",
      "minecraft.kirsle.net", "mc.kirsle.net", "rophako.kirsle.net" ],
    [ "noah.is", "www.noah.is", "petherbridge.org", "www.petherbridge.org",
      "noah.petherbridge.org", "noahpetherbridge.com",
      "www.noahpetherbridge.com" ],
    [ "rivescript.com", "www.rivescript.com", "static.rivescript.com" ],
]

# Minimum lifetime for certificate before renewing it?
LIFETIME = 60*60*24*30  # Once a month.

# Command to run after finishing if certs were renewed.
RESTART_COMMAND = ["service", "nginx", "reload"]

################################################################################
# End Configuration Section                                                    #
################################################################################

def main():
    os.chdir(LE_APPDIR)

    # If any certs were renewed, we'll schedule the restart command at the end.
    any_renewed = False

    # See which certificates are ready to be renewed.
    print("Checking SSL certificates for renewal")
    for cert in CERTS:
        ready = False  # Ready to renew this one
        primary = cert[0]

        # Find its existing live certificate file.
        home = os.path.join(LE_CERTS, primary)
        chain = os.path.join(home, "cert.pem")

        # When was it last modified?
        if not os.path.isfile(chain):
            print("NOTE: No existing cert file found for {} ({})".format(
                primary,
                chain,
            ))
            ready = True
        else:
            mtime = os.stat(chain).st_mtime
            if time.time() - mtime > LIFETIME:
                print("Cert for {} is old; scheduling it for renewal!"\
                    .format(primary))
                ready = True

        # Proceed?
        if ready:
            print("Renewing certificate for {}...".format(primary))
            command = []
            command.extend(COMMON)

            # Add all the domains.
            for domain in cert:
                command.extend(["-d", domain])

            print("Exec: {}".format(" ".join(command)))
            subprocess.call(command)
            any_renewed = True

    # If any certs were changed, restart the web server.
    if any_renewed:
        print("Restarting the web server: {}".format(" ".join(RESTART_COMMAND)))
        subprocess.call(RESTART_COMMAND)

if __name__ == "__main__":
    main()

Categories:

[ Blog ]

Comments

There is 1 comment on this page.

Avatar
guest
Posted on Wednesday, January 06 2016 @ 05:29:13 PM by Windigo.

Thanks a lot - your post had the in-depth details of the Let's Encrypt process I needed to get my own hacked-together solution up and running.

I also see you have a Minecraft server - I'm half convinced I should mine you a few blocks of diamond in thanks. Hmm...

Either way, thanks a bunch!

Add a Comment

Your name:
Your Email:
Message:
Comments can be formatted with Markdown, and you can use
emoticons in your comment.

If you can see this, don't touch the following fields.



Kirsle
Channels
Creativity
Software
Web Tools
Subdomains
Miscellany
Links


Fan Club