Howto HAProxy
- Installation
- Configuration
- Configuration avancée
- Exemple d’une configuration avec frontend/backend HTTP
- SSL
- Exemple avec plusieurs backends et du « sticky session »
- Reproduire un ProxyPass Apache
- Rediriger le trafic vers le serveur web local
- Exemple en mode TCP
- Exemple pour MySQL
- Protection d’attaques
- Protocol PROXY
- Checks
- Ajustement dynamique
- HTTP basic authentication
- Redirection
- Résolution DNS et nombre dynamique de serveurs
- Conservation des états
- Haute performance
- Utiliser l’API runtime
- Lecture des logs
- Challenge ACME
- Attaques (D)DOS
- HAPEE – HAProxy Enterprise Edition
- FAQ
- Documentation : https://docs.haproxy.org/
- Statut de cette page : prod / bullseye
HAProxy est un puissant load balancer pour les protocoles TCP/HTTP/HTTPS. Il gère la répartition de charge et la tolérance de panne. Son principal auteur est Willy Tarreau, un développeur actif du noyau Linux. HAProxy est écrit en langage C, il est optimisé pour Linux, mais tourne également sous BSD. Des sites web importants l’utilisent comme Twitter, Github, Reddit, Airbnb, etc.
Installation
# apt install haproxy
$ /usr/sbin/haproxy -version
HA-Proxy version 2.2.9-2+deb11u5 2023/04/10 - https://haproxy.org/
Status: long-term supported branch - will stop receiving fixes around Q2 2025.
Known bugs: http://www.haproxy.org/bugs/bugs-2.2.9.html
Running on: Linux 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64
Configuration
La configuration se passe dans le fichier
/etc/haproxy/haproxy.cfg
:
global
log 127.0.0.1 local5 debug
defaults
mode http
listen www
bind *:80
balance roundrobin
option httpchk OPTIONS * HTTP/1.1\r\nHost:\ www.example.com
server www00 192.0.2.1:80 maxconn 50 check inter 10s
server www01 192.0.2.2:80 maxconn 50 check inter 10s
On note l’activation des logs en debug ce qui permet de voir
toutes les requêtes. Attention, il faut donc que le
démon syslog
ait un paramétrage sur la facilité
local5
(ou autre selon votre configuration). Pour
rsyslog
cela se fait ainsi dans rsyslog.conf
:
local5.* -/var/log/haproxy.log
Vérifier la validité de la configuration
On vérifie qu’il n’y a pas d’erreur de syntaxes :
haproxy -c -V -f /etc/haproxy/haproxy.cfg
Exemples de configuration basique
Voici quelques exemples inspirants :
# Empecher les requetes de type PROPFIND
acl ACL1 method PROPFIND
http-request deny if ACL1
# Envoi vers un backend en fonction du path
use_backend BACKEND1 if { path_beg /blog }
Configuration avancée
Exemple d’une configuration avec frontend/backend HTTP
global
log /dev/log local5
log /dev/log local5 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
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
default-server port 80 maxconn 250 on-error fail-check slowstart 60s inter 1m fastinter 5s downinter 10s weight 100
listen stats
bind *:8080
stats enable
stats uri /
stats show-legends
stats show-node
stats realm Auth\ required
stats auth foo:bar
stats admin if TRUE
frontend myfront
option forwardfor
maxconn 800
bind 0.0.0.0:80
# Garde en mémoire et log l'en-tête de la requête
capture request header Host len 32
default_backend myback
backend myback
balance roundrobin
server web01 192.0.2.1:80 check observe layer4 weight 100
server web02 192.0.2.2:80 check observe layer4 weight 100
server web03 192.0.2.3:80 check observe layer4 weight 100
La visualisation des statistiques peut aussi se faire via la console :
hatop -s /run/haproxy/admin.sock
SSL
global
# Default SSL material locations
ca-base /etc/ssl/certs
crt-base /etc/ssl/private
# Default ciphers to use on SSL-enabled listening sockets.
# For more information, see ciphers(1SSL). This list is from:
# https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
ssl-default-bind-options no-sslv3 no-tls-tickets
ssl-default-server-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
ssl-default-server-options no-sslv3 no-tls-tickets
Dans un frontend il faut ensuite faire un “binding” avec des arguments pour le SSL :
frontend fe_https
bind 0.0.0.0:443 ssl crt /etc/ssl/haproxy/example_com.pem
http-request set-header X-Forwarded-Proto https
default_backend myback
Le fichier example_com.pem
doit contenir le certificat
ainsi que la clé privée et éventuellement des paramètres Diffie-Hellman
(tout au format PEM).
Il est possible d’indiquer plusieurs fois
crt /chemin/vers/fichier.pem
pour avoir plusieurs
certificats possibles. HAProxy utilisera alors le mécanisme de SNI. Si
on indique plutôt un dossier (par exemple
/etc/ssl/haproxy/
) tous les fichiers trouvés seront chargés
par ordre alphabétique.
Pour chaque fichier PEM trouvé, HAProxy cherchera un fichier
.ocsp
du même nom. Il peut être vide ou contenir une
réponse OCSP valide (au format DER). Cela active le mécanisme de « OCSP
stapling »
Tous les détails de configuration pour l’attribut crt
sont consultables sur http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#5.1-crt
Dans le cas où HAProxy gère plusieurs domaines dont certains
seulement ont un certificat SSL, HAProxy enverra par défaut le
certificat défini par la directive crt
. S’il pointe sur un
répertoire, un des certificats du répertoire (ça ne semble pas être le
premier/dernier par ordre alphabétique) sera envoyé. Pour éviter ce
comportement, on peut rajouter la directive strict-sni
.
Dans ce cas, si HAProxy ne trouve pas le certificat qui correspond au
domaine demandé par le client, il retournera l’erreur SSL
SSL_ERROR_BAD_CERT_DOMAIN.
Il est aussi possible de désigner un fichier contenant la liste de tous les PEM :
frontend fe_https
bind 0.0.0.0:443 ssl crt-list /etc/ssl/crt-list
Le fichier crt-list contient le chemin des fichiers PEM, ligne par ligne, comme ceci :
/chemin/vers/fichier1.pem
/chemin/vers/fichier2.pem
/chemin/vers/fichier3.pem
Terminaison SSL
Si HAProxy doit faire la terminaison SSL et dialoguer en clair avec le backend, on se contente de transmettre la requête.
backend myback
balance roundrobin
server web01 192.0.2.1:80 check observe layer4 weight 100
server web02 192.0.2.2:80 check observe layer4 weight 100
Si HAProxy doit faire la terminaison SSL et maintenir une
communication chiffrée avec le backend, on doit le spécifier
dans le backend (avec l’argument
ssl verify [none|optional|required]
, car le port 443 ne
suffit pas à forcer le ssl).
backend myback
balance roundrobin
server web01 192.0.2.1:443 ssl verify none check observe layer4 weight 100
server web02 192.0.2.2:443 ssl verify none check observe layer4 weight 100
La vérification du SSL sur le backend est à voir en fonction des besoins de sécurité.
Attention, HAProxy ne supporte pas le SNI pour la communication avec le backend, du moins en version 1.5. L’erreur n’est pas explicite, il se contente d’indiquer une erreur de négociation SSL, cela peut se voir en détail avec wireshark par exemple. Il faut donc s’assurer que le backend délivre le bon certificat par défaut.
Exemple avec plusieurs backends et du « sticky session »
global
[...]
defaults
[...]
frontend http-in
bind *:8080
# On définit des ACL qui associe un Host: HTTP à un backend
acl is_domain1 hdr_end(host) -i domain1.example.com
acl is_domain2 hdr_end(host) -i domain2.example.com
use_backend domain1 if is_domain1
use_backend domain2 if is_domain2
default_backend domain1
backend domain1
# Avec cette directive, HAProxy ajoute automatiquement un cookie SERVERID aux réponses HTTP,
# et l'utilise pour sélectionner le bon serveur lors de la prochaine requête
cookie SERVERID insert indirect
balance roundrobin
# Pour ce serveur, la valeur du cookie SERVERID sera "web01" (directive "cookie")
server web01 192.0.2.1:80 cookie web01 check
# Pour ce serveur, la valeur du cookie SERVERID sera "web02"
server web02 192.0.2.2:80 cookie web02 check
backend domain2
cookie SERVERID insert indirect
balance roundrobin
server web01 192.0.2.1:80 cookie web01 check
server web02 192.0.2.2:80 cookie web02 check
Reproduire un ProxyPass Apache
Avec Apache il est courant de faire un proxy qui modifie le chemin (path) :
ProxyPass / http://localhost:9999/path/to/app
ProxyPassReverse / http://localhost:9999/path/to/app
Ainsi, une requete à /foo/bar
sera transise au final à
/path/to/app/foo/bar
.
Il est possible de reproduire le même comportement directement dans HAProxy :
backend be_http
mode http
http-request set-path /path/to/app%[path]
acl header_location res.hdr(Location) -m found
http-response replace-header Location (https?://%[req.hdr(Host)](:[0-9]+)?)?(/path/to/app)(.*) \1\4 if header_location
server localhost 127.0.0.1:9999
La partie http-request set-path
permet de modifier le
path au moment du traitement de la requête (équivalent à
ProxyPass
pour Apache).
Le serveur amont n’ayant aucune information de l’URL intiale, s’il doit envoyer un en-tête de redirection calculé de manière relative à la requête, celui-ci ne sera pas correct. Il faut le modifier à la volée avant de renvoyer la réponse.
La partie http-response replace-header
va donc remplacer
la valeur de l’en-tête Location
. L’expression régulière ne
sera satisfaite que si le domaine d’origine est utilisé (ou totalement
absent), conservant ainsi la possibilité d’avoir des redirections
intactes vers d’autres domaines. Détail des captures :
- http ou https, suivi du host et éventuellement un port (facultatif)
- port (factultatif)
- partie du path à supprimer
- reste du path à garder
L’utilisation d’un ACL (très rapide) permet de ne faire l’opération (plus lente) que si l’entête est présent.
Rediriger le trafic vers le serveur web local
C’est utile par exemple pour accéder aux graphes Munin du load-balancer.
Il faut faire écouter le serveur web local sur le port 81 (avec les bonnes restrictions d’accès) en activant le Proxy Protocol (exemple pour Nginx), et y rediriger les requêtes :
frontend myfront
(...)
acl is_localhost hdr(host) -i <LOAD_BALANCER_HOSTNAME> <LOAD_BALANCER_HOSTNAME>.<LOAD_BALANCER_DOMAIN> # ex : mylb mylp.mydomain.com
acl is_wan_ip hdr(host) -m ip <LOAD_BALANCER_IP_WAN>
use_backend be_localhost if is_localhost || is_wan_ip
(...)
backend be_localhost
server local_www 127.0.0.1:81 maxconn 10 send-proxy-v2
ATTENTION : Si le backend final utilise apache2 avec
mpm itk, il faut mettre la directive http-reuse never
dans
le backend.
Sinon haproxy va faire du keepalive et réutiliser des connections http pour différents vhosts, ce qui posera problème car avec apache / mpm itk, chacunes des connexions à des privilèges restreints à un vhost en particulier.
Exemple en mode TCP
frontend fe_memcached
bind 127.0.0.1:11211
mode tcp
default_backend be_memcached
backend be_memcached
mode tcp
option tcp-check
default-server check on-marked-down shutdown-session on-marked-up shutdown-backup-session
server nosql00 192.0.2.3:11211
server nosql01 192.0.2.4:11211 backup
Exemple pour MySQL
Il existe 2 modes principaux pour un proxy MySQL :
- le mode simple, qui effectue un test de connexion au serveur MySQL ;
- le mode avancé, qui exécute des tests poussés (et personnalisés) pour valider le bon fonctionnement du serveur.
Mode simple
HAProxy fourni une option “mysql-check”. Il va alors faire une connexion identifiée au serveur MySQL, puis la fermer et vérifier dans les infos renvoyées que tout semble correct.
Ce mode ne nécessite pas d’outillage supplémentaire et nous le recommandons lorsqu’HAProxy agit seulement comme un proxy et pas comme un load-balancer ou pour de la tolérance de panne.
frontend fe_mysql
bind 127.0.0.1:3306
mode tcp
default_backend be_mysql
backend be_mysql
mode tcp
option mysql-check user haproxy_check post-41
default-server on-marked-down shutdown-sessions on-marked-up shutdown-backup-sessions check
server sql00 192.0.2.1:3306
server sql01 192.0.2.2:3306 backup
Il faut penser à créer l’utilisateur “haproxy_check” (sans mot de passe mais sans droits et restreint à une IP source) sur les serveurs ciblés
L’utilisation de default-server
permet de définir des
caractéristiques communes à tous les serveurs du backend te simplifier
la configuration (cf. documentation).
L’utilisation de on-marked-down shutdown-sessions
permet
d’interrompre immédiatement les sessions en cours lorsqu’un hôte est
considéré comme “DOWN” (cf. documentation).
Idem pour on-marked-up shutdown-backup-session
qui va
couper les sessions sur les serveurs “backup” lorsque le serveur “actif”
change.
Mode avancé
Protection d’attaques
HAProxy peut prendre des mesures face à des attaques de type HTTP flood. Pour cela, il utilise un mécanisme qui permet de suivre l’activité des visiteurs en stockant en mémoire le nombres de requêtes par client. Cela est possible grâce à l’utilisation des « stick tables ». Les « stick tables » fournissent un stockage clé-valeur pouvant être utilisé pour suivre divers compteurs associés à chaque client. Ces compteurs peuvent se baser sur tout ce qui se trouve dans la requête (adresse IP, UserAgent, URL, token…). Les valeurs comptabilisées sont le nombre de requêtes ainsi que le taux sur une période donnée.
La première chose à faire est de définir une directive
stick-table
dans un backend
ou un
frontend
. Il faut avoir à l’esprit que chaque
backend/frontend ne peut contenir qu’une directive
stick-table
. On peut voir cela comme une vraie limitation,
si l’on veut par exemple pouvoir suivre le nombre de requêtes par IP
ainsi que que le nombre de requêtes par IP pour chaque URL. On peut
vouloir également utiliser ces compteurs dans plusieurs backend/frontend
simultanément. La bonne nouvelle est qu’il est possible de définir des
frontend/backend dont la seule utilité est de contenir une directive
stick-table
. Il est alors possible ensuite d’utiliser ce
compteur ailleurs via une directive http-request
y faisant
référence.
Dans l’exemple suivant, on définit deux backends. Le premier sert à enregistrer le nombre de requête par IP « per_ip_rates ». Le second sert à suivre le nombre requête par IP pour chaque URL « per_ip_and_url_rates » :
backend per_ip_rates
stick-table type ip size 1m expire 10m store http_req_rate(10s)
backend per_ip_and_url_rates
stick-table type binary len 8 size 1m expire 1h store http_req_rate(10s)
Pour les détails, dans le backend « per_ip_rates » on définit une
directive stick-table
de type IP qui peut contenir jusqu’à
1 million d’entrées, qui expire au bout de 10 minutes et qui
comptabilise le nombre de requêtes sur les 10 dernières secondes.
Dans le second backend « per_ip_and_url_rates », on définit une
directive stick-table
de type binary (longueur de 8 bits),
qui peut contenir jusqu’à 1 million d’entrées, qui expire au bout d’une
heure et qui comptabilise le nombre de requêtes IP/URL sur les 10
dernières secondes.
On peut ensuite utiliser ces deux compteurs dans les
frontends/backend de notre choix en y faisant référence dans une
directive http-request
via le paramètre
table
.
Dans l’exemple suivant, on crée deux blocs de 2 directives
http-request
.
frontend default
[…]
http-request track-sc0 src table per_ip_rates unless { path_end .css .js .png .jpeg .gif }
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
http-request track-sc1 url32+src table per_ip_and_url_rates unless { path_end .css .js .png .jpeg .gif }
http-request deny deny_status 429 if { sc_http_req_rate(1) gt 10 }
[…]
Le premier bloc contient deux lignes dont la première définit une règle « track-sc0 » qui se basant sur la table du backend « per_ip_rates » en omettant toutes les requêtes vers certains types de fichiers (CSS, JS…). La seconde ligne indique ensuite de renvoyer un status 429 si le nombre des requêtes HTTP/sec pour une même IP selon les données provenant de « track-sc0 » est supérieur à 100 (donc 100 requêtes depuis une même IP sur les 10 dernières seconndes).
Le second bloc contient quant à lui deux lignes dont la première définit une règle « track-sc1 » qui se base sur la table du backend « per_ip_and_url_rates » en omettant toutes les requêtes vers certains types de fichiers (CSS, JS…). La seconde ligne indique ensuite de renvoyer un status 429 si le nombre des requêtes HTTP/sec vers une URL unique depuis une même IP selon les données provenant de « track-sc1 » est supérieur à 10 (donc 10 requêtes depuis une même IP et vers la même URL sur les 10 dernières secondes).
Protocol PROXY
L’utilisation du protocol PROXY permet de faciliter la communication et la traçabilité lorsque des proxys sont impliqués.
Voir la présentation générale du protocol.
HAProxy en amont
Pour envoyer du trafic à un serveur de backend en utilisant le
protocol PROXY, il suffit d’ajouter le mot clé
send-proxy-v2
dans la définition du serveur :
backend be_web
server web01 192.0.2.1:80 […] send-proxy-v2
HAProxy en aval
Pour recevoir du trafic en utilisant le protocol PROXY, il suffit
d’ajouter le mot clé accept-proxy
dans la définition du
binding :
frontend foo
bind […] accept-proxy
Checks
Un article dédié au fonctionnement des checks HAProxy est disponible.
HTTP
Cela consiste à utiliser un check http pour déterminer l’état du serveur.
frontend fe_mysql
bind 127.0.0.1:3306
mode tcp
default_backend be_mysql
backend be_mysql
mode tcp
option httpchk HEAD
http-check disable-on-404
default-server on-marked-down shutdown-sessions on-marked-up shutdown-backup-sessions check port 8306
server sql00 192.0.2.1:3306
server sql01 192.0.2.2:3306 backup
On note l’option httpchk qui va permettre de faire un check en HTTP et vérifier des conditions avancées (réplication OK, etc.).
Un moyen simple (inspiré de ce vieux blog post) est de créer un script qui sera déclenché par xinetd.
# apt install xinetd
On ajoute un service à xinetd, dans
/etc/xinetd.d/mysqlchk
(droits: root:root 0644) :
service mysqlchk
{
flags = REUSE
socket_type = stream
port = 8306
wait = no
user = root
server = /root/mysqlchk
log_on_failure += USERID
disable = no
only_from = 192.0.2.0/27 10.0.0.1/24
per_source = UNLIMITED
}
Il faut penser à ajuster la liste d’adresses IP autorisées dans
only_from
. On peut utiliser IP, des plages… séparées par
des espaces.
On ajoute la ligne suivante dans /etc/services
:
mysqlchk 8306/tcp # mysqlchk
On crée le script à exécuter dans /root/mysqlchk
(droits: root:root 0750) :
#!/bin/sh
# Mysql is down, return a 503
return="503 Service Unavailable"
# Mysql is fine, return a 200
/usr/lib/nagios/plugins/check_mysql -f /etc/mysql/debian.cnf >/dev/null && return="200 OK"
# Mysql is up but replication is not ok, return a 404
# You may want to comment this line in master/master mode
# It disable server (NOLB status) when replication is down or lagging
/usr/lib/nagios/plugins/check_mysql -f /etc/mysql/debian.cnf --check-slave -c 60 >/dev/null || return="404 Not Found"
cat <<EOF
HTTP/1.0 ${return}
Content-Type: Content-Type: text/plain
Content-Length: 0
EOF
On redémarre xinetd (surveiller /var/log/syslog
pour
d’éventuelles erreurs) et on pense à autoriser le port 8306 au niveau
firewall depuis les IP concernées.
Il est également possible d’utiliser tout programme ou script, pourvu qu’au final il puisse être accessible en HTTP.
Ajustement dynamique
Le propos est d’utiliser l’option agent-check
pour
pouvoir ajuster dynamiquement le poids ou les états des membres d’un
backend.
On utilisera ici xinetd
pour pouvoir interroger la
charge de chacun des membres à intervale régulier
frontend fe_www
bind :80
bind :443 ssl crt /etc/ssl/haproxy/ alpn h2,http/1.1
option forwardfor
default_backend be_www
backend be_www
balance roundrobin
option httpchk OPTIONS *
default-server check agent-check agent-inter 5s
server www01 192.0.2.1:80 agent-port 9999
server www02 192.0.2.2:80 agent-port 9999
server www03 192.0.2.3:80 agent-port 9999
Un moyen simple (inspiré de ce blog post) est de créer un script qui sera déclenché par xinetd.
# apt install xinetd
On ajoute un service à xinetd, dans
/etc/xinetd.d/haproxy-agent-check
(droits: root:root 0644)
:
service haproxy-agent-check
{
disable = no
flags = REUSE
socket_type = stream
port = 9999
wait = no
user = nobody
server = /usr/local/bin/haproxy-agent-check
log_on_failure += USERID
only_from = 192.0.2.0/27
per_source = UNLIMITED
}
Il faut penser à ajuster la liste d’adresses IP autorisées dans
only_from
.
On ajoute la ligne suivante dans /etc/services
:
haproxy-agent-check 9999/tcp # haproxy-agent-check
On crée le script à exécuter dans
/usr/local/bin/haproxy-agent-check
(droits: root:root 0755)
:
#!/bin/bash
LMAX=90
load=$(uptime | grep -E -o 'load average[s:][: ].*' | sed 's/,//g' | cut -d' ' -f3-5)
cpus=$(grep processor /proc/cpuinfo | wc -l)
while read -r l1 l5 l15; do {
l5util=$(echo "$l5/$cpus*100" | bc -l | cut -d"." -f1);
[[ $l5util -lt $LMAX ]] && echo "up 100%" && exit 0;
[[ $l5util -gt $LMAX ]] && [[ $l5util -lt 100 ]] && echo "up 50%" && exit 0;
echo "drain";
}; done < <(echo $load)
exit 0
On redémarre xinetd (surveiller /var/log/syslog
pour
d’éventuelles erreurs) et on pense à autoriser le port 9999 au niveau
firewall depuis les IP concernées.
Les valeurs renvoyées peuvent être les suivantes :
- Un % entier (ex 75%) pour l’ajustement du poids
- La chaîne
maxconn:
suivie d’un entier pour spécifier le nombre max de connexions - Les mots
ready
,drain
,maint
,down
etup
pour modifier les états
Il faut bien avoir en tête qu’il y a 2 types d’états :
- “administrative” : ready (prêt), drain (ne plus envoyer de trafic, sans couper l’existant), maint (maintenance, plus de trafic du tout)
- “operative” : up (actif), down/failed/stopped (inactif)
C’est pourquoi il peut être important de combiner plusieurs mots clés
dans le résultat de l’agent, en particulier up ready
pour
assurer que les 2 états sont bien remis.
On notera que seuls les algorithmes d’équilibrage dynamiques
(roundrobin et leastconn) permettront l’ajustement du poids via
agent-check
. Dans le cas de l’utilisation d’un algorithme
statique comme source
par exemple, seules des opérations
telles que le passage en DRAIN, DOWN, MAINT et UP seront possibles.
Agent-check sur la surveillance de l’espace disque
On peut faire un agent check avec xinetd comme vu plus haut et le
script diskchk.sh
qui permet de surveiller l’espace disque,
exemple avec la partition /var
:
#!/bin/bash
# This variable check current disk space on /var partition in percentage
partition="/var"
disk_space=$(/usr/lib/nagios/plugins/check_disk -c 5% -K 5% -p "$partition" | awk '{ print $9 }' | tr -d -c '0-9')
# This variable is maximum percentage on disk space on /var partition
disk_space_max="5"
if [ "$disk_space" -gt "$disk_space_max" ]; then
echo "ready"
else
echo "drain"
fi
exit 0
Pour utiliser ce script il faut au préalable que le check
nrpe check_disk
soit installé sur la machine que
l’on veut surveiller.
La variable disk_space_max
indique en pourcentage
l’espace disque restant à partir duquel le script retournera
drain
.
Et un exemple de configuration haproxy pour le port 25 en mode tcp :
frontend mail
bind :25
mode tcp
option tcplog
default_backend mail
backend mail
balance roundrobin
default-server check agent-check agent-inter 5s
server mail00 192.168.0.1:25 inter 10000 rise 2 fall 5 check weight 50 agent-port 9999
server mail01 192.168.0.2:25 inter 10000 rise 2 fall 5 check weight 50 agent-port 9999
Dans ce cas le serveur mis en drain
par le script, le
serveur concerné n’enverra plus de trafic, mais les mails en cours
d’envoi ne sont pas coupés.
HTTP basic authentication
Pour mettre en place une authentification HTTP basique au niveau d’HAProxy, définir dans la section globale une liste d’utilisateurs, soit avec un mot de passe en clair, soit avec un mot de passe chiffré :
userlist NomDeMaUserList
user user1 insecure-password passwordEnClair
user user2 password $6$passwordSHA512
[…]
Dans le backend concerné, ajouter :
auth AuthOkayPourMonSite http_auth(NomDeMaUserList)
http-request auth realm Texte if !AuthOkayPourMonSite
Redirection
Remplacez les variables $nom
par vos valeurs (en
enlevant le $
).
Les conditions après le if
sont par défaut
(implicitement) combinées avec des ET logiques. Si nécessaire, on peut
utiliser explicitement le mot-clé or
.
Redirection HTTPS
acl $nom_acl hdr(host) -i domaine-from.org
redirect scheme https code 301 if $nom_acl !{ ssl_fc }
Attention, dans le cas d’une utilisation de certbot pour les renouvellements de certificats Let’s encrypt, il peut être nécessaire d’utiliser un backend spécifique et/ou d’exclure de la redirection le chemin du challenge. Mais ceci sort du champ de cette section.
Redirection d’un domaine
acl $nom_acl hdr(host) -i $domaine-from.org
redirect prefix https://www.domaine-to.org code 301 if $nom_acl
La redirection est faite lorsque l’ACL correspond au domaine spécifié.
Attention, dans le cas d’une utilisation de certbot pour les renouvellements de certificats Let’s encrypt, il peut être nécessaire d’utiliser un backend spécifique et/ou d’exclure de la redirection le chemin du challenge. Mais ceci sort du champ de cette section.
Si la redirection ne concerne qu’un seul domaine, on peut utiliser une ACL dite “anonyme” (= implicite) :
redirect prefix https://www.domaine-to.org code 301 if { hdr(host) -i domaine-from.org }
Résolution DNS et nombre dynamique de serveurs
À partir de HAProxy 1.8
Lorsqu’on ne connait pas l’adresse IP d’un serveur on peut donner à HAProxy un nom de domaine.
Il faut alors ajouter une section resolvers
pour
indiquer à HAProxy comment faire la résolution. Par défaut nous
conseillons de reprendre la même configuration que dans /etc/resolv.conf
(par exemple 192.168.10.1
sur le port 53
),
mais il est possible d’indiquer d’autres resolveurs.
resolvers mydns
nameserver self 192.168.10.1:53
nameserver google 8.8.8.8:53
nameserver quad9 9.9.9.9:53
nameserver cloudflare 1.1.1.1:53
backend myback
balance roundrobin
server www01 www01.example.com:80 check resolvers mydns
server www02 www02.example.com:80 check resolvers mydns
À partir de la verison 2.0 il est possible d’iniquer l’utilisation
automatique du resolveur configuré dans /etc/resolv.conf
:
resolvers mydns
parse-resolv-conf
Lorsque le nom de domaine indiqué comporte plusieurs adresses IP et
qu’on veut avoir autant de serveurs disponibles que d’IP disponibles, on
peut utiliser server-template
. Exemple avec un domaine qui
aurait 4 IP résolues :
backend myback
balance roundrobin
server-template example 4 example.com:80 check init-addr none resolvers mydns
La section resolvers
peut être configurée avec plusieurs
paramètres pour indiquer les timeout, nombres de tentatives,
comportement en cas d’erreur…
Conservation des états
Lorsqu’on reload ou restart HAProxy, les états en mémoire sont perdus. Ça concerne les serveurs ajoutés dynamiquement, les serveurs passés en MAINT ou DRAIN, les poids modifiés…
Pour conserver les états, on peut indiquer à HAProxy de lire les états dans un fichier au démarrage. Ça peut être un fichier global, pour tous les backends, mais ça peut aussi être plus spécifique.
Pour la version simple, on ajoute :
global
[…]
server-state-file /var/lib/haproxy/state
defaults
[…]
load-server-state-from-file global
Par contre, HAProxy ne génère pas ce fichier autmatiquement. On peut le faire via :
# echo "show servers state" | socat stdio unix-connect:/run/haproxy/admin.sock > /var/lib/haproxy/state
Pour avoir une conservation automatique, il faut modifier l’unité
systemd pour générer le fichier d’état avant de stopper ou recharger
HAproxy. On place alors un override systemd, par exemple dans
/etc/systemd/system/haproxy.service.d/keep-state.conf
:
[Service]
# Remove state file after start
ExecStartPost=-/bin/sh -c 'rm -f /var/lib/haproxy/state'
# Store state file before stopping
ExecStop=-/bin/sh -c 'echo "show servers state" | socat stdio unix-connect:/run/haproxy/admin.sock > /var/lib/haproxy/state'
# Store state file before reloading
ExecReload=
ExecReload=/usr/sbin/haproxy -Ws -f $CONFIG -c -q $EXTRAOPTS
ExecReload=/bin/sh -c 'echo "show servers state" | socat stdio unix-connect:/run/haproxy/admin.sock > /var/lib/haproxy/state'
ExecReload=/bin/kill -USR2 $MAINPID
Et après un systemctl daemon-reload
on peut recharger ou
redémarrer HAproxy en conservant les états.
Note: Il est possible que le premier redémarrage après cette configuration ne recharge pas les états. Il faut alors le refaire une fois pour que ça fonctionne bien.
Haute performance
Si HAProxy encaisse un nombre de connexions non négligables, voici nos pistes d’optimisations applicables.
Sources d’inspiration :
- https://medium.com/@pawilon/tuning-your-linux-kernel-and-haproxy-instance-for-high-loads-1a2105ea553e
- https://gist.github.com/jayjanssen/4039319
- Haute performance sous Linux
Optimisations TCP/kernel
net.netfilter.nf_conntrack_max=1000000
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=20
net.ipv4.ip_local_port_range=1025 65534
net.ipv4.tcp_max_orphans=262144
net.core.somaxconn=100000
net.core.netdev_max_backlog=100000
fs.file-max=2000000
Optimisations systemd
$ cat /etc/systemd/system/haproxy.service.d/limits.conf
[Service]
LimitNOFILE=500000
Optimisations haproxy.cfg
global
maxconn 500000
#nbproc 2
# cpu-map 1 0
# cpu-map 2 1
tune.maxrewrite 8000
tune.http.maxhdr 800
tune.bufsize 60000
tune.ssl.cachesize 1000000
defaults
maxconn 500000
Note : à partir d’HAProxy 2.5, les « optimisations » concernant le CPU ne sont plus nécessaires
maxconn global vs defaults
Attention, le maxconn
(global) fixe une limite globale à
HAProxy. Il n’est pas surchargé par le maxconn
(defaults)
qui va s’appliquer à tous les frontend et pourra lui être
surchargé au cas par cas.
Utiliser l’API runtime
HaProxy propose une API accessible via un socket ou un port permettant de lui envoyer dynamiquement des commandes.
Attention, les changements dynamiques de configuration ne seront pas reportés dans la configuration statique.
Documentation : https://www.haproxy.com/documentation/haproxy-runtime-api/reference/
Les exemples d’utilisation suivants peuvent aussi être faits depuis
hatop
à l’onglet 5-CLI
. Par exemple, au lieu
d’entrer
echo "show info" | socat stdio /run/haproxy/admin.sock
, on
peut entrer show info
dans le shell de
hatop
.
Debug
# echo "show info" | socat stdio /run/haproxy/admin.sock
# echo "show acl" | socat stdio /run/haproxy/admin.sock
# echo "show acl #<ID>" | socat stdio /run/haproxy/admin.sock
Note : on peut aussi utiliser
nc(1)
du paquetnetcat-openbsd
:# echo "show info" | nc -U /run/haproxy/admin.sock
Désactiver/Activer un serveur
# echo disable server <backend>/<server> | socat stdio /run/haproxy/admin.sock
# echo enable server <backend>/<server> | socat stdio /run/haproxy/admin.sock
Afficher le status des FRONTEND/BACKEND
# echo "show stat" | socat stdio unix-connect:/run/haproxy/admin.sock | cut -d ',' -f1,2,18
Afficher les certificats SSL utilisés
A partir de HaProxy 2.2 :
# echo "show ssl cert" | socat stdio unix-connect:/run/haproxy/admin.sock # ou autre chemin de socket configuré dans "stats socket"
Lecture des logs
HAProxy a des logs différents selon le type de connexion.
Pour les connexions TCP (Voir la doc complète) :
frontend fnt
mode tcp
option tcplog
log global
default_backend bck
backend bck
server srv1 127.0.0.1:8000
>>> Feb 6 12:12:56 localhost haproxy[14387]: 10.0.1.2:33313 [06/Feb/2009:12:12:51.443] fnt bck/srv1 0/0/5007 212 -- 0/0/0/0/3 0/0
Field Format Extract from the example above
1 process_name '[' pid ']:' haproxy[14387]:
2 client_ip ':' client_port 10.0.1.2:33313
3 '[' accept_date ']' [06/Feb/2009:12:12:51.443]
4 frontend_name fnt
5 backend_name '/' server_name bck/srv1
6 Tw '/' Tc '/' Tt* 0/0/5007
7 bytes_read* 212
8 termination_state --
9 actconn '/' feconn '/' beconn '/' srv_conn '/' retries* 0/0/0/0/3
10 srv_queue '/' backend_queue 0/0
Pour les connexions HTTP (Voir la doc complète) :.
frontend http-in
mode http
option httplog
log global
default_backend bck
backend static
server srv1 127.0.0.1:8000
>>> Feb 6 12:14:14 localhost haproxy[14389]: 10.0.1.2:33317 [06/Feb/2009:12:14:14.655] http-in static/srv1 10/0/30/69/109 200 2750 - - ---- 1/1/1/1/0 0/0 {1wt.eu} {} "GET /index.html HTTP/1.1"
Field Format Extract from the example above
1 process_name '[' pid ']:' haproxy[14389]:
2 client_ip ':' client_port 10.0.1.2:33317
3 '[' request_date ']' [06/Feb/2009:12:14:14.655]
4 frontend_name http-in
5 backend_name '/' server_name static/srv1
6 TR '/' Tw '/' Tc '/' Tr '/' Ta* 10/0/30/69/109
7 status_code 200
8 bytes_read* 2750
9 captured_request_cookie -
10 captured_response_cookie -
11 termination_state ----
12 actconn '/' feconn '/' beconn '/' srv_conn '/' retries* 1/1/1/1/0
13 srv_queue '/' backend_queue 0/0
14 '{' captured_request_headers* '}' {haproxy.1wt.eu}
15 '{' captured_response_headers* '}' {}
16 '"' http_request '"' "GET /index.html HTTP/1.1"
Challenge ACME
Si vous avez besoin de valider des challenges ACME (pour la création/renouvellement de certificats Let’s Encrypt, par exemple), il est possible que le proxy fasse relai.
Il faut d’abord que le frontend qui écoute sur le port de challenge
(80
pour http-01
) détecte les requêtes du
challenge et utilise alors un backend dédié. Si votre challenge est fait
localement ou sur un autre serveur, il faudra ajuster l’adresse du
serveur web faisant réellement le challenge.
frontend fe_www
bind :80
bind :443 ssl crt /etc/ssl/haproxy/ alpn h2,http/1.1
option forwardfor
acl letsencrypt path_dir -i /.well-known/letsencrypt
use_backend be_letsencrypt if letsencrypt
default_backend be_www
backend be_letsencrypt
server localhost 192.168.2.1:80 maxconn 10
Le processus qui générera ensuite le certificat devra générer un fichier pour HAProxy, contenant la concaténation du certificat, l’éventuelle chaîne intermédiaire, la clé privée, les eventuels paramètres Diffie-Helmann et l’éventuelle réponse OCSP.
Attaques (D)DOS
Fail2Ban
Fail2Ban est indépendant de HAProxy, mais
peut être utilisé pour bloquer des requêtes en amont pour qu’elles
n’atteignent pas HAProxy. Cela nous a été utile dans certains cas
d’attaques DDOS avec plusieurs centaines de milliers de requêtes
(maxconn 500000
atteint) où HAProxy devenait lent à traiter
les requêtes.
Exemple d’une règle qui bannit en fonction d’un entête
Host:
spécifique :
[haproxy-bad-host]
enabled = true
port = http,https
logpath = /var/log/haproxy.log
banaction = iptables-multiport
maxretry = 1
bantime = 86400
findtime = 60
failregex = ^%(__prefix_line)s<HOST>.*<NOSRV> .* \{www\.example\.com/bad\}
HAPEE – HAProxy Enterprise Edition
HAProxy est également disponible en édition « Enterprise », avec des fonctionnalités supplémentaires, un support direct et des outils additionnels.
Plusieurs versions sont disponibles en parallèle.
Liens utiles :
- site commercial : https://www.haproxy.com/products/haproxy-enterprise/
- Customer Portal : https://my.haproxy.com/portal/cust/index
- Documentation générale (publique) : https://www.haproxy.com/documentation/hapee/
- Documentation complète (privée) : https://customer-docs.haproxy.com/haproxy-enterprise/
Installation
L’accès aux paquets Debian spécifiques se fait grâce à un dépôt APT dont l’URL contient une clé de license.
L’installation se fait dans /opt
, il ets donc important
d’avoir quelques dizaines/centaines de Mo disponibles sur la partition
associée.
Les binaires chemins et outils changent par rapport à HAProxy. Exemples avec la version 2.4 (LTS actuelle) :
- config générale :
/etc/hapee-2.6/hapee-lb.cfg
- unité systemd :
systemctl status hapee-2.6-lb.service
- binaire principal :
/opt/hapee-2.6/sbin/hapee-lb
- vérification de config :
/opt/hapee-2.6/sbin/hapee-lb -c -V -f /etc/hapee-2.6/hapee-lb.cfg
Outils complémentaires
Des outils complémentaires sont proposés :
- un paquet pour keepalived/vrrp
- un module WAF à base de ModSecurity
- une interface de monitoring plus riche
- …
Logs
Par défaut, HAPEE écrit ses logs dans
/var/log/hapee-2.6/
avec un fichier par jour (grace à une
configuration de rsyslog), et aucune configuration de logrotate n’est
présente. Nous préférons personnaliser cela pour coller à la manière
dont c’est géré dans le paquet Debian de HAProxy.
On log dans un fichier non daté :
diff --git a/rsyslog.d/hapee-26-lb.conf b/rsyslog.d/hapee-26-lb.conf
index 578fbcd..6c0dd48 100644
--- a/rsyslog.d/hapee-26-lb.conf
+++ b/rsyslog.d/hapee-26-lb.conf
@@ -7,8 +7,8 @@ $UDPServerRun 514
$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
# Configure template name for daily rotation
-$template lb-access,"/var/log/hapee-2.6/lb-access-%$YEAR%%$MONTH%%$DAY%.log"
-$template lb-admin,"/var/log/hapee-2.6/lb-admin-%$YEAR%%$MONTH%%$DAY%.log"
+$template lb-access,"/var/log/hapee-2.6/lb-access.log"
+$template lb-admin,"/var/log/hapee-2.6/lb-admin.log"
# By default, hapee-lb sends trafic log with local0 facility and admin
# logs on local1
On ajoute une configuration logrotate dans
/etc/logrotate.d/hapee-26-lb
:
/var/log/hapee-2.6/*.log {
daily
rotate 52
missingok
notifempty
compress
nodelaycompress
postrotate
/usr/lib/rsyslog/rsyslog-rotate
endscript
dateext
}
FAQ
Check HTTP et Erreur 400 Bad Request
HAProxy 2.2 a modifié sonc omportement pour les checks HTTP
(option httpchk
dans un backend) et envoie désormais un
en-tête Content-Length: 0
. Malheureusement certains
serveurs (comme les ELB de AWS) n’aiment pas ça et renvoient une erreur
400 Bad Request
.
D’après ce fil du forum d’HAProxy on peut contourner en faisant du HTTP dans un check TCP :
option tcp-check
tcp-check comment send\ HTTP\ request
tcp-check send GET\ /status\ HTTP/1.0\r\n
tcp-check send User-Agent:\ Haproxy\ Health\ Check\r\n
tcp-check send \r\n
tcp-check expect rstring HTTP/1\..\ 200 comment check\ HTTP\ response
HTTP 100 CONTINUE
Dans le cas de l’utilisation spécifique de
HTTP 100 CONTINUE
, cf HowtoHTTP#http1.1-100-continue nous avons
constaté qu’avec HAProxy 1.8, il ne retransmet pas la réponse
HTTP/1.1 100 Continue
au client comme signalé
ici provoquant un timeout et une erreur 502 renvoyée par HAProxy
(debug complexe par manque de log).
Solution pour contourner : passer en version supérieure (par exemple HAProxy 2.4)
Go away bad bots
Ajout d’une règle :
# goaway bad bots
acl goaway-badbots hdr_sub(user-agent) -i -f /etc/haproxy/blacklist-useragent.txt
http-request deny if goaway-badbots
avec /etc/haproxy/blacklist-useragent.txt
contenant
:
thesis-research-bot
Amazonbot
Bytespider
SeekportBot
PetalBot
DotBot
YandexBot
SEOkicks
serpstatbot
AhrefsBot
MJ12bot