TLS and SNI with OpenBSD HTTPd and LetsEncrypt

May 7, 2017 by Thomas A. Frederiksen

Having wanted to switch all the sites on my OpenBSD server to HTTPS only for quite some time, but with proper certificates and at no expense, the inclusion of acme-client in OpenBSD prompted me to actually start doing something about it. Since then I’ve spent some time working on an acceptably modern set of ciphers and options, as well as automation of renewals and whatnot. Here’s a rough set of notes for how I did it, allowing me to forget it all again.

Configuring httpd for letsencrypt, tls, and redirection

prefork 10

server "default" {
        listen on * port 80
        listen on :: port 80
}

# Redirect to HTTPS

server "domain.tld" {
        listen on * port 80
        listen on :: port 80
        alias "www.domain.tld"
        block return 301 "https://$SERVER_NAME$REQUEST_URI"

        location "/.well-known/acme-challenge/*" {
            root "/acme"
            root strip 2
        }
}   

# Define HTTPS sites

server "domain.tld" {
    listen on * tls port 443
    listen on :: tls port 443
    root "/domain.tld"
    alias "www.domain.tld"
    directory index index.html

    # Define server-specific log files relative to /logs
    log { access "domain-access.log", error "domain-error.log" }

    tls certificate "/etc/ssl/domain.tld.fullchain.pem"
    tls key "/etc/ssl/private/domain.tld.key"
    tls ciphers "CHACHA20,AES,!kRSA,!aNULL,!DH,!3DES,!SHA,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-ECDSA-AES256-SHA384"
    tls ticket lifetime default

    hsts max-age 16000000
    hsts preload
    hsts subdomains

    location "/.well-known/acme-challenge/*" {
            root "/acme"
            root strip 2
    }
}       

Using the above example as a template, it’s easy to generate an HTTP section and an HTTPS section for each site hosted on the server - SNI happens as if by magic. Thank you OpenBSD devs.

A few notes on some of the specific clauses:

listen on {...}

Note the separate lines for IPv4 (listen on *) and IPv6 (listen on ::). Rather than using those, specifying a specific network interface is also an option, see the httpd.conf man page.

block return 301 "https://$SERVER_NAME$REQUEST_URI"

This is where the redirect to HTTPS magic happens. Using the $SERVER_NAME variable ensures that the redirect works with SNI, and using the $REQUEST_URI variable passes the full request path along as well, ensuring that whatever you might want to run actually works.

location "/.well-known/acme-challenge/\*" ...

This section is pretty well explained in the acme-client man page.

tls ciphers "CHACHA20,AES,!kRSA,!aNULL,!DH,!3DES,!SHA,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-ECDSA-AES256-SHA384"

This cipher list ensures the best combination I’ve found when it comes to working with recent clients and good security, while setting the defining line for “recent” to something fairly sane. The server preferred cipher order is ECDHE-RSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305-OLD ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-SHA256

tls ticket lifetime default 

This enables TLS session resumption using TLS session tickets. Sadly there’s no caching IDs yet according to the Qualys SSL Labs SSL Server Test, but since TLS session tickets were introduced in OpenBSD 6.1 there’s hope that it will follow in later releases.

hsts ...

Setting up HTTP Strict Transport Security with a max-age of 16000000 seconds and enabling preload seemed a good idea.

Configuring the acme-client and automating key renewal

Start by configuring the sites in httpd, but leaving the TLS-enabled ones disabled as the keys have yet to be generated (it won’t start without them, so you’ll probably notice). With a running httpd set up the acme-client configuration:

authority letsencrypt {
    agreement url "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"
    api url "https://acme-v01.api.letsencrypt.org/directory"
    account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
    agreement url "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"
    api url "https://acme-staging.api.letsencrypt.org/directory"
    account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

domain  domain.tld {
    alternative names { www.domain.tld }
    domain key "/etc/ssl/private/domain.tld.key"
    domain certificate "/etc/ssl/domain.tld.crt"
    domain full chain certificate "/etc/ssl/domain.tld.fullchain.pem"
    sign with letsencrypt
    challengedir "/var/www/acme"
}

With config and httpd prepared, it’s time to create a letsencrypt account and request certificates:

# acme-client -vAD example.com

Provided this completes with no errors, it’s time to enable the TLS-enabled site definition in httpd.conf and restart httpd.

Automating key renewal

I’ve opted for a simple shell script with way less error handling than it should have - YMMV. Here’s the script:

#!/bin/sh

# Renew certificates
/usr/sbin/acme-client domain.tld

# Reload httpd
/usr/sbin/rcctl reload httpd

Without a cron definition, it’s not going to run, so I’ve added it to the crontab. Certificate renewal is on the first day of every month:

# Update letsencrypt certs
0       1       1       *       *       /usr/local/sbin/acme-update.sh

And that’s it. Only thing to do from here is to test that everything works as intended and test whether the cipher list suits the clients you’re using - I tend to use either the Qualys SSL Labs SSL Server Test or testssl.sh.

© 2017 Thomas Alexander Frederiksen | Follow on Twitter | Hucore theme & Hugo