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_challenge3.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)
fi4.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
doneAnd 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