Securing your website using Let's Encrypt and automated renewals

Posted on 08/08/2017 by Brian Carey

Let's Encrypt is a free, automated certificate authority available to anyone.  However, unlike going to Godaddy and purchasing a certificate thats valid for a year or more Let's Encrypt certificates are only valid for 90 days, so having the proper automation in place to easily renew them is a must.  Today we're going to cover our approach to managing this.  We've been implementing a lot of certs recently using this service and are very impressed with its functionality, especially the ability to automate the certificate renewals in conjunction with existing Apache configurations.  The following example will show how to go from the initial certificate request to automated renewals on a FreeBSD system running Apache.  Linux systems running Apache would be essentially the same with a few minor path adjustments.

Prerequisites

Before getting started, we need to install the Certbot tool used to interact with the certificate authority.  On FreeBSD this is as simple as the following:

pkg install py27-certbot

Linux would be very similar, for example on CentOS you would do the following, assuming you already have the EPEL repo installed:

yum install certbot

Initial Apache configuration & certificate request

Now that you have certbot installed, its time to make our initial certificate request.  For this example, we've setup a test domain and Virtual Host in Apache that is currently only available using HTTP.

<VirtualHost *:80>
    DocumentRoot /var/www/test.kissitconsulting.com
    ServerName  test.kissitconsulting.com
    <Directory />
        Options +FollowSymLinks
        Require all granted
        AllowOverride None
    </Directory>
    <Directory /var/www/test.kissitconsulting.com>
        Options +FollowSymLinks +Indexes
        Require all granted
AllowOverride None
    </Directory>
</VirtualHost>

Once the URL is verified working, we can make the initial cert request using certbot as follows:

certbot certonly --webroot -w /var/www/test.kissitconsulting.com -d test.kissitconsulting.com

Here we're using the webroot plugin to have certbot use our existing apache server to validate the request over HTTP by writing its check files to the webroot.  This is important for the automated renewal process.  Assuming this is the first time using certbot on your server, you'll first be prompted for an email for notifications and to accept the Terms of Service.  The whole process should look like the following:

root@certbot-test:~ # certbot certonly --webroot -w /var/www/test.kissitconsulting.com -d test.kissitconsulting.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel):example@email.com
-------------------------------------------------------------------------------
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf. You must agree
in order to register with the ACME server at
https://acme-v01.api.letsencrypt.org/directory
-------------------------------------------------------------------------------
(A)gree/(C)ancel: A
-------------------------------------------------------------------------------
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about EFF and
our work to encrypt the web, protect its users and defend digital rights.
-------------------------------------------------------------------------------
(Y)es/(N)o: N
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for test.kissitconsulting.com
Using the webroot path /var/www/test.kissitconsulting.com for all unmatched domains.
Waiting for verification...
Cleaning up challenges
IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /usr/local/etc/letsencrypt/live/test.kissitconsulting.com/fullchain.pem.
   Your cert will expire on 2017-11-07. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:
   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF: 

Note that the certificate file is listed in the IMPORTANT NOTES section of the output.  In this case, our certificate file is /usr/local/etc/letsencrypt/live/test.kissitconsulting.com/fullchain.pem.  Similarly, the matching private key is also located in the same directory at /usr/local/etc/letsencrypt/live/test.kissitconsulting.com/privkey.pem.  Note that you should always reference the files in this location as they are symlinks that point to the current version of the certs.

At this point, we can now add the HTTPS configuration to our Vhost:

<VirtualHost *:443>
    DocumentRoot /var/www/test.kissitconsulting.com
    ServerName  test.kissitconsulting.com
    <Directory />
        Options +FollowSymLinks
        Require all granted
        AllowOverride None
    </Directory>
    <Directory /var/www/test.kissitconsulting.com>
        Options +FollowSymLinks +Indexes
        Require all granted
	AllowOverride None
    </Directory>

    ## SSL Config
    SSLEngine on
    SSLCertificateFile "/usr/local/etc/letsencrypt/live/test.kissitconsulting.com/fullchain.pem"
    SSLCertificateKeyFile "/usr/local/etc/letsencrypt/live/test.kissitconsulting.com/privkey.pem"
    SSLOptions +StrictRequire
