Chapitre 1021 minIntermédiaire
Reverse proxy et répartition de charge : Nginx, HAProxy et keepalived
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer ce qu'apporte un reverse proxy et le distinguer d'un proxy ;
- publier une application interne derrière Nginx, WebSocket compris ;
- transmettre l'adresse du client sans faire confiance à n'importe qui ;
- répartir la charge avec HAProxy et des contrôles de santé HTTP ;
- supprimer le point unique de panne du répartiteur avec keepalived.
3 exercices corrigés1 schéma
Un reverse proxy (proxy inverse) est un serveur placé devant un ou plusieurs serveurs web : il reçoit les requêtes des clients à leur place, les transmet au bon serveur, puis renvoie la réponse. Il masque les serveurs internes, porte le certificat TLS, et peut répartir la charge entre plusieurs serveurs : on parle alors de répartiteur de charge (load balancer). Ce chapitre publie une application interne derrière Nginx, explique comment transmettre l'adresse du vrai client, répartit le site de suivi entre deux serveurs avec HAProxy, puis supprime le point unique de panne avec keepalived. La théorie de la haute disponibilité (calculs, algorithmes, clusters) est dans le cours Haute disponibilité.
À quoi sert un reverse proxy
| Fonction | Ce que le reverse proxy apporte |
|---|---|
| Point d'entrée unique | Une seule adresse et un seul nom publiés ; les serveurs derrière peuvent changer sans que les clients le voient |
| Terminaison TLS | Le certificat et la clé sont sur le proxy seulement ; les applications n'ont pas à gérer TLS |
| Masquage | Les serveurs internes, leurs ports et leurs versions ne sont pas exposés |
| Répartition de charge | Les requêtes sont réparties entre plusieurs serveurs, et un serveur en panne est écarté |
| Filtrage | Limitation du débit, blocage d'adresses, pare-feu applicatif (WAF) |
| Cache, compression | Les ressources statiques sont servies sans solliciter l'application |
Un reverse proxy n'est pas un proxy tout court (dit « proxy direct ») : le proxy direct est placé devant les clients de l'entreprise et filtre leur navigation vers Internet (cours Cybersécurité des infrastructures, proxy et filtrage web) ; le reverse proxy est placé devant les serveurs et reçoit les visiteurs.
C'est aussi une réponse classique à l'exigence « solution d'accès sécurisé à un service interne (intranet) ou externe (extranet) » de l'environnement de l'E6 SISR, et HAProxy répond à celle de la « répartition de charge » : l'annexe du référentiel ne nomme aucun produit, c'est à toi de justifier ton choix (environnement de l'E6 SISR).
Nginx en reverse proxy devant une application
Grafana, l'outil de tableaux de bord de l'équipe, tourne sur srv-journaux et écoute sur 127.0.0.1:3000, en HTTP, sans certificat. Plutôt que de lui confier TLS, on place Nginx devant, sur la même machine, avec un certificat de Sorbier-AC pour le nom tableaux.ad.sorbier.example.
# /etc/nginx/sites-available/tableaux, sur srv-journaux
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
http2 on;
server_name tableaux.ad.sorbier.example;
ssl_certificate /etc/ssl/tableaux/tableaux.crt;
ssl_certificate_key /etc/ssl/tableaux/tableaux.key;
access_log /var/log/nginx/tableaux.access.log;
error_log /var/log/nginx/tableaux.error.log;
location / {
proxy_pass http://127.0.0.1:3000;
include proxy_params;
}
# Connexions WebSocket (mises à jour en direct des tableaux de bord)
location /api/live/ {
proxy_pass http://127.0.0.1:3000;
include proxy_params;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
| Directive | Rôle |
|---|---|
proxy_pass http://127.0.0.1:3000 | Transmet la requête à l'application |
include proxy_params | Le fichier de Debian qui transmet les en-têtes Host, X-Real-IP, X-Forwarded-For et X-Forwarded-Proto |
proxy_http_version 1.1, Upgrade, Connection | Laissent passer les connexions WebSocket, qu'une application temps réel ouvre en « montant » une connexion HTTP |
map $http_upgrade $connection_upgrade | Calcule l'en-tête Connection selon que le client demande ou non une connexion WebSocket |
Comme l'application n'écoute que sur 127.0.0.1, personne ne peut la joindre sans passer par Nginx, donc sans TLS. C'est le schéma le plus courant pour publier une application qui a son propre serveur HTTP (Grafana, une API Node.js, une application Java) : l'application sur une adresse locale, le reverse proxy devant.
Transmettre l'adresse du client
Derrière un proxy, l'application et le serveur web reçoivent toutes les connexions depuis l'adresse du proxy. Les journaux deviennent inutiles (« tout vient de 127.0.0.1 »), et une limitation par adresse IP bloquerait tout le monde d'un coup. Le proxy ajoute donc des en-têtes :
| En-tête | Contenu |
|---|---|
X-Forwarded-For | L'adresse du client, suivie de celles des proxys traversés : 198.51.100.23, 192.168.10.228 |
X-Real-IP | L'adresse du client (propre à Nginx) |
X-Forwarded-Proto | https ou http : le protocole utilisé par le client, que l'application ne voit pas si le proxy déchiffre |
Sur les serveurs web placés derrière le proxy, le module realip de Nginx remplace l'adresse de la connexion par celle de l'en-tête, seulement si la requête vient d'un proxy de confiance :
# Dans le server block de srv-web1 et srv-web2
set_real_ip_from 192.168.10.228;
set_real_ip_from 192.168.10.229;
real_ip_header X-Forwarded-For;
HAProxy : répartir le site de suivi sur deux serveurs
Le site de suivi est devenu indispensable aux clients de Sorbier. Pour qu'une panne ou une mise à jour de srv-web ne le coupe plus, l'équipe le passe sur deux serveurs web, srv-web1 (192.168.10.226) et srv-web2 (192.168.10.227), configurés à l'identique comme aux chapitres 2 et 4, derrière deux répartiteurs HAProxy, lb1 (192.168.10.228) et lb2 (192.168.10.229), tous dans la DMZ. Les répartiteurs se partagent l'adresse 192.168.10.225, l'ancienne adresse de srv-web : la redirection du pare-feu et le DNS public ne changent pas.
Sur pfSense, les règles sortantes de la DMZ qui citaient srv-web .225 comme source changent en revanche : .225 est désormais l'adresse des répartiteurs. Crée un alias DMZ_WEB (.226, .227) autorisé vers srv-gestion en TCP 8443, un alias DMZ_SERVEURS (.226 à .229) autorisé vers srv-journaux en TCP 6514 et vers les dépôts de mises à jour, et autorise lb1 vers l'API de Let's Encrypt. Sans la première règle, le contrôle de santé (/sante.php interroge l'API de srv-gestion) échoue et HAProxy écarte les deux serveurs. Les règles de la DMZ et la matrice des flux sont expliquées dans Cybersécurité des infrastructures, et leur écriture dans pfSense, alias compris, dans le cours pfSense (règles, ordre et alias, DMZ et proxy inverse HAProxy).
HAProxy est un répartiteur de charge et un reverse proxy libre, spécialisé, très performant, qui fonctionne en HTTP (couche 7) ou en TCP (couche 4). Sur lb1 et lb2 :
sudo apt install haproxy
haproxy -v
# HAProxy version 3.0.x …
Garde les sections global et defaults du fichier /etc/haproxy/haproxy.cfg de Debian (journal vers le journal système, compte haproxy, mode HTTP, délais), et ajoute :
frontend fe_http
bind :80
acl acme path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 unless acme
use_backend be_acme if acme
frontend fe_https
bind :443 ssl crt /etc/haproxy/certs/suivi.pem alpn h2,http/1.1
http-request set-header X-Forwarded-Proto https
option forwardfor
default_backend be_suivi
backend be_suivi
balance roundrobin
option httpchk
http-check send meth GET uri /sante.php ver HTTP/1.1 hdr Host suivi.sorbier-logistique.fr
http-check expect status 200
default-server inter 2s fall 3 rise 2
cookie SERVEUR insert indirect nocache httponly secure
server web1 192.168.10.226:80 check cookie web1
server web2 192.168.10.227:80 check cookie web2
backend be_acme
server certbot 192.168.10.228:8888
listen statistiques
bind :8404
http-request deny unless { src 192.168.10.208/28 }
stats enable
stats uri /
stats refresh 10s
| Élément | Rôle |
|---|---|
frontend | Ce qui reçoit les clients : une adresse, un port, des règles |
backend | Un groupe de serveurs et la façon de répartir entre eux |
bind :443 ssl crt … | Écoute en TLS sur le port 443 de toutes les adresses, VIP comprise, avec le certificat (chaîne et clé dans un même fichier .pem) |
option forwardfor | Ajoute l'en-tête X-Forwarded-For |
balance roundrobin | Chacun son tour |
option httpchk, http-check … | Contrôle de santé : HAProxy demande /sante.php à chaque serveur et attend une réponse 200 |
inter 2s fall 3 rise 2 | Un contrôle toutes les 2 secondes ; un serveur est écarté après 3 échecs, réintégré après 2 succès |
cookie SERVEUR insert … | Persistance : HAProxy dépose un cookie qui renvoie chaque visiteur vers le même serveur |
listen statistiques | Une page d'état, réservée au réseau d'administration |
Le script /sante.php de l'application ne répond 200 que si elle fonctionne vraiment (par exemple, si l'API de srv-gestion répond) et 503 sinon. Un contrôle qui ne vérifie que l'ouverture du port 80 laisserait en service un serveur dont PHP-FPM est arrêté.
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
# Configuration file is valid
sudo systemctl reload haproxy
Pourquoi la persistance ? L'application garde la session de chaque visiteur dans des fichiers, sur le serveur qui l'a créée. Si la requête suivante arrive sur l'autre serveur, la session est inconnue et le visiteur est déconnecté. Le cookie de persistance l'évite. L'autre solution, plus robuste, est de partager les sessions entre les serveurs (dans une base, ou un service comme Redis) : un serveur peut alors tomber sans déconnecter personne.
| Algorithme | Fonctionnement | Quand |
|---|---|---|
roundrobin | Chacun son tour, éventuellement pondéré (weight) | Serveurs équivalents, requêtes courtes : le cas du site de suivi |
leastconn | Le serveur qui a le moins de connexions en cours | Connexions longues, durées inégales |
source | Une même adresse cliente va toujours au même serveur | Persistance sans cookie, en mode TCP |
Sur les deux serveurs web, le pare-feu local n'accepte le port 80 que des adresses de lb1 et lb2, et Nginx applique set_real_ip_from pour ces deux adresses. Teste une panne : arrête Nginx sur srv-web1, et observe dans la page de statistiques (http://192.168.10.228:8404/ depuis le poste d'administration) le serveur web1 passer en rouge en 6 secondes, pendant que le site continue de répondre.
Le certificat sur les répartiteurs
C'est maintenant HAProxy qui présente le certificat de suivi.sorbier-logistique.fr. Certbot tourne sur lb1 en mode autonome : pendant le défi HTTP-01, il écoute lui-même sur le port 8888, et la règle acme du frontend fe_http lui envoie les requêtes /.well-known/acme-challenge/. HAProxy attend la chaîne et la clé dans un seul fichier : un script de déploiement les assemble après chaque renouvellement.
sudo apt install certbot
sudo mkdir -p /etc/haproxy/certs
sudo tee /usr/local/bin/certbot-haproxy.sh > /dev/null <<'EOF'
#!/bin/sh
# Lancé par Certbot après chaque obtention ou renouvellement réussi
set -e
umask 077
cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" > /etc/haproxy/certs/suivi.pem
systemctl reload haproxy
EOF
sudo chmod 755 /usr/local/bin/certbot-haproxy.sh
sudo certbot certonly --standalone --http-01-address 192.168.10.228 --http-01-port 8888 \
-d suivi.sorbier-logistique.fr --deploy-hook /usr/local/bin/certbot-haproxy.sh
Le backend be_acme désigne l'adresse de lb1, 192.168.10.228, et non 127.0.0.1, parce que lb2 utilise le même haproxy.cfg. Le jour où lb2 porte l'adresse virtuelle, il transmet les défis HTTP-01 au Certbot de lb1, et le renouvellement réussit quand même. Le pare-feu local de lb1 doit donc accepter ip saddr 192.168.10.229 tcp dport 8888 accept.
Au tout premier passage, le fichier suivi.pem n'existe pas encore et HAProxy refuse de démarrer avec un bind … crt qui le désigne : mets en commentaire le frontend fe_https le temps d'obtenir le certificat, puis rétablis-le et recharge HAProxy.
Certbot enregistre l'option --deploy-hook dans le fichier de renouvellement du certificat : le script sera relancé à chaque renouvellement automatique, et la variable RENEWED_LINEAGE désigne le dossier /etc/letsencrypt/live/suivi.sorbier-logistique.fr. Le fichier suivi.pem contient la clé privée : umask 077 le crée avec les droits 600. Il reste à le recopier sur lb2 après chaque renouvellement (une ligne scp ou rsync par SSH dans le même script, avec une clé dédiée), sinon lb2 présentera un certificat expiré le jour où il prendra la main.
keepalived : ne pas faire du répartiteur un point unique de panne
Avec un seul HAProxy, le site tombe quand le répartiteur tombe, même si les deux serveurs web fonctionnent. keepalived fait flotter l'adresse virtuelle 192.168.10.225 entre lb1 et lb2 avec le protocole VRRP : lb1 la porte ; s'il tombe, ou si son HAProxy s'arrête, lb2 la reprend en quelques secondes.
sudo apt install keepalived
/etc/keepalived/keepalived.conf sur lb1 :
global_defs {
router_id LB1
enable_script_security
}
vrrp_script chk_haproxy {
script "/usr/bin/systemctl is-active --quiet haproxy"
interval 2
fall 2
rise 2
weight -30
}
vrrp_instance VI_SUIVI {
state MASTER
interface ens18
virtual_router_id 60
priority 150
advert_int 1
virtual_ipaddress {
192.168.10.225/28
}
track_script {
chk_haproxy
}
}
Sur lb2, le même fichier avec router_id LB2, state BACKUP et priority 130. Si HAProxy s'arrête sur lb1, sa priorité tombe à 120, sous les 130 de lb2, qui prend l'adresse.
sudo systemctl enable --now keepalived
ip -br address show ens18
# ens18 UP 192.168.10.228/28 192.168.10.225/28 ← sur lb1 : l'adresse virtuelle est là
Les annonces VRRP (protocole IP 112, vers 224.0.0.18) doivent être autorisées par le pare-feu local des deux machines, sinon chacune se croit seule et les deux prennent l'adresse. Le détail de chaque directive, la préemption, les scripts de notification et les tests de bascule sont dans le cours Haute disponibilité.
Exercices
Exercice 1 sur 3 Les journaux qui ne montrent que le proxy Réussi À revoir
Depuis la mise en place des répartiteurs, le journal d'accès de srv-web1 ne contient plus que les adresses 192.168.10.228 et 192.168.10.229, et la règle Nginx qui bloquait une adresse malveillante ne sert plus à rien. Explique pourquoi, et donne la configuration à ajouter, sur quelle machine.
Voir la réponseMasquer la réponse de l'exercice 1
Les serveurs web reçoivent toutes les connexions des répartiteurs, pas des clients : Nginx enregistre et filtre donc l'adresse de lb1 ou lb2. L'adresse du vrai client est dans l'en-tête X-Forwarded-For, qu'HAProxy ajoute grâce à option forwardfor.
Sur srv-web1 et srv-web2, dans le server block :
set_real_ip_from 192.168.10.228;
set_real_ip_from 192.168.10.229;
real_ip_header X-Forwarded-For;
Nginx remplace alors l'adresse de connexion par celle de l'en-tête, uniquement pour les requêtes qui viennent des deux répartiteurs : journaux et règles deny retrouvent l'adresse du client. On vérifie que option forwardfor est bien dans le frontend ou le backend de HAProxy, et que le pare-feu des serveurs web n'accepte le port 80 que des répartiteurs, pour qu'un client ne puisse pas envoyer directement un en-tête inventé.
Tu avais trouvé ?
Exercice 2 sur 3 Le contrôle de santé trompeur Réussi À revoir
Un collègue a écrit server web1 192.168.10.226:80 check sans option httpchk. Un matin, PHP-FPM ne démarre plus sur srv-web1 après une mise à jour : la moitié des visiteurs reçoivent une erreur 502, l'autre moitié voit le site. Explique pourquoi HAProxy n'a pas écarté web1, et corrige.
Voir la réponseMasquer la réponse de l'exercice 2
Sans option httpchk, le contrôle check est un simple contrôle TCP : HAProxy vérifie que le port 80 de srv-web1 accepte une connexion. Nginx tourne toujours et accepte les connexions ; c'est seulement PHP-FPM qui est arrêté, et Nginx renvoie 502 pour chaque page PHP. Le serveur est donc vu « en bonne santé ».
Correction : un contrôle HTTP sur une page qui fait travailler toute la pile.
backend be_suivi
option httpchk
http-check send meth GET uri /sante.php ver HTTP/1.1 hdr Host suivi.sorbier-logistique.fr
http-check expect status 200
server web1 192.168.10.226:80 check cookie web1
server web2 192.168.10.227:80 check cookie web2
/sante.php passe par Nginx, PHP-FPM et l'application : si l'un manque, il répond une erreur et HAProxy écarte le serveur après 3 échecs. Puis haproxy -c -f /etc/haproxy/haproxy.cfg et systemctl reload haproxy.
Tu avais trouvé ?
Exercice 3 sur 3 Reverse proxy ou proxy ? Réussi À revoir
Pour chaque phrase, dis s'il s'agit d'un proxy (direct) ou d'un reverse proxy : (a) il filtre les sites que les salariés peuvent consulter ; (b) il reçoit les visiteurs d'Internet et les envoie vers deux serveurs web ; (c) il porte le certificat de suivi.sorbier-logistique.fr ; (d) le navigateur des postes est configuré pour l'utiliser ; (e) il ajoute l'en-tête X-Forwarded-For.
Voir la réponseMasquer la réponse de l'exercice 3
- (a) Proxy direct : placé devant les clients de l'entreprise, il filtre leur navigation.
- (b) Reverse proxy (et répartiteur de charge) : placé devant les serveurs.
- (c) Reverse proxy : il termine TLS pour le site qu'il publie. Un proxy direct qui inspecte TLS présente, lui, des certificats générés à la volée par une autorité interne.
- (d) Proxy direct : les clients le connaissent (configuration du navigateur, découverte automatique, ou mode transparent). Un visiteur d'un site ne sait pas qu'il passe par un reverse proxy.
- (e) Les deux peuvent l'ajouter ; c'est surtout le reverse proxy qui en a besoin, pour que les serveurs derrière lui connaissent l'adresse du client.
Tu avais trouvé ?