Dev Ups

Published 2022-03-08 in certbot

Using Certbot with an Apache server

I recently posted on getting started with Nginx and Certbot. Nginx was at odds with the series I had intended to follow up on, Securing a LAMP server. This post is to show how to apply Certbot to an existing Apache installation. For reproducibility, I'm including the commands to set up Apache from a fresh VM, here I'm using Azure since I know it works nicely with DNS and Certbot.

Introduction

Pre-requisite is a working Ubuntu installation, I'll leave securing your SSH access to you. To be safe, using UFW is a lot quicker than crafting a firewall from raw iptables:

apt-get update
apt-get install -y apache2 jq

Illustration in Azure

Azure provides a different IP address for each VM we create there. We get about five certificate requests from Letsencrypt (Certbot's backend service) per IP address.

We need to allow port 80, at least until Certbot has validated its challenges to our domain ownership. In Azure, it's most expedient to do this whilst setting up an Azure VM, in the basics tab:

Azure VM basics tab. Port options

We can get a DNS name by going to the VM on the Azure Portal and clicking underneath DNS name where it is initially "Not configured":

VM menu DNS not configured

This takes us to the settings for the Azure PIP (public IP address). From here we must:

  1. Choose a "DNS name label" (subdomain name). Note the suffix under the line that is appended to make the FQDN.
  2. Click Save.

PIP menu DNS label and static address

That's all the clicking around the portal needed for now. Azure VMs, and accounts, are described in more detail here.

Continuing in the shell

We want 3 variables for the shell, or script:

  • domain_to_secure, ie domain name.
  • document_root is where requests to the domain name map to on the host's file system.
  • newusername originally I was creating a user to manage the server. For demonstration purposes it should be the user you shell into the VPS as.

If you aren't using Azure, I hope those explanations are clear enough for you to substitute. If you are using Azure this script will help set the variables correctly:

# subdomain to be unique, and what you set inside of DNS name label for an Azure VM:
subdomain="porejemplo"
az_region=`curl -H Metadata:true --noproxy "*" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" | jq -r .compute.location`
domain_to_secure="$subdomain.$az_region.cloudapp.azure.com"
echo $domain_to_secure
document_root="/var/www/${domain_to_secure}/html"
newusername="azureuser"

We provide the necessary files, folders, and vhost configuration for Apache:

mkdir -p ${document_root}
chown -R $newusername:$newusername ${document_root}
chmod -R 755 ${document_root}
echo "hello <sub>world</sub> from $domain_to_secure" > /var/www/${domain_to_secure}/html/index.html

cat << EOF > /etc/apache2/sites-available/${domain_to_secure}.conf
<VirtualHost *:80>
    DocumentRoot ${document_root}
    ServerName ${domain_to_secure}
</VirtualHost>
<VirtualHost *:443>
    DocumentRoot ${document_root}
    ServerName ${domain_to_secure}
</VirtualHost>
EOF
cat /etc/apache2/sites-available/${domain_to_secure}.conf

We need port 80 so Certbot can verify us and certify SSL on port 443. If we specify the --hsts option, Certbot will update this configuration to prevent unencrypted HTTP.

We need security parameters, which can be lifted from mozilla after dropping the vhosts blocks:

cat << EOF > /etc/apache2/conf-available/${domain_to_secure}.conf
# https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=modern&openssl=1.1.1k&guideline=5.6
# modern configuration
SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2
SSLHonorCipherOrder     off
SSLSessionTickets       off

SSLUseStapling On
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
EOF
cat /etc/apache2/conf-available/${domain_to_secure}.conf

We need to configure and reload apache:

a2enmod ssl
a2enmod headers
a2enmod rewrite
a2ensite ${domain_to_secure}
a2enconf ${domain_to_secure}
a2dissite 000-default
systemctl reload apache2

Introducing Certbot

Certbot is our automating friend. Certbot takes the burden from our shoulders, particularly regarding certificate renewal. If you are freeloading on a cloud providers domain name and you attempt to grab the www subdomain too, this will fail:

apt-get install -y certbot python3-certbot-apache
certbot --apache --agree-tos --email <for-expiry-notification> --hsts -n --redirect -d ${domain_to_secure} -d www.${domain_to_secure}

In case of problems, check your domain with server-daten.de. The site is asynchronous, you need to submit your URL then wait, and "find" it. It will update the sections as results come in. This will also grade your site. The above invocation earned an A.

The same site earned the top A+ at ssllabs, https://www.ssllabs.com/ssltest/analyze.html:

A+ rating from ssllabds.com

The combination of --hsts (to prohibit HTTP connections) and --redirect resulted in certbot generating the following vhosts configuration:

<VirtualHost *:80>
    DocumentRoot /var/www/pegasuse.eastus.cloudapp.azure.com/html
    ServerName pegasuse.eastus.cloudapp.azure.com
RewriteEngine on
RewriteCond %{SERVER_NAME} =pegasuse.eastus.cloudapp.azure.com
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
<VirtualHost *:443>
    DocumentRoot /var/www/pegasuse.eastus.cloudapp.azure.com/html
    ServerName pegasuse.eastus.cloudapp.azure.com
    SSLCertificateFile      /etc/letsencrypt/live/pegasuse.eastus.cloudapp.azure.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/pegasuse.eastus.cloudapp.azure.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Header always set Strict-Transport-Security "max-age=31536000"
</VirtualHost>

This does a lot in a few lines.

Experimenting with Certbot

Letsencrypt, Certbot's certificate provider, is rate limited per IP address. Experimenting with Certbot is far easier on somebody else's IP address, Azure's.

certbot certificates
certbot delete

Reapply the original heredoc given above, and reload apache2.

Renewing with Certbot

Certbot includes automated certificate renewal (check with systemctl list-timers). There is no need to create a hook to reload Apache. The Certbot service doesn't and Apache continues to serve the latest certificates.

Tests

Depends on what you're expecting, but testing is best automated with curl, not least because it isn't caching as much as in a browser.

Redirect to HTTP:

azureuser@cedric:~$ curl cedricsite.westeurope.cloudapp.azure.com
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

Response from functioning HTTPS:

azureuser@cedric:~$ curl https://cedricsite.westeurope.cloudapp.azure.com
hello <sub>world</sub> from cedricsite.westeurope.cloudapp.azure.com

Testing with the browser is another solution if you can be sure you're avoiding stale cache hits. Getting a response, the state of the padlock (like a red strike through it) and specific details of the certificate are all easily observable through these very manual methods.

Use with existing certs

Besides the default, and renew arguments to Certbot, we can also specify certonly. This obtains, or renews a certificate without installing it. This could then form part of a script to certificate a server that isn't currently managed by Certbot.

root@testvm:~# certbot --apache certonly -n -d zalenko.eastus.cloudapp.azure.com
root@testvm:~# certbot --apache certonly -n -d zalenko.eastus.cloudapp.azure.com --force-renewal
root@testvm:~# ls -l /etc/letsencrypt/archive/zalenko.eastus.cloudapp.azure.com/
total 48
-rw-r--r-- 1 root root 1891 Mar  8 01:16 cert1.pem
-rw-r--r-- 1 root root 1891 Mar  8 01:19 cert2.pem
-rw-r--r-- 1 root root 3750 Mar  8 01:16 chain1.pem
-rw-r--r-- 1 root root 3750 Mar  8 01:19 chain2.pem
-rw-r--r-- 1 root root 5641 Mar  8 01:16 fullchain1.pem
-rw-r--r-- 1 root root 5641 Mar  8 01:19 fullchain2.pem
-rw------- 1 root root 1704 Mar  8 01:16 privkey1.pem
-rw------- 1 root root 1704 Mar  8 01:19 privkey2.pem
:~# ls -l /etc/letsencrypt/live/zalenko.eastus.cloudapp.azure.com/
total 20
-rw-r--r-- 1 root root  692 Mar  8 01:16 README
lrwxrwxrwx 1 root root   57 Mar  8 01:19 cert.pem -> ../../archive/zalenko.eastus.cloudapp.azure.com/cert2.pem
lrwxrwxrwx 1 root root   58 Mar  8 01:19 chain.pem -> ../../archive/zalenko.eastus.cloudapp.azure.com/chain2.pem
lrwxrwxrwx 1 root root   62 Mar  8 01:19 fullchain.pem -> ../../archive/zalenko.eastus.cloudapp.azure.com/fullchain2.pem
lrwxrwxrwx 1 root root   60 Mar  8 01:19 privkey.pem -> ../../archive/zalenko.eastus.cloudapp.azure.com/privkey2.pem
root@testvm:/home/azureuser#

We can read the certificate's expiry without Certbot if we want more control or a deeper understanding of the process. This is also useful for gatekeeping use of the rate-limited letsencrypt service:

:~# openssl x509 -dates -noout < /etc/letsencrypt/live/cedricsite.westeurope.cloudapp.azure.com/cert.pem 
notBefore=Mar  5 14:24:13 2022 GMT
notAfter=Jun  3 14:24:12 2022 GMT
:~# openssl x509 -dates -noout < /etc/letsencrypt/live/cedricsite.westeurope.cloudapp.azure.com/fullchain.pem 
notBefore=Mar  5 14:24:13 2022 GMT
notAfter=Jun  3 14:24:12 2022 GMT
:~# openssl x509 -dates -noout < /etc/letsencrypt/live/cedricsite.westeurope.cloudapp.azure.com/chain.pem 
notBefore=Sep  4 00:00:00 2020 GMT
notAfter=Sep 15 16:00:00 2025 GMT