La rotation de certificats dans HAProxy exigeait avant un reload complet — correct pour quelques certs, pénible pour une flotte avec des centaines. L’API d’exécution permet maintenant d’échanger les certificats à chaud de façon atomique : charger le nouveau bundle, valider, commit. L’ancien cert continue à servir jusqu’au commit ; si le nouveau cert est mal formé, le commit échoue et l’ancien reste en direct. Cet article couvre le flux de rotation, le motif d’automatisation Let’s Encrypt, le rafraîchissement OCSP, et le cron qu’on livre.
Comment vérifier
Pour un cert existant, l’API d’exécution et openssl ensemble disent ce qui est chargé et quand il expire :
echo "show ssl cert" | sudo socat /run/haproxy/admin.sock - | head -10
echo "show ssl cert /etc/haproxy/certs/example.com.pem" | sudo socat /run/haproxy/admin.sock -
openssl x509 -in /etc/haproxy/certs/example.com.pem -noout -dates -subject -issuer
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
La dernière commande est la source de vérité — ce que le fil présente réellement — versus ce qui est sur disque. Ils devraient correspondre ; sinon, HAProxy a l’ancien cert en cache et un rechargement à l’exécution du cert est en retard.
Ce qui se passe
HAProxy lit les bundles PEM au démarrage et au systemctl reload. L’API d’exécution ajoute un chemin de mise à jour atomique :
- Set le nouveau PEM via
set ssl cert <chemin> <<EOF ... EOF. HAProxy parse et valide le nouveau bundle mais ne l’active pas. - Commit avec
commit ssl cert <chemin>. HAProxy échange atomiquement la référence en mémoire ; les nouvelles poignées de main TLS utilisent le nouveau cert, les connexions TLS existantes continuent d’utiliser l’ancien jusqu’à ce qu’elles se ferment. - Abort avec
abort ssl cert <chemin>si vous voulez jeter la mise à jour en attente.
L’ancien cert continue à servir jusqu’au commit. Si le cert en attente échoue au parsing (intermédiaire manquant, mauvais PEM, incompatibilité clé/cert), le commit échoue avec une erreur claire et l’ancien cert reste en direct. Pas de fenêtre de course, pas de coupure.
Le fichier PEM sur disque est juste l’entrée — HAProxy garde le bundle parsé en mémoire. Mettre à jour le fichier sur disque ne met pas à jour le cert en mémoire ; vous devez set + commit (ou reload) pour que HAProxy prenne le changement.
La procédure
-
Flux de rotation standard. Étant donné un nouveau PEM à
/tmp/new-example.com.pem:sudo cp /tmp/new-example.com.pem /etc/haproxy/certs/example.com.pem echo "set ssl cert /etc/haproxy/certs/example.com.pem <<\nEND" | sudo socat /run/haproxy/admin.sock - # En pratique, le payload du cert doit être envoyé dans l'entrée socat : { printf 'set ssl cert /etc/haproxy/certs/example.com.pem <<\n' cat /etc/haproxy/certs/example.com.pem printf '\n\n' } | sudo socat /run/haproxy/admin.sock - echo "commit ssl cert /etc/haproxy/certs/example.com.pem" | sudo socat /run/haproxy/admin.sock -La syntaxe here-doc
<<dans l’API d’exécution est sensible aux espaces — une ligne vide termine le payload. Dans un script, utilisezsocatavecEXEC:catou alimentez le payload en un seul flux. -
Script de rotation de qualité production. Un helper shell qui gère la chorégraphie :
#!/bin/bash set -euo pipefail CERT_PATH=$1 SOCKET=/run/haproxy/admin.sock # valider localement avant de mettre en attente openssl x509 -in "$CERT_PATH" -noout -checkend 86400 || { echo "cert expire dans 24h"; exit 1; } ( echo "set ssl cert $CERT_PATH <<" cat "$CERT_PATH" echo echo ) | sudo socat "$SOCKET" - echo "commit ssl cert $CERT_PATH" | sudo socat "$SOCKET" - | grep -q 'Success' \ || { echo "commit échoué"; echo "abort ssl cert $CERT_PATH" | sudo socat "$SOCKET" -; exit 1; } echo "roté $CERT_PATH" -
Automatisation Let’s Encrypt avec certbot. Utilisez un
deploy-hookpour que certbot pousse le nouveau cert vers HAProxy après le renouvellement :sudo certbot certonly --webroot -w /var/www/acme-challenge -d example.com --deploy-hook /usr/local/sbin/hap-cert-deploycat > /usr/local/sbin/hap-cert-deploy <<'EOF' #!/bin/bash set -euo pipefail for domain in $RENEWED_DOMAINS; do LIVE=/etc/letsencrypt/live/$domain TARGET=/etc/haproxy/certs/$domain.pem cat "$LIVE/fullchain.pem" "$LIVE/privkey.pem" > "$TARGET.new" mv "$TARGET.new" "$TARGET" /usr/local/sbin/hap-rotate-cert "$TARGET" done EOF sudo chmod +x /usr/local/sbin/hap-cert-deploy -
TLS-ALPN-01 vs HTTP-01. Pour HAProxy sur le port 443, le défi TLS-ALPN-01 est le plus propre parce qu’il n’exige pas que le port 80 soit ouvert ou un webroot. Avec certbot, utilisez :
sudo certbot certonly --standalone --preferred-challenges tls-alpn-01 -d example.comCertbot doit écouter sur 443 brièvement pendant le défi. Le motif : arrêter HAProxy sur 443, lancer certbot, redémarrer HAProxy. Ou utiliser un helper ACME HAProxy qui proxifie le défi.
Pour HTTP-01 (le défaut le plus simple), mettez HAProxy sur 80 avec une route qui sert
/.well-known/acme-challenge/depuis le disque :frontend fe_http bind *:80 acl is_acme path_beg /.well-known/acme-challenge/ use_backend be_acme if is_acme http-request redirect scheme https code 301 unless is_acme default_backend be_app backend be_acme server acme 127.0.0.1:8080Faites tourner un petit serveur de fichiers statiques sur 127.0.0.1:8080 pointant vers
/var/www/acme-challenge. -
Rafraîchissement OCSP en pas de marche. L’agrafage exige le fichier de réponse OCSP. Rafraîchissez-le à la même cadence que le cert et poussez via l’API d’exécution :
CERT=/etc/haproxy/certs/example.com.pem ISSUER_URL=$(openssl x509 -in "$CERT" -noout -ocsp_uri) ISSUER=$(curl -s "$ISSUER_URL" || true) # on a besoin du cert émetteur openssl ocsp -issuer chain.pem -cert "$CERT" -url "$ISSUER_URL" -no_nonce -respout "$CERT.ocsp" echo "set ssl ocsp-response $(base64 -w0 $CERT.ocsp)" | sudo socat /run/haproxy/admin.sock -Rafraîchir toutes les 12-24 heures. La réponse OCSP est typiquement valide 5-7 jours mais vous voulez rafraîchir bien à l’intérieur de cette fenêtre.
-
Crontab pour le cycle complet. Vérification certbot toutes les heures, rafraîchissement OCSP quotidien :
17 */12 * * * root certbot renew --quiet 42 6 * * * root /usr/local/sbin/refresh-ocsp-allcertbot renewest un no-op à moins qu’un cert soit à 30 jours de son expiration ; sûr de lancer sur une planification rapide. -
Vérifier après rotation. Une vérification ponctuelle depuis l’extérieur :
echo | openssl s_client -connect example.com:443 -servername example.com -status 2>&1 | grep -E 'notBefore|notAfter|OCSP' curl -sI https://example.com/ | head -5Le nouveau
notAfterdevrait être à 90 jours (Let’s Encrypt) ou ce que l’émetteur a défini. Le statut OCSP devrait lire « successful ».
Pièges courants
- Le fichier PEM sur disque et le cert en mémoire peuvent dériver. On a vu des ingénieurs d’astreinte mettre à jour le fichier, redémarrer leur cron, partir, et manquer que HAProxy servait encore l’ancien cert parce qu’aucun
set ssl certn’a tourné. Vérifiez toujours avecs_clientaprès rotation. set ssl certexigeexpose-fd listenerssur la lignestats socket. Sans cela, la commande échoue silencieusement sur certaines versions HAProxy ou renvoie une erreur peu utile.- Le bundle doit inclure à la fois le certificat et la clé privée. Les
fullchain.pemetprivkey.pemde Let’s Encrypt sont séparés ; concaténez-les. Manquer la clé échoue auset. certbot --standalonesur le port 443 entre en collision avec HAProxy. Soit utilisez--webrootavec HTTP-01 (HAProxy sert/.well-known/acme-challenge/), TLS-ALPN-01 avec HAProxy momentanément arrêté, ou un démon ACME dédié comme Caddy ou lego.- Les agrafes OCSP sont liées au cert ; faites tourner l’agrafe après avoir fait tourner le cert. Une vieille agrafe contre un nouveau cert est ignorée silencieusement par les clients et vous perdez l’agrafage sans erreur évidente.
Stack Harbor opère la rotation de certificats comme infrastructure automatisée pour les clients — Let’s Encrypt pour les hostnames publics, PKI interne pour le service-à-service, rapport mensuel d’expiration pour tout cert non auto-renouvelé. Le script de rotation est idempotent et tourne depuis le runner d’orchestration avec journalisation d’audit. C’est partie des opérations day-2 qu’on maintient dans le cadre de Managed Operations, afin que les certificats qui expirent ne réveillent jamais personne.