Prerequisites:

  1. Load balancer with Haproxy version 1.8
  2. 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

4Create 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