Aller au contenu

Rotation de certificat HAProxy

Rotation à chaud des certificats TLS dans HAProxy via l'API d'exécution — automatisation Let''s Encrypt, motif transactionnel set/commit, rafraîchissement OCSP, et le cron qu'on livre aux clients.

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 :

  1. Set le nouveau PEM via set ssl cert <chemin> <<EOF ... EOF. HAProxy parse et valide le nouveau bundle mais ne l’active pas.
  2. 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.
  3. 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

  1. 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, utilisez socat avec EXEC:cat ou alimentez le payload en un seul flux.

  2. 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"
  3. Automatisation Let’s Encrypt avec certbot. Utilisez un deploy-hook pour 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-deploy
    cat > /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
  4. 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.com

    Certbot 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:8080

    Faites tourner un petit serveur de fichiers statiques sur 127.0.0.1:8080 pointant vers /var/www/acme-challenge.

  5. 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.

  6. 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-all

    certbot renew est un no-op à moins qu’un cert soit à 30 jours de son expiration ; sûr de lancer sur une planification rapide.

  7. 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 -5

    Le nouveau notAfter devrait ê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 cert n’a tourné. Vérifiez toujours avec s_client après rotation.
  • set ssl cert exige expose-fd listeners sur la ligne stats 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.pem et privkey.pem de Let’s Encrypt sont séparés ; concaténez-les. Manquer la clé échoue au set.
  • certbot --standalone sur le port 443 entre en collision avec HAProxy. Soit utilisez --webroot avec 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.