Back to post index

Let's Encrypt all the things!
Tags: [letsencrypt] [encryption] [openbsd]
Published: 12 Dec 2015 21:09

Let’s Encrypt entered public beta recently, offering a free and automated resource for getting signed certificates for your domain. I jumped at the chance to test this on the OpenBSD VPS that hosts this site and answers @operand.ca email. Here’s how I did it:

I chose the awesome acme-tiny Let’s Encrypt client because it is a tiny (auditable) Python script that performs all the required steps for getting one of these certificates. The only prerequisites are Python and OpenSSL.

overview

There are two places that these certificates will be used: for HTTPS and SMTPS connections. For HTTPS, I’m going to use stunnel in front of the existing varnish cache to provide a SSL endpoint - varnish does not support SSL / TLS yet (perhaps never). For SMTPS, OpenSMTPD has TLS baked in.

getting certificates with acme-tiny

In the directory /root/letsencrypt/, generate the appropriate key and CSR files:

openssl genrsa 4096 > account.key
openssl genrsa 4096 > domain.key
openssl req -new -sha256 -key domain.key -subj "/CN=operand.ca" > domain.csr

In /root/letsencrypt/, I created run.sh:

#!/bin/ksh

RUN_DIR="/root/letsencrypt"
WEB_DIR="/var/www/htdocs/default/.well-known/acme-challenge"
CERT_DIR="/etc/stunnel"

mkdir -p "${WEB_DIR}"

[ -e "${RUN_DIR}" ] || exit 1
[ -e "${WEB_DIR}" ] || exit 1

python2.7 /home/jwm/src/acme-tiny/acme_tiny.py \
        --account-key "${RUN_DIR}/account.key" \
        --csr "${RUN_DIR}/domain.csr" \
        --acme-dir "${WEB_DIR}" > "${RUN_DIR}/signed.crt"
rc=${?}

if [ ${rc} -ne 0 ];
then
        echo "acme_tiny.py failed with ${rc}"
        exit 1
fi

if [ ! -e "${RUN_DIR}/intermediate.pem" ];
then
        echo "Downloading intermediate.pem"
        ftp -o "${RUN_DIR}/intermediate.pem" https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem
        rc=${?}

        if [ ${rc} -ne 0 ];
        then
                echo "ftp failed with ${rc}"
                exit 1
        fi
fi

cat "${RUN_DIR}/signed.crt" "${RUN_DIR}/intermediate.pem" > "${RUN_DIR}/chained.pem"

cp "${RUN_DIR}/chained.pem" "${CERT_DIR}/stunnel.pem"
cp "${RUN_DIR}/domain.key" "${CERT_DIR}/stunnel.key"

chmod 0600 "${CERT_DIR}/stunnel.pem"
chmod 0600 "${CERT_DIR}/stunnel.key"

# restart all services that use these keys
rcctl restart stunnel
rcctl restart smtpd

exit 0

This script:

stunnel configuration

Here’s my stunnel.conf:

chroot = /var/stunnel/
; It is recommended to drop root privileges if stunnel is started by root
setuid = _stunnel
setgid = _stunnel

; PID file is created inside the chroot jail (if enabled)
pid = /stunnel.pid

output = /stunnel.log
debug = info

[https]
accept  = 443
connect = 80
cert = /etc/stunnel/stunnel.pem
key = /etc/stunnel/stunnel.key

Enable it with

# rcctl enable stunnel

httpd and varnish require no changes - stunnel sits in front of varnish so the cache is still functional.

Open up the HTTPS port with the following pf rule:

pass in on $ext_if inet proto tcp to ($ext_if) port 443

opensmtpd configuration

Here are the changes required for /etc/mail/smtpd.conf:

-listen on vio0
+pki operand.ca certificate "/etc/stunnel/stunnel.pem"
+pki operand.ca key "/etc/stunnel/stunnel.key"
+
+listen on vio0 secure pki operand.ca hostname operand.ca

After restarting smtpd, look for TLS sessions in the /var/log/maillog:

Dec 10 21:10:55 enamel smtpd[19313]: smtp-in: Started TLS on session
759fc069292a4848: version=TLSv1.2, cipher=ECDHE-RSA-AES256-GCM-SHA384,
bits=256

There are no pf rule changes required: STARTTLS is advertised by OpenSMTPD on the same smtp port (SMTPS is deprecated). Unknown endpoints still need to pass spamd before they can communicate with OpenSMTPD because spamd does not perform any part of the SMTP protocol except a barebones handshake, and this includes advertising any STARTTLS capability.

testing and verification

ssl-tools.net provides the ability to test these two new encryption protocols:

https://ssl-tools.net/webservers/operand.ca
https://ssl-tools.net/mailservers/operand.ca

The webserver test found that the weak ECDHE_RSA_WITH_RC4_128_SHA cipher is supported by stunnel. To only allow strong ciphers, modify stunnel.conf:

 debug = info

 +; https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
 +ciphers =
 ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS

 [https]

The webserver test also found that HSTS was not offered. Modifying httpd.conf fixed this:

 server "default" {
         listen on lo0 port 8080
+        hsts
         root "/htdocs/default"
 }

The mailserver test did not find problems.

blocking IPv6 traffic

I noticed the following in stunnel’s log file:

21:23:58 LOG5[0]: Service [https] accepted connection from IPv4:38534
21:23:58 LOG5[1]: Service [https] accepted connection from IPv4:38536
21:23:58 LOG5[0]: Service [https] connected remote server from ::1:22429
21:23:58 LOG5[1]: Service [https] connected remote server from 127.0.0.1:13247
21:24:03 LOG5[1]: Connection closed: 0 byte(s) sent to SSL, 0 byte(s) sent to socket
21:24:07 LOG5[0]: Connection closed: 232 byte(s) sent to SSL, 381 byte(s) sent to socket

The HTTPS connections took on the order of seconds to complete, and stunnel seemed to be using IPv6 when varnish was not configured for it. Blocking IPv6 with pf caused the connections to complete quickly:

block quick on lo inet6
block quick on $ext_if inet6

automation

I put a call for /root/letsencrypt/run.sh into /etc/daily.local so that (unnecessarily) a new certificate is generated every day. I did this because I wanted the process to be easily scriptable and already the daily.local run showed errors:

/root/letsencrypt/run.sh[15]: python2.7: not found
acme_tiny.py failed with 127

Once these errors are fixed, the call to run.sh can be moved into /etc/weekly.local or /etc/monthly.local - Let’s Encrypt certificates expire after 90 days so this is mandatory.

offering only HTTPS resources

If all resources on a page are provided over HTTPS, Firefox shows a closed lock:

If any resource on a page is grabbed by a HTTP connection, Firefox will show a warning:

I had to verify each blog page to make sure that no resources were explicitly offered over HTTP, like in the case of page on Smyslov’s fans.