Gestion des certificats TLS avec HaProxy

 

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

Sources

blog HaProxy