HAProxy : exploitation au quotidien
Tout ce qu'on fait sur un HAProxy en production sans le redémarrer : lire les stats, sortir un node du pool, et récupérer la vraie IP du client quand il y a un CDN devant.
Le socket d'admin
Tout ce qui suit passe par le socket d'administration, à déclarer dans la section global. Sans le niveau admin, les commandes de lecture fonctionnent mais tout ce qui modifie l'état est refusé.
global
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
Lire les stats
show stat sort du CSV, donc on filtre sur les colonnes qui nous intéressent.
echo "show stat" | socat stdio /run/haproxy/admin.sock | cut -d "," -f 1-2,5-10,34-36 | column -s, -t
# pxname svname scur smax slim stot bin bout rate rate_lim rate_max
cluster_analytics custfront1 0 6 119151 117985796 1458531881 2 15
cluster_analytics custfront2 0 4 54427 36461337 715425308 0 9
cluster_analytics BACKEND 0 9 3277 287818 269535559 3593163041 2 22
cluster_bo custbo1 0 11 605 1294617 5522403 0 22
Mettre un node en maintenance
drain sort le serveur progressivement : il ne prend plus de nouvelle connexion mais laisse les connexions en cours se terminer. maint coupe tout d'un coup.
echo "set server backend_name/svc_name state drain" | socat stdio /run/haproxy/admin.sock
echo "set server backend_name/svc_name state maint" | socat stdio /run/haproxy/admin.sock
echo "set server backend_name/svc_name state ready" | socat stdio /run/haproxy/admin.sock
Pendant un drain les health checks continuent de partir. Pour les couper aussi :
echo "disable health backend_name/svc_name" | socat stdio /run/haproxy/admin.sock
echo "enable health backend_name/svc_name" | socat stdio /run/haproxy/admin.sock
Ces commandes se tapent aussi directement dans HAtop.
Le drain n'est pas instantané
drain attend la fin des connexions en cours. Sur du WebSocket ou du long-polling elles peuvent durer des heures, donc ne jamais enchaîner un drain et un arrêt du service sans vérifier.
Avant de couper le serveur, on contrôle que le compteur de sessions courantes est bien retombé à 0 :
echo "show stat" | socat stdio /run/haproxy/admin.sock \
| awk -F, '$1=="backend_name" && $2=="svc_name" {print "connexions restantes:", $5}'
Autres commandes du socket
Celles qui sortent quand ça part en vrille :
# Sessions en cours, avec leur âge — pour trouver ce qui traîne
echo "show sess" | socat stdio /run/haproxy/admin.sock
# Dernières erreurs vues par HAProxy (requêtes malformées, réponses backend invalides)
echo "show errors" | socat stdio /run/haproxy/admin.sock
# Contenu d'une stick-table : utile pour du rate limiting ou de la persistance
echo "show table" | socat stdio /run/haproxy/admin.sock
echo "show table st_src_global" | socat stdio /run/haproxy/admin.sock
# Ajuster le poids d'un serveur à chaud, pour un canary par exemple
echo "set weight backend_name/svc_name 10" | socat stdio /run/haproxy/admin.sock
# Vider une entrée de stick-table (débloquer une IP rate-limitée à tort)
echo "clear table st_src_global key 203.0.113.4" | socat stdio /run/haproxy/admin.sock
Conserver l'IP du visiteur
Derrière un reverse-proxy on logge très vite la mauvaise IP. Plus gênant, certaines ACL applicatives autorisent ou non des pages selon l'IP source, donc il faut la propager jusqu'au backend. Une ligne dans le frontend suffit.
L'option renseigne X-Forwarded-For avec l'adresse du client.
X-Forwarded-For est un header empilable
S'il y a déjà un proxy devant, le header contient une liste (client, proxy1, proxy2) et n'importe qui peut en forger un. Ne jamais faire confiance à un X-Forwarded-For entrant sur un frontend exposé : soit on l'écrase avec http-request set-header X-Forwarded-For %[src], soit on ne l'accepte que depuis les IP du CDN, comme ci-dessous.
Derrière Cloudflare
Cloudflare passe l'IP d'origine dans CF-Connecting-IP. On réécrit la source avec, mais uniquement si la connexion vient bien de chez eux, sinon n'importe qui usurpe son IP en envoyant le header.
frontend https
bind 0.0.0.0:443 ssl crt /etc/haproxy/acme-certs/ alpn h2,http/1.1 allow-0rtt
mode http
acl from_cf src -f /etc/haproxy/acl/cloudflare_ips.lst
http-request set-src req.hdr(CF-Connecting-IP) if from_cf
option forwardfor
Cloudflare publie ses préfixes, qui changent de temps en temps. Un cron hebdomadaire évite de bloquer du trafic légitime le jour où ils en ajoutent un.
#!/usr/bin/env bash
set -euo pipefail
LST=/etc/haproxy/acl/cloudflare_ips.lst
TMP=$(mktemp)
curl -fsS https://www.cloudflare.com/ips-v4 > "$TMP"
curl -fsS https://www.cloudflare.com/ips-v6 >> "$TMP"
# On ne remplace que si le fichier est non vide et a changé
if [[ -s "$TMP" ]] && ! cmp -s "$TMP" "$LST"; then
mv "$TMP" "$LST"
systemctl reload haproxy
else
rm -f "$TMP"
fi
Le même principe marche pour les autres CDN, seul le header change : True-Client-IP chez Akamai, Fastly-Client-IP chez Fastly.
Voir aussi
- Reverse proxy : HAProxy — la configuration de base
- HAProxy : performance tuning — nbthread, maxconn, TLS et sysctls
- Limiter un DDoS applicatif — rate limiting et stick-tables en situation