Hi all,
After reading quite a lot of documentation on HPKP, I decided to enable it on my site which is using an LE-issued certificate via certbot.
Let me preface this by saying that I also found today that Comodo offers a free 90 day certificate, so I have opted to go with them for my first backup. I am still working on sourcing a secondary backup.
Also, I want to note that the site where I have deployed HPKP is merely a test site for things I plan to do at work.
In the process of setting this up, I wanted to make it automated so that the pins are updated when LetsEncrypt renews.
Since I am using LE for my primary certificate, and have opted to go with Comodo for my secondary, I have downloaded both LE root CA files, and the Comodo intermediate CA bundle (I could not find their root CA online so if anyone has a link, that would be appreciated!)
I went ahead and generated pins against my existing LE cert, as well as the 2 LE root CAs and the Comodo intermediate CA. After manually inserting those into my configuration, I started working on a script which can be called with --renew-hook in either manual or automated renewals.
THIS WILL NOT WORK WITHOUT THE INITIAL SETUP OF CONFIGURING YOUR SERVER WITH HPKP – IT IS NOT AN ALL-IN-ONE SCRIPT, INITIAL SETUP IS MANUAL
Disclaimer: I am not responsible if you brick your website with this script. You have been warned. Set the maximum age to a very low number, or use the HPKP specific report-only header rather than the enforcing header.
To run this tool, call it as part of your renewal:
certbot-auto renew --renew-hook “/path/to/script.sh”
You must setup HPKP on your own first before using this script.
Once you have HPKP setup, this tool will simply update the pins in your apache configuration when a renewal takes place.
I am open to suggestions, but this is a free-time-only project, and so I will only be providing limited support. Also, I only have CentOS and I am not familiar with other distros, so if you want to port it, or add features, please feel free.
Also, the top of the file contains a small configuration section for users to outline their apache configuration. This is the only part of the script which should be edited by normal users. Please do make necessary edits, as my defaults do not apply on any system except for my own.
#!/bin/bash
#
# letsencrypt-hpkp-post-renewal-hook.sh
#
# Author: Thomas D. Spear <speeddymon@gmail.com>
# Version: 0.1
# Date: 2018-05-05
# Description: Renews your certs using certbot-auto and then generates new HPKP PINs and inserts them into apache config
# Count how many certs are being renewed
shopt -s nullglob
CERTPATHS=(/etc/letsencrypt/archive/*)
DIRECTORIES=(/var/www/vhosts/*)
shopt -u nullglob
# Change this to your own conf file path
CONFPATH=/etc/httpd/conf.d/vhosts
DEFAULTCONF=1-default
DEFAULTDOMAIN=$(hostname -f)
########## End of user configurable options ##########
# Exit if we can't find directories
[ ${#CERTPATHS[@]} -lt 1 ] && exit 1
[ ${#DIRECTORIES[@]} -lt 1 ] && exit 1
# We will use this later for domains renewed as Subject Alternative Names on another certificate
PASS2=("${DIRECTORIES[@]##*/}")
for CERTDIR in "${CERTPATHS[@]}"
do
# Path for the cert directory we are working on
if [ ! -d "$CERTDIR" ]
then
echo "Cannot find directory $CERTDIR. Please check renewal manually"
echo "Please note: HPKP will not work for this site and you may have downtime if this is not investigated soon!"
continue
fi
# Path to the cert
CERT=$(find "$CERTDIR" -mtime 0 -type f -name "cert*.pem")
declare -p CERT 2>/dev/null | grep -q -- '^declare -a'
# In case we got an array from the find above, we keep only the last element
if [ $? -eq 0 ]
then
# In case this is an array, let's only keep the last element
CERT=${CERT[-1]}
fi
# In case we got multiple paths from the find above, we remove all but the last
CERT=${CERT//*[$'\t\r\n ']}
# Skip if we can't find the cert
if [ ! -e "$CERT" ]
then
echo "Cannot find cert $CERT. Please check renewal manually."
echo "Please note: HPKP will not work for this site and you may have downtime if this is not investigated soon!"
continue
fi
# Domain we are working on
DOMAIN=${CERTDIR##*/}
# Path to the config
CONF=$CONFPATH/$DOMAIN-le-ssl.conf
if [ ! -e "$CONF" ]
then
if [ "$DOMAIN" != "$DEFAULTDOMAIN" ]
then
echo "Cannot find config $CONF. Please check renewal manually."
echo "Please note: HPKP will not work for this site and you may have downtime if this is not investigated soon!"
continue
fi
CONF=$CONFPATH/$DEFAULTCONF-le-ssl.conf
fi
# New PIN from the new cert
PIN=$(openssl x509 -pubkey < "$CERT" | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64)
# Search and replace the old primary PIN with the new one
sed -i "s|\\(pin-sha256=\\\\\"\\)[A-Za-z0-9+/=]\\+|\\1$PIN|1" $CONF
echo "New PIN ($PIN) written to $CONF"
# Cleanup old saves
rm -f /root/hpkp/"$DOMAIN"/primary/*
find $CERTDIR -mtime 0 -type f -exec cp '{}' /root/hpkp/$DOMAIN/primary \;
PASS2=("${PASS2[@]##$DOMAIN}")
done
for DOMAIN in ${PASS2[@]}
do
# Default is refreshed as part of the for loop above
if [ "$DOMAIN" == "default" ]
then
continue
fi
CONF=$(find $CONFPATH -type f -name "$DOMAIN-le-ssl.conf")
if [ ! -e "$CONF" ]
then
echo "Cannot find config $CONF. Please check renewal manually."
echo "Please note: HPKP will not work for this site and you may have downtime if this is not investigated soon!"
continue
fi
CERT=$(awk '/SSLCertificateFile/ {print $NF}' "$CONFPATH/$DOMAIN-le-ssl.conf")
# New PIN from the new cert
PIN=$(openssl x509 -pubkey < "$CERT" | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64)
# Search and replace the old primary PIN with the new one
sed -i "s|\\(pin-sha256=\\\\\"\\)[A-Za-z0-9+/=]\\+|\\1$PIN|1" "$CONF"
echo "New PIN ($PIN) written to $CONF"
done
exit 0