[HAProxy](http://www.haproxy.org/) 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](http://1wt.eu/#wami), 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.
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é 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 »
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`. Si 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*.
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).
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.
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 redirection intactes vers d'autres domaines.
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 restriction d'accès) en activant le Proxy Protocol ([exemple pour Nginx](/HowtoNginx#nginx-en-aval)), 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
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.
L'utilisation de `default-server` permet de définir des caractéristiques communes à tous les serveurs du backend te simplifier la configuration (cf. [documentation](http://docs.haproxy.org/2.4/configuration.html#4.2-default-server)).
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](http://docs.haproxy.org/2.4/configuration.html#5.2-on-marked-down)).
HAProxy peut prendre des mesures face à des attaques de type [HTTP flood](https://en.wikipedia.org/wiki/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](https://www.haproxy.com/blog/introduction-to-haproxy-stick-tables/) ». Les « [stick tables](https://www.haproxy.com/blog/introduction-to-haproxy-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 la paramètre `table`.
Dans l'exemple suivant, on crée deux blocs de 2 directives `http-request`.
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 type 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 toute 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).
L'utilisation du protocol PROXY permet de faciliter la communication et la traçabilité lorsque des proxy sont impliqués.
Voir la [présentation générale du protocol](https://www.haproxy.com/blog/use-the-proxy-protocol-to-preserve-a-clients-ip-address/).
#### 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 :
Un moyen simple (inspiré de ce [vieux blog post](http://sysbible.org/2008/12/04/having-haproxy-check-mysql-status-through-a-xinetd-script/)) est de créer un script qui sera déclenché par **xinetd**.
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.
Un moyen simple (inspiré de ce [blog post](https://icicimov.github.io/blog/server/HAProxy-dynamically-adjust-server-weight-using-external-agent/)) 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` :
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 mot `ready`, `drain`, `maint`, `down` et `up` 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.
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é :
Remplacez les variables `$nom` par vos valeurs (en enlevant le `$`).
Les conditions après le `if` sont pas défaut (implicitement) combinées avec des ET logiques. Si nécessaire, on peut utiliser explicitement le mot-clé `or`.
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.
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.
### 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.
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 adresse 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 :
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.
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.
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.
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](https://discourse.haproxy.org/t/bad-request-on-health-check-due-to-content-length-header/6588) on peut contourner en faisant du HTTP dans un check TCP :