Après avoir fait une jolie installation de HaProxy, voir Installation HaProxy (nb qui fonctionne aussi très bien pour la version 2.9), je vais vous présenter dans cet article deux façons de faire pour la mise en place de la gestion des certificats TLS pour les différents sites qui passent par HaProxy.
Je pars du principe ou je fais que du certificat ssl Let's Encrypt.
Jusqu'à la version 2.8
Voici comment je précédais jusqu'à la version 2.8 de HaProxy :
Création du certificats SSL avec les commandes letsencrypt
letsencrypt certonly --standalone -d url --non-interactive --agree-tos --email email --http-01-port=666
Avec cette configuration de haproxy :
global
...
# modern configuration
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets
#tune DH to 4096
tune.ssl.default-dh-param 4096
ssl-dh-param-file /etc/haproxy/dhparams/dhparams.pem
...
frontend sites
bind IP:80 alpn h2,h2c,http/1.1
bind IP:443 ssl crt /etc/haproxy/ssl alpn h2,h2c,http/1.1
# Access log
log 127.0.0.1 local6
option httplog
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
http-request redirect scheme https unless { ssl_fc }
# forward header
option forwardfor
# HSTS (63072000 seconds)
http-response set-header Strict-Transport-Security max-age=63072000
...
# Backend Let's Encrypt
backend let-backend
server letsencrypt 127.0.0.1:666 # The Number of The Beast ...
C'était fonctionnel mais ... il fallait que je synchronise les certificats ssl dans le répertoire de haproxy sous forme d'un seul fichier par site :
for i in $(ls /etc/letsencrypt/live/|grep -v README) ; do cat /etc/letsencrypt/live/$i/fullchain.pem /etc/letsencrypt/live/$i/privkey.pem > /etc/haproxy/ssl/$i.pem; done
Et il fallait redémarrer HaProxy lors des changements de certificats, pas bien méchant mais cela peut occasionner une petite coupure de service.
A partir de la 2.8
Depuis la version 2.8 il est possible d'intégrer la gestion des certificats TLS avec acme.sh et donc dès que le certificat est mis à jour il n'est plus nécessaire de redémarrer HaProxy pour que la mise à jour du certificat soit prise en compte.
Voici comment procéder sous Debian 12
Mise en place de acme.sh
Création d'un utilisateur sans password et login dans le groupe HaProxy
adduser --system --disabled-password --disabled-login --home /var/lib/acme --quiet --force-badname --group acme
adduser acme haproxy
On va déployer dans un répertoire le projet :
mkdir /usr/local/share/acme.sh/
cd /tmp
git clone https://github.com/acmesh-official/acme.sh.git
cd acme.sh
./acme.sh --install --no-cron --no-profile --home /usr/local/share/acme.sh
ln -s /usr/local/share/acme.sh/acme.sh /usr/local/bin/
chmod 755 /usr/local/share/acme.sh/
Il faut ensuite mettre en place un script qui va permettre de recharger haproxy sans être root :
curl https://raw.githubusercontent.com/haproxy/haproxy/master/admin/acme.sh/haproxy.sh | tee /usr/local/share/acme.sh/deploy/haproxy.sh
Clé d'authentification Let's Encrypt
Pour que acme.sh puisse gérer le challenge Let's Encrypt à travers HaProxy, il va falloir mettre en place le hash d'une clé d'un compte Let's Encrypt.
sudo -u acme -s
acme.sh --register-account --server letsencrypt -m youremail@example.com
Il est possible d'utiliser les serveurs letsencryt_test pour ne pas taper directement sur la prod lors des test, oubliez pas que si vous avez 5 échec de suite vous êtes bloqué 24h sur les serveurs.
Il va falloir récupérer la valeur de ACCOUNT_THUMBPRINT
Configuration de HaProxy
On crée le répertoire qui va accueillir les certificats :
mkdir /etc/haproxy/certs
chown haproxy:haproxy /etc/haproxy/certs
chmod 770 /etc/haproxy/certs
Et dans la configuration de HaProxy :
global
...
setenv ACCOUNT_THUMBPRINT 'lCufto4sDRTHdmWL0EugFywGV54hBCuTTXvwifi65R4'
frontend web
bind :80
bind :443 ssl crt /etc/haproxy/certs/ strict-sni
http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }
Et bien sur on redémarre HaProxy
systemctl restart haproxy
Génération du certificat
Pour créer le certificat
sudo -u acme -s
acme.sh --issue -d example.com --stateless --server letsencrypt
Le déployer
DEPLOY_HAPROXY_HOT_UPDATE=yes DEPLOY_HAPROXY_STATS_SOCKET=/var/run/haproxy/admin.sock DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy/certs acme.sh --deploy -d example.com --deploy-hook haproxy
Mettre en place la crontab
Pour le renouvellement
sudo -u acme -s
acme.sh --install-cronjob
Ce qui donne quelquechose comme ceci, qui est adaptable bien sur
15 14 * * * /usr/local/share/acme.sh/acme.sh --cron --home "/var/lib/acme/.acme.sh" > /dev/null
Configuration complète de HaProxy
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
# For acme.sh
setenv ACCOUNT_THUMBPRINT '******'
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
# Frontends
frontend stats
bind 0.0.0.0:8080
stats enable
stats hide-version
stats uri /
stats realm Haproxy\ Statistics
stats auth haproxy:supertopsecretpasswod
stats refresh 10s
frontend web
bind :80
bind :443 ssl crt /etc/haproxy/certs/ strict-sni alpn h2,h2c,http/1.1
http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }
tcp-request inspect-delay 5s
# Redirect ssl
tcp-request content accept if { req_ssl_hello_type 1 }
http-request redirect scheme https unless { ssl_fc }
# forward header
option forwardfor
# HSTS (63072000 seconds)
http-response set-header Strict-Transport-Security max-age=63072000
# pmx-lb.
acl host_pmx hdr(host) -i url
use_backend pmx if host_pmx
# Backends
backend pmx
description Back-end proxmox
mode http
balance roundrobin
option forwardfor
option httpchk GET /
cookie SERVERID insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
server pmx-01 192.168.47.11:8006 cookie S1 check ssl verify none
server pmx-02 192.168.47.12:8006 cookie S1 check ssl verify none
server pmx-03 192.168.47.13:8006 cookie S1 check ssl verify none