Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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

Clients sur Internetsuivi.sorbier-logistique.fr → 203.0.113.17pfSense : NAT vers 192.168.10.225VIP .225lb1 .228HAProxy actif, priorité 150lb2 .229HAProxy secours, priorité 130VRRPsrv-web1 .226Nginx, PHP-FPMsrv-web2 .227Nginx, PHP-FPMchemins de lb2, utilisés s’il prend l’adresse virtuelleContrôle de santé : GET /sante.php toutes les 2 s ; en-tête X-Forwarded-For ajouté.
Le site de suivi derrière deux répartiteurs HAProxy : une adresse publique, deux serveurs web, aucun point unique de panne
FonctionCe que le reverse proxy apporte
Point d'entrée uniqueUne seule adresse et un seul nom publiés ; les serveurs derrière peuvent changer sans que les clients le voient
Terminaison TLSLe certificat et la clé sont sur le proxy seulement ; les applications n'ont pas à gérer TLS
MasquageLes serveurs internes, leurs ports et leurs versions ne sont pas exposés
Répartition de chargeLes requêtes sont réparties entre plusieurs serveurs, et un serveur en panne est écarté
FiltrageLimitation du débit, blocage d'adresses, pare-feu applicatif (WAF)
Cache, compressionLes 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.

nginx
# /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;
    }
}
DirectiveRôle
proxy_pass http://127.0.0.1:3000Transmet la requête à l'application
include proxy_paramsLe 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, ConnectionLaissent passer les connexions WebSocket, qu'une application temps réel ouvre en « montant » une connexion HTTP
map $http_upgrade $connection_upgradeCalcule 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êteContenu
X-Forwarded-ForL'adresse du client, suivie de celles des proxys traversés : 198.51.100.23, 192.168.10.228
X-Real-IPL'adresse du client (propre à Nginx)
X-Forwarded-Protohttps 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 :

nginx
# 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 :

bash
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 :

text
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émentRôle
frontendCe qui reçoit les clients : une adresse, un port, des règles
backendUn 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 forwardforAjoute l'en-tête X-Forwarded-For
balance roundrobinChacun 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 2Un 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 statistiquesUne 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é.

bash
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.

AlgorithmeFonctionnementQuand
roundrobinChacun son tour, éventuellement pondéré (weight)Serveurs équivalents, requêtes courtes : le cas du site de suivi
leastconnLe serveur qui a le moins de connexions en coursConnexions longues, durées inégales
sourceUne même adresse cliente va toujours au même serveurPersistance 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.

bash
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.

bash
sudo apt install keepalived

/etc/keepalived/keepalived.conf sur lb1 :

text
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.

bash
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 :

nginx
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.

text
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é ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Messagerie sous Linux : relais Postfix et IMAP avec DovecotRevoir le sommaire du cours