Using Let’s Encrypt with acme-client on a FreeBSD 11/Apache 2.4

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

 

5 thoughts on “Using Let’s Encrypt with acme-client on a FreeBSD 11/Apache 2.4

  1. David Gessel

    Executing, first,
    # mkdir -pm750 /usr/local/www/.well-known && chown -R www:www /usr/local/www/.well-known
    then
    # acme-client -mvnNC /usr/local/www/.well-known/acme-challenge/ domain.com http://www.domain.com

    results in:
    acme-client: /usr/local/www/.well-known/acme-challenge/: -C directory must exist

    Was acme-client expected to create the ../acme-challenge/ directory?

    Reply
    1. rpf Post author

      First of all, running acme-client requires using root, so make sure you’re logged in as root or use sudo.

      Assuming you set /usr/local/www/.well-known up as your challenge directory in Apache, you would create that folder manually with the first command (again, run as root). This directory needs to exist permanently because you will need it to renew your certificates.

      Except that WordPress is injecting http:// in front of the www in your command, it looks like you’re doing it correctly. In the future, running

      acme-client -mv -C /path/to/challenge/directory/ domain www. domain.com (removed space between www. and domain.com)

      should renew your certificate if you’re near expiration.

      Reply
  2. David Gessel

    Thanks! Yes, running as root and funny – didn’t notice the WP insert until you mentioned it. Sometimes convenience can be a complication.

    Yes, running
    # mkdir -pm750 /usr/local/www/.well-known/acme-challenge && chown -R www:www /usr/local/www/.well-known/acme-challenge
    Seemed to clear that hurdle. Then I ran into

    acme-client: https://acme-v01.api.letsencrypt.org/acme/challenge/h_2462QzmjXdshgAoW9hhEm1BsgdlCblSpuc-_EiuKg/2617044563: bad response
    acme-client: transfer buffer: [{ “type”: “http-01”, “status”: “invalid”, “error”: { “type”: “urn:acme:error:unauthorized”, “detail”: “Invalid response from http://blackrosetech.com/.well-known/acme-challenge/xFv8GQsOmmH9Xb7ilLbEwm6HdLK2V7r9hN7CehmX88Y: \”\u003c?xml version=\”1.0\” encoding=\”UTF-8\”?\u003e\n\u003c!DOCTYPE html PUBLIC \”-//W3C//DTD XHTML 1.0 Strict//EN\”\n \”http://www.w3.org/TR/xhtml1/D\””, “status”: 403 }, “uri”: “https://acme-v01.api.letsencrypt.org/acme/challenge/h_2462QzmjXdshgAoW9hhEm1BsgdlCblSpuc-_EiuKg/2617044563”, “token”: “xFv8GQsOmmH9Xb7ilLbEwm6HdLK2V7r9hN7CehmX88Y”, “keyAuthorization”: “xFv8GQsOmmH9Xb7ilLbEwm6HdLK2V7r9hN7CehmX88Y.avuwT15Qa6f3G4JGQhZwkIQs2eeNN6_EAtalAI2qhtg”, “validationRecord”: [ { “url”: “http://blackrosetech.com/.well-known/acme-challenge/xFv8GQsOmmH9Xb7ilLbEwm6HdLK2V7r9hN7CehmX88Y”, “hostname”: “blackrosetech.com”, “port”: “80”, “addressesResolved”: [ “173.228.36.130” ], “addressUsed”: “173.228.36.130”, “addressesTried”: [] } ] }] (1049 bytes)
    acme-client: bad exit: netproc(36406): 1

    which I’m thinking might either be because of IPV6 or 1:1 NAT (80 and 443 are forwarded to 80/443)… still working on that, but thank you for the helpful guide!

    Reply
    1. rpf Post author

      Looks like a web server configuration issue, specifically for domain.com’s token. If your domain is already accessible to the outside world, you should be in the clear with NAT.

      Your local acme-client puts a token in the .well-known/acme-challenge folder. From the outside, whatever is in both domain.com/.well-known/acme-challenge/ and http://www.domain.com/.well-known/acme-challenge/ should be accessible. LetsEncrypt needs to be able to see the token your client generates, and this in turn is used to prove you have authority over the domain. The token is created for this process and then deleted when LetsEncrypt approves the certificate, so usually the folder sits empty. A token should be created for both domain.com and double-u double-u double-u dot domain.com.

      My tactic is to just forward all traffic from domain.com/ to http://www.domain.com, but in that case you need to make sure the forward is configured correctly. http://www.domain.com/.well-known/acme-challenge is accessible.

      Reply
  3. David Gessel

    I had two errors:
    A) the command
    # acme-client -mvnNC /usr/local/www/.well-known/acme-challenge/ blackrosetech.com http://www.blackrosetech.com

    has a trailing slash which resulted in an error I didn’t notice until I looked carefully:
    acme-client: /usr/local/www/.well-known/acme-challenge//AWluaZkVuxX0LVejYE5B3zN0KvoIg4l2UpeEGUJ7msc: created

    note the double slash before the long string file name.

    B)….
    did not execute
    # apachectl restart
    after modifying httpd.conf
    doh! works now.

    certificate verified. Thanks for the great how-to!

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *