Prerequisites:
- Load balancer with Haproxy version 1.8
- Haproxy configured without sticky session traffic (FTP, mail)
Installation
1: Login to haproxy and install certbot agent
sudo apt-get update sudo apt-get install software-properties-common sudo add-apt-repository universe sudo add-apt-repository ppa:certbot/certbot sudo apt-get update sudo apt-get install certbot mkdir -p /var/www
2: Disable autorenew
systemctl stop certbot.timer systemctl disable certbot.timer systemctl mask certbot.service systemctl daemon-reload
3: Configure haproxy
edit the following settings in /etc/haproxy/haproxy.cfg
global <span class="fr-marker" data-id="0" data-type="false" style="display: none; line-height: 0;"></span><span class="fr-marker" data-id="0" data-type="true" style="display: none; line-height: 0;"></span> # stateless acme-challenges lua-load /usr/lib/stateless_acme_challenge.lua
3.1: For each web frontend/backend configure acme challenges:
frontend apachecluster01 bind IP:80 bind IP:443 ssl crt /etc/haproxy/atomia_certificates/default.pem crt /etc/haproxy/atomia_certificates crt /etc/haproxy/le_certs acl acme_challenge path_beg /.well-known/acme-challenge/ http-request use-service lua.stateless_acme_challenge if acme_challenge default_backend apache_servers01 backend apache_servers01 acl acme_challenge path_beg /.well-known/acme-challenge/ http-request use-service lua.stateless_acme_challenge if acme_challenge
3.2: Add a challenge script in /usr/lib/stateless_acme_challenge.lua
core.register_service("stateless_acme_challenge", "http", function(applet) local last_slash_index = string.find(applet.path, "/[^/]*$") local response = string.sub(applet.path, last_slash_index + 1) .. ".83Xd7GbtHlaHq2u20lSZ9Bi8sLkx1sDL3skztvdMoEA\n" applet:set_status(200) applet:add_header("content-length", string.len(response)) applet:add_header("content-type", "text/plain") applet:start_response() applet:send(response) end)
make sure to replace the thumbprint (83Xd7GbtHlaHq2u20lSZ9Bi8sLkx1sDL3skztvdMoEA in the example above) with the thumbprint from your certbot installation.
3.3: Reload haproxy
service haproxy reload
Add a Let's encrypt order
4: Create a script: /usr/bin/letsencrypt_order.sh
#!/bin/sh PATH=$PATH:/usr/sbin export PATH ATO_NETS="172.16.52.207/32 172.16.52.208/32" certbot="/usr/bin/certbot" wanted_cert_path="/etc/letsencrypt/live" synced_apache_config="/etc/haproxy/synced_apache_config" synced_iis_config="/etc/haproxy/synced_iis_config" if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then echo "usage: $0 rsync_path_to_apache_config rsync_path_to_iis_config preview_domain" echo "example:" echo "$0 \"[email protected]:/storage/configuration/maps\" \"[email protected]:/storage/configuration/iis\" preview.dev.atomia.com" exit 1 fi # Sync all config files from /storage to local dir rsync -a -e "ssh -o StrictHostKeyChecking=no" --delete "$1"/ "$synced_apache_config" rsync -a -e "ssh -o StrictHostKeyChecking=no" --delete "$2"/ "$synced_iis_config" in_net() { perl -e ' use strict; my $net = shift @ARGV or die "no net"; my $ip = shift @ARGV or die "no ip"; my @pair = split("/", $net); my $pf = $pair[0]; my $pl = $pair[1]; $pf =~ s/(\d+)([.]|$)/sprintf("%02X", $1)/ge; $pf = unpack("N", pack("H8", $pf)); $pl = unpack("N", pack("b32", "1" x $pl . "0" x (32 - $pl))); $ip =~ s/(\d+)([.]|$)/sprintf("%02X", $1)/ge; $ip = unpack("N", pack("H8", $ip)); exit 1 if ($pf & $pl) ne ($ip & $pl); ' "$1" "$2" } in_ato_nets() { is_in=no for net in $ATO_NETS; do in_net $net "$1" && { is_in=yes break } done echo $is_in } if [ -f "$synced_apache_config/vhost.map" ]; then NEW_CERTS=0 cat "$synced_apache_config"/vhost.map | awk '{ print $1 }' | grep -vE "$3"'$' | grep -v '^www\.' | grep -E '^[a-zA-Z0-9.-]+$' \ | sort -u | awk '{ print $0 " www." $0 }' | (while read cert; do wanted_cert=$(echo "$cert" | cut -d " " -f 1) wanted_wwwcert=$(echo "$cert" | cut -d " " -f 2 ) desired=$(echo "$cert" | cut -d " " -f 1 ) if [ ! -d `echo "$wanted_cert_path/$desired"` ]; then ip1="$(dig +short $wanted_wwwcert | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b')" ip2="$(dig +short $wanted_cert | head -1)" if [ -n "$ip1" ] && [ "$(in_ato_nets $ip1)" = yes ] && [ -n "$ip2" ] && [ "$(in_ato_nets $ip2)" = yes ] ; then echo "$wanted_cert_path/$desired" certbot certonly --webroot -w /var/www -d $wanted_cert -d $wanted_wwwcert --non-interactive --agree-tos --email [email protected] if [ -d `echo "$wanted_cert_path/$desired"` ]; then DOMAIN=$wanted_cert sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/le_certs/$DOMAIN.pem' chmod go-rwx /etc/haproxy/le_certs/$DOMAIN.pem find /var/log/letsencrypt/ -size 0 -delete NEW_CERTS=$((NEW_CERTS+1)) fi fi fi done #Delete broken PEMs find /etc/haproxy/le_certs/ -size 0 -delete #If there is at least one new cert, reload haproxy if [ "$NEW_CERTS" -ge "1" ]; then echo "Reloading haproxy" service haproxy reload echo "Reloading done" fi) fi if [ -f "$iis_config/applicationHost.config" ]; then NEW_CERTS=0 grep -F binding "$iis_config/applicationHost.config" | grep -F ":80:" | awk -F ':80:' '{ print $2 }' | cut -d '"' -f 1 \ | grep -vE "$1"'$' | grep -v '^www\.' | grep -E '^[a-zA-Z0-9.-]+$' | sort -u | awk '{ print $0 " www." $0 }' | (while read cert; do wanted_cert=$(echo "$cert" | cut -d " " -f 1) wanted_wwwcert=$(echo "$cert" | cut -d " " -f 2 ) desired=$(echo "$cert" | cut -d " " -f 1 ) if [ ! -d `echo "$wanted_cert_path/$desired"` ]; then ip1="$(dig +short $wanted_wwwcert | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b')" ip2="$(dig +short $wanted_cert | head -1)" if [ -n "$ip1" ] && [ "$(in_ato_nets $ip1)" = yes ] && [ -n "$ip2" ] && [ "$(in_ato_nets $ip2)" = yes ] ; then echo "$wanted_cert_path/$desired" certbot certonly --webroot -w /var/www -d $wanted_cert -d $wanted_wwwcert --non-interactive --agree-tos --email [email protected] if [ -d `echo "$wanted_cert_path/$desired"` ]; then DOMAIN=$wanted_cert sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/le_certs/$DOMAIN.pem' chmod go-rwx /etc/haproxy/le_certs/$DOMAIN.pem find /var/log/letsencrypt/ -size 0 -delete NEW_CERTS=$((NEW_CERTS+1)) fi fi fi done #Delete broken PEMs find /etc/haproxy/le_certs/ -size 0 -delete #If there is at least one new cert, reload haproxy if [ "$NEW_CERTS" -ge "1" ]; then echo "Reloading haproxy" service haproxy reload echo "Reloading done" fi) fi
4.1: Make the script executable
chmod +x /usr/bin/letsencrypt_order.sh
IMPORTANT NOTICE: In /usr/bin/letsencrypt_order.sh modify ATO_NETS variable according to your setup. This variable contains all public ip addresses
bind in haproxy config for web. Lets encrypt certificates will be ordered only for websites that resolves to one if this IPs
4.2: Create a Cronjob that will run every hour
'/etc/cron.d/letsencrypt_order'
0 */1 * * * root flock -n /var/lock/letsencrypt_order.lock /usr/bin/letsencrypt_order.sh [email protected]:/storage/configuration/maps [email protected]:/storage/configuration/iis preview.dev.atomia.com
IMPORTANT NOTICE: In /etc/cron.d/letsencrypt_order, 192.168.33.2 should be replaced with Atomia fsagent IP address, preview.dev.atomia.com
needs to be replaces with your preview domain.
Configure renewal
5: Create a script - /usr/bin/letsencrypt_renew.sh
#!/bin/bash PATH=$PATH:/usr/sbin export PATH ATO_NETS="172.16.52.207/32 172.16.52.208/32" CERTS_DIR="/etc/haproxy/le_certs" curdate=$(date +%s) renewed_cert=0 revoked_cert=0 in_net() { perl -e ' use strict; my $net = shift @ARGV or die "no net"; my $ip = shift @ARGV or die "no ip"; my @pair = split("/", $net); my $pf = $pair[0]; my $pl = $pair[1]; $pf =~ s/(\d+)([.]|$)/sprintf("%02X", $1)/ge; $pf = unpack("N", pack("H8", $pf)); $pl = unpack("N", pack("b32", "1" x $pl . "0" x (32 - $pl))); $ip =~ s/(\d+)([.]|$)/sprintf("%02X", $1)/ge; $ip = unpack("N", pack("H8", $ip)); exit 1 if ($pf & $pl) ne ($ip & $pl); ' "$1" "$2" } in_ato_nets() { is_in=no for net in $ATO_NETS; do in_net $net "$1" && { is_in=yes break } done echo $is_in } cd $CERTS_DIR ls -tr | \ while read pem; do expdate1=$(date --date="$(openssl x509 -enddate -noout -in "$pem"|cut -d= -f 2)" +%s) expdays=$(( (expdate1-curdate) / 86400)) if [ $expdays -le 25 ]; then wanted_cert=`echo -n "$pem" | head -c-4` ip="$(dig +short $wanted_cert | head -1)" if [ -n "$ip" ] && [ "$(in_ato_nets $ip)" = yes ] ; then echo "Certificate $wanted_cert expire on $expdate1, in $expdays days !" echo "Renewing.." certbot renew --cert-name $wanted_cert --force-renewal OUT=$? #0 if renew is succesfull, 1 if it's failed if [ $OUT -eq 0 ];then DOMAIN=$wanted_cert sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/le_certs/$DOMAIN.pem' chmod 600 /etc/haproxy/le_certs/$wanted_cert.pem rm /var/log/letsencrypt/letsencrypt.log find /var/log/letsencrypt/ -size 0 -delete ((renewed_cert++)) fi else echo "Certificate $wanted_cert not hosted on Swisscom !" echo "Revoking.." certbot revoke -d $wanted_cert --cert-path /etc/letsencrypt/live/${wanted_cert}/cert.pem --non-interactive rm -f /etc/haproxy/le_certs/$wanted_cert.pem ((revoked_cert++)) fi fi if [ $revoked_cert -ge 1000 ]; then echo "Revoke limit hit - breaking" echo "Total number of revoked certs is $revoked_cert" echo "Total number of renewed certs is $renewed_cert" break fi if [ $renewed_cert -ge 1000 ]; then echo "Renew limit hit - breaking" echo "Total number of revoked certs is $revoked_cert" echo "Total number of renewed certs is $renewed_cert" break fi done
And make it executable
chmod +x /usr/bin/letsencrypt_renew.sh<span class="fr-marker" data-id="0" data-type="false" style="display: none; line-height: 0;"></span><span class="fr-marker" data-id="0" data-type="true" style="display: none; line-height: 0;"></span>
Create a cronjob that will run this script once a day
/etc/cron.d/letsencrypt_renew
0 1 * * * root flock -n /var/lock/letsencrypt_renew.lock /usr/bin/letsencrypt_renew.sh