</VirtualHost>

Assuming all went well, we now have both HTTP and HTTPS working, great!

The certificate renewal process

The renewal process uses a similar command to the initial request.  It will check all active certificates on the server for needed renewals, and perform each as needed.  As previously mentioned this requires that the server be serving the webroot over HTTP for the check files to be accessible to the provider.  

For example, a real renewal check currently responds with a message saying its not yet due:

root@certbot-test:~ # certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

-------------------------------------------------------------------------------
Processing /usr/local/etc/letsencrypt/renewal/test.kissitconsulting.com.conf
-------------------------------------------------------------------------------
Cert not yet due for renewal

The following certs are not due for renewal yet:
  /usr/local/etc/letsencrypt/live/test.kissitconsulting.com/fullchain.pem (skipped)
No renewals were attempted.

Certbot provides a "dry run" feature, which we'll use here to demonstrate what a renewal would look like:

root@certbot-test:~ # certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

-------------------------------------------------------------------------------
Processing /usr/local/etc/letsencrypt/renewal/test.kissitconsulting.com.conf
-------------------------------------------------------------------------------
Cert not due for renewal, but simulating renewal for dry run
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for test.kissitconsulting.com
Waiting for verification...
Cleaning up challenges

-------------------------------------------------------------------------------
new certificate deployed without reload, fullchain is
/usr/local/etc/letsencrypt/live/test.kissitconsulting.com/fullchain.pem
-------------------------------------------------------------------------------
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /usr/local/etc/letsencrypt/live/test.kissitconsulting.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)

IMPORTANT NOTES:
 - Your account credentials have been saved in your Certbot
   configuration directory at /usr/local/etc/letsencrypt. You should
   make a secure backup of this folder now. This configuration
   directory will also contain certificates and private keys obtained
   by Certbot so making regular backups of this folder is ideal.

Once a certificate renewal takes place, you'll need to restart Apache to pick up the updated file.

Forcing HTTPS without breaking certbot

Its common nowadays for all HTTP requests to be redirected to HTTPS to ensure users are accessing a site securely when simply typing the URL into a browser.  In most cases this is done via a catch all redirect in Apache that sends everything hitting a Virtual Host via HTTP to HTTPS.  However, doing this will break subsequent Certbot renewal requests.  And we don't want to have to manually change configurations or anything else service impacting when needing to renew our certs.

As per the certbot documentation, it writes its check files to a hidden directory named .well-known in the web root directory specified when generating the certificate.  So lets add an exception to the forced HTTPS rule to allow these requests to still work over HTTP.  Our HTTP Virtual Host config now looks like this.  Note the section added at the bottom.

<VirtualHost *:80>
    DocumentRoot /var/www/test.kissitconsulting.com
    ServerName  test.kissitconsulting.com
    <Directory />
        Options +FollowSymLinks
        Require all granted
        AllowOverride None
    </Directory>
    <Directory /var/www/test.kissitconsulting.com>
        Options +FollowSymLinks +Indexes
        Require all granted
	AllowOverride None
    </Directory>

    ## Redirect all HTTP TO HTTPS *EXCEPT* Certbot requests
    RewriteEngine on
    RewriteCond %{REQUEST_URI} !^/\.well-known/.*
    RewriteRule (.*) https://test.kissitconsulting.com$1 [R=301,L]
</VirtualHost>

Automating renewals

At this point, all we need to do is add a crontab entry to run a renew check as desired.  Certbot documentation recommends this is run once per day, at a random hour and minute.  So we do something like this:

49 10 * * * /usr/local/bin/certbot renew --post-hook /usr/local/bin/certbot_renew_freebsd.sh > /tmp/certbot.log 2>&1

You'll notice another option thrown in there, --post-hook.  This points to a script to be run after a renewal happens.  We use this to restart Apache as well as receive a simple email from the server just so we are aware the renewal happened.  Any script will do, however ours is available here.  Note this one is specific to FreeBSD currently but at some time we may polish it up to make it more generic.


Thanks for your time and hope this is helpful to someone.  As always, we welcome any feedback you may have on this or any other post!