Let's Encrypt, FreeBSD and nginx
Recently, Let’s Encrypt entered public beta. This is pretty exciting news, and we wanted to give it a try to replace our old, broken Comodo SSL certs from that free Github thingy. I was pleased and a little surprised to find that there’s a native FreeBSD Let’s Encrypt client. Unfortunately, at the time of testing it appeared that the native FreeBSD client wasn’t up to speed with the public beta and wouldn’t give me any browser-trusted certs. Similarly, the FreeBSD support of the letsencrypt-auto
is still very experimental. I tried tinkering with it for a little while but couldn’t get it to do anything useful.
One neat thing that the client offers is manual mode, accessed with the certonly
and --manual
flags. This allows you to use a machine different from the one the cert is destined for to obtain the cert. Since I couldn’t get the certs I wanted on the FreeBSD server I wanted them for, I decided to give manual mode a try. I installed letsencrypt-auto
on a Debian VM I had lying around and got started.
On Debian VM:
./letsencrypt-auto certonly --manual
In the pretty ncurses thingy, give it the domain names you want in the cert (it appears that the first name is used as the CN and following names are added as altnames) and continue. Now, you need to ACME validate control of the domain names you provided. It’ll give you something like this:
Make sure your web server displays the following content at http://[hostname]/.well-known/acme-challenge/[token] before continuing:
[full token]
Now, you can either set up your existing webserver to display this content at this location, or if you don’t have a web server up (or just don’t feel like messing with it and don’t mind a bit of downtime) run the shell stuff it gives you to set up a simple Python HTTP server to serve the token. I went for the latter, since in this context turning nginx off for a minute wasn’t a problem.
On FreeBSD webserver:
mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
cd /tmp/letsencrypt/public_html
printf "%s" [full token] > .well-known/acme-challenge/[token]
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \
"import BaseHTTPServer, SimpleHTTPServer; \
s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.serve_forever()"
Once the ACME stuff is live on the destination server one way or the other, continue on the other box. It’ll validate the challenge and, if successful, do its stuff. If you gave it multiple domain names, you’ll need to do the same validation multiple times (i.e. change what your webserver’s displaying / ctrl-C out of the Python HTTP server, put the new token in place and start it up again). If this doesn’t work, make sure that the content is actually being served at the correct location. Check firewall rules and the like. Once you’re done, it’ll give you some success text with an important path in it:
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/[hostname]/fullchain.pem. Your cert will
expire on 2016-03-04. To obtain a new version of the certificate in
the future, simply run Let's Encrypt again.
That fullchain.pem
file, along with privkey.pem
in the same directory, are what you’ll need to copy over to the destination server. I used scp
, but any similar method of secure file transfer would have worked. Stick them wherever your SSL stuff is (usually something like /etc/ssl/private
, and update your nginx confs. In my case, the updated SSL directives looked like
ssl_certificate /etc/ssl/private/[domain]/fullchain.pem
ssl_certificate_key /etc/ssl/private/[domain]/privkey.pem
Reload nginx and test the certs. Hopefully, all is well and now you’ve got nice, shiny new Let’s Encrypt certs.