[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.
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.
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
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'utilisateur, soit avec un mot de passe en clair soit avec un mot de passe chiffré :
### 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 web
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 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 :
~~~
backend web
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…