I recently opted to use the Let’s Encrypt project to handle my noncommercial TLS needs. If this works well, I will probably start pushing commercial clients over to it as well.
I couldn’t find a good guide to setting it up to use multiple domains smoothly on FreeBSD. This technique should work with a single domain or multiple domains.
About Let’s Encrypt and acme-client
Let’s Encrypt is a project to simplify, automate, and normalize the creation and maintenance of TLS certificates used by web sites.
acme-client is a C program written to interact with the Let’s Encrypt service. It was written for the OpenBSD project, but is useful on other platforms. The key argument in its favor is arguably security. There are few dependencies.
Assumptions
FreeBSD 11 is assumed here, but any recent version should work work. With some effort these instructions could surely be adapted to several other environments.
I assume you want to configure multiple sites, but this should work for a single site as well. It will simply be multi-site ready (which, to my mind, costs nothing).
I mostly use the default paths in FreeBSD for the configuration of both Apache and acme-client.
Though you don’t strictly need to use Apache to follow most of these instructions, the configuration suggests I make about challenge-response configuration are Apache-specific. Any web daemon that includes aliased folders should permit the same kind of configuration. If you’re using something besides Apache, you’ll need to adjust accordingly but this should not prevent you from using these instructions.
Even if you do use Apache, I assume you have a configuration where Apache already basically works and is compiled and configured to use encrypted HTTP. I used a pretty stock installation of Apache 2.4 from /etc/ports/www/apache24. I use FreeBSD paths and conventions throughout. FreeBSD by default uses www:www (UID/GID of 80) as the username for httpd. I tend to configure all my web sites in /usr/vhosts/ these days and create a logs/ folder and htdocs/ folder within.
I use a fairly large number of virtual hosts in Apache. By habit, I store these in /usr/vhosts/. I create a new vhost tree with a one-liner like:
export THISD="mydomain.com"; mkdir -p /usr/vhosts/$THISD/htdocs && mkdir -p /usr/vhosts/$THISD/logs && chown -R 80:80 /usr/vhosts/$THISD
Using this example would create a /usr/vhosts/mydomain.com/ directory with separate folders for logs and web content. Of course, how you organize your folders is entirely up to you.
Overview of what we want
Before I go through the steps, I want to make clear what the end result should look like. You should end up with a directory tree of TLS keys and certificates. For example:
Certificate file: /usr/local/etc/ssl/acme/richardfassett.com/cert.pem
Chain file: /usr/local/etc/ssl/acme/richardfassett.com/chain.pem
Full chain: /usr/local/etc/ssl/acme/richardfassett.com/fullchain.pem
Private key: /usr/local/etc/ssl/acme/private/richardfassett.com/privkey.pem
You shouldn’t need to do anything with these files except refer to them in relevant configurations (e.g., Apache, mail, PHP). acme-client will handle that for you.
Optional but highly recommended: the file /usr/local/etc/acme/domains.txt will contain a list of certificates. Each line in the file should contain the domains for one certificate. For example:
mydomain.com www.mydomain.com myotherdomain.com www.myotherdomain.com mail.myotherdomain.com
Each line should be unique. The importance of this file is apparently later when it’s time to write a renewal script.
Step 1: install your software
Make sure you have a working Apache configuration already configured to use TLS. This is pretty easy to do, but there are many ways to do it and it’s generally outside the scope of this guide.
I installed acme-client from ports by running:
cd /usr/ports/security/acme-client && make install
It was not available in with the pkg utility at the time, but in the future hopefully you could run pkg install acme-client.
Step 2: configure Let’s Encrypt challenge (.well-known)
To verify you own the domain, Let’s Encrypt checks a challenge-response routine. You write some data Let’s Encrypt asks you to write to a certain place on your web server. FreeBSD sets aside /usr/local/www/acme for this purpose, but I ended up using my own folder.
I ran this command:
mkdir -pm750 /usr/local/www/.well-known && chown -R www:www /usr/local/www/.well-known
Then I added this to my httpd.conf file:
<Directory "/usr/local/www/.well-known/"> Options None AllowOverride None Require all granted Header add Content-Type text/plain </Directory>
Now I can simply add the line
Alias /.well-known/ /usr/local/www/.well-known/
to relevant an un-encrypted Apache host to set up the challenge-response. My way of doing this was to configure the non-SSL site like so:
<VirtualHost 23.25.105.194:80> ServerName mydomain.com RewriteEngine On RewriteRule ^/?(.*) http://www.mydomain.com/$1 [R,L] </VirtualHost> <VirtualHost 23.25.105.194:80> ServerAdmin webmaster@megapipe.net DocumentRoot "/usr/vhosts/mydomain.com/htdocs/" ServerName www.mydomain.com ServerAlias www.mydomain.com # share well-known for renewal via Let's Encrypt! Alias /.well-known/ /usr/local/www/.well-known/ # Anything that isn't going to domain.org/.well-known gets forwarded to the https site RewriteEngine on RewriteCond %{REQUEST_URI} !^/.well-known RewriteRule (.*) https://www.mydomain.com/$1 [R=301,L] ErrorLog "/usr/vhosts/mydomain.com/logs/error_log" <Directory "/usr/vhosts/mydomain.com/htdocs/"> AllowOverride All </Directory> <IfModule mod_log_config.c> CustomLog "|/usr/local/sbin/rotatelogs -l /usr/vhosts/mydomain.com/logs/access_log-%Y-%m-%d.log 86400" combined </IfModule> </VirtualHost>
The first <VirtualHost> entry tells Apache to simply forward anything from mydomain.com to www.mydomain.com.
The second entry tells it to forward everything from http://www.mydomain.com to https://www.mydomain.com/ except the query to the .well-known folder where Let’s Encrypt will look for its challenge response. This way I never really have an unencrypted site running, except for the minimal amount I need to in order to configure acme-client.
Step 3: run the acme-client to get a basic configuration
Configure your first domain with a command like
export DS="domain.org www.domain.org"; \ acme-client -mvnNC /usr/local/www/.well-known/acme-challenge/ \ $DS && echo $DS >> /usr/local/etc/acme/domains.txt
Note: I’m doing a little inline scripting to simplify the steps necessary to creating the configuration. If you run this command twice, you may need to edit /usr/local/etc/acme/domains.txt and remove any duplicate entries. In the event that there is a problem issuing the certificate, this should prevent the domains from being added to the domains.txt file (but check anyway). Also, if you are skipping creating a renewal script or find yourself needing to run the command a second time after correcting your configuration, you can simplify this command to acme-client -mvnNC /usr/local/www/.well-known/acme-challenge/ domain.org www.domain.org
If all goes well, acme-client will do some initial configuration and create your first keys.
To issue another certificate, .e.g, for another domain, use a command like
export DS="anotherdomain.org www.anotherdomain.org"; \ acme-client -mvnNC /usr/local/www/.well-known/acme-challenge/ $DS \ && echo $DS >> /usr/local/etc/acme/domains.txt
Note: there are fewer parameters this time. We dropped the As above, you can simplify this further if you are skipping the renewal script:
Step 4: configuring Apache vhost to use the TLS certificate
Once your certficate is successfully installed you can configure Apache’s secure HTTP sites. We’ve already configured the insecure sites to forward to the secure sites above (except for the .well-known folder), so now we just need to add the secure sites.
This is completely optional, but what I like to do is forward mydomain.com to www.mydomain.com:
<VirtualHost 23.25.105.194:443> ServerName mydomain.com SSLEngine on SSLCertificateFile /usr/local/etc/ssl/acme/mydomain.com/cert.pem SSLCertificateKeyFile /usr/local/etc/ssl/acme/private/mydomain.com/privkey.pem SSLCertificateChainFile /usr/local/etc/ssl/acme/mydomain.com/fullchain.pem RewriteEngine On RewriteRule ^/?(.*) https://www.mydomain.com/$1 [R,L] </VirtualHost>
The actual configuration for mydomain.com is:
<VirtualHost 23.25.105.194:443> ServerAdmin webmaster@megapipe.net DocumentRoot "/usr/vhosts/mydomain.com/htdocs/" ServerName www.mydomain.com ServerAlias www.mydomain.com <IfModule mod_rewrite.c> RewriteEngine on </IfModule> SSLEngine on SSLCertificateFile /usr/local/etc/ssl/acme/mydomain.com/cert.pem SSLCertificateKeyFile /usr/local/etc/ssl/acme/private/mydomain.com/privkey.pem SSLCertificateChainFile /usr/local/etc/ssl/acme/mydomain.com/fullchain.pem # ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9001/usr/vhosts/mydomain.com/htdocs/$1 timeout=120 ErrorLog "/usr/vhosts/mydomain.com/logs/error_log" <Directory "/usr/vhosts/mydomain.com/htdocs/"> AllowOverride All </Directory> <IfModule mod_log_config.c> CustomLog "|/usr/local/sbin/rotatelogs -l /usr/vhosts/mydomain.com/logs/access_log-%Y-%m-%d.log 86400" combined </IfModule> </VirtualHost>
Step 5: maintain your certificates
Checking to see if your certificate needs to be renewed is as simple as running:
acme-client -mv mydomain.com www.mydomain.com
As I understand it, the certificate should update if it’s older than 60 days. You can force the certificate to update early with a command like:
acme-client -Fmv mydomain.com www.mydomain.com
(-F parameter forces the update even if it’s too soon. To either of the above commands you could also add the -b parameter, which will force current certificates to backup if there is a change.)
There are several ways you could auto-renew.
Option 1: crontab
If you just have one domain, or only a few, maybe you can live with a crontab entries instead of writing a script. If that’s the case, you might not need the domains.txt file.
This would have acme-client run every Sunday, Wednesday, and Friday at 9pm and renew if necessary.
0 21 * * 0,3,5 /usr/local/bin/acme-client -m mydomain.com www.mydomain.com 2>&1
You can have one entry for every domain.
Option 2: custom script
#!/bin/sh ### # # This script was adapted from letskencrypt.sh by Bernard Spil # See https://brnrd.eu/security/2016-12-30/acme-client.html # # To initially configure a new domain, configure # apache properly and use a command like: # ### # Define location of dirs and files DOMAINSFILE="/usr/local/etc/acme/domains.txt" CHALLENGEDIR="/usr/local/www/.well-known/acme-challenge" SSLDIR="/usr/local/etc/ssl" # is changed to 1 if any domains expired CHECKEXPIRATION=0 # Check for account key and create dir and key (-n) if required if [ ! -f "/usr/local/etc/acme/privkey.pem" ] ; then EXTRAARGS="${EXTRAARGS} -n" fi # Loop through the domains.txt file with lines like # example.org www.example.org img.example.org cat ${DOMAINSFILE} | while read domain subdomains ; do # directory where cert.pem, fullchain.pem and chain.pem are # saved (${SSLDIR}/acme/${domain} should be FreeBSD default) # when -m is is invoked CERTDIR="${SSLDIR}/acme/${domain}" # Define the name of the private key # (${SSLDIR}/acme/private/${domain}/privkey.pem should # be default with the -m option invoked) DOMAINKEY="${SSLDIR}/acme/private/${domain}/privkey.pem" # acme-client returns RC=2 when certificates # weren't changed; use set +e to capture the return code set +e # Renew the key and certs if required acme-client -mvb -C "${CHALLENGEDIR}" \ -k "${DOMAINKEY}" \ -c "${CERTDIR}" \ ${EXTRAARGS} ${domain} ${subdomains} RC=$? # now that we have the return code, set script to exit if # nonzero is returned set -e # if anything is expired, we'll want to do something # (e.g., restart HTTPS) if [ $RC -ne 2 ] ; then CHECKEXPIRATION=1 fi done if [ "$CHECKEXPIRATION" -ne "0" ] ; then service apache24 restart fi