Dans le cadre de mes expérimentations, j'ai eu besoin de mettre en place un deuxième reverse-proxy pour mon environnement de lab afin d'éviter de toucher à celui qui est en prod. N'ayant qu'une seule adresse IP publique, j'ai mis en place HAProxy en frontal de mes deux reverse-proxy auxquels il enverra les requêtes suivant le nom DNS du domaine donné en s'appuyant sur le SNI (Server Name Indication) appelé aussi familièrement "routage SSL".
HAProxy est à l'origine un répartiteur de charge créé début 2001 dans des environnements très exigeants en matière de fiabilité, et qui depuis n'a eu de cesse de toujours accroître la fiabilité des infrastructures au cœur desquelles il s'intègre. Son utilisation principale le place en frontal des serveurs d'applications web bien que de nombreux autres usages soient fréquemment rencontrés.
Le SNI est une extension TLS qui permet au client d’annoncer le nom de domaine qu’il souhaite atteindre dès qu’il initie la connexion SSL. Cette extension est supportée par tous les navigateurs récents et par la majorité des librairies TLS. Grâce à cette extension, Haproxy en mode TCP peut donc directement transmettre le flux SSL du client au serveur visé (ici Nginx) sans l’interrompre pour lire les en-têtes HTTP. HAProxy effectue un routage du flux TCP contenant le flux HTTPS sans toucher au flux chiffré, simplement à partir d’une information qui se trouve en clair dans ce flux : le SNI.
Schéma de principe
L'installation d'HAProxy a été réalisée sur Ubuntu 22.04.
# apt install haproxy
# vim /etc/haproxy/haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
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
frontend stats
# Pour accéder aux stats sur l'url http://IP_HAPROXY:8404/stats
mode http
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats admin if LOCALHOST
frontend http
bind IP_HAPROXY:80
mode http
redirect scheme https code 301
frontend https
bind IP_HAPROXY:443
mode tcp
option tcplog
# cette ligne indique à HAProxy qu’il doit attendre d’avoir accumulé
# suffisamment d’information afin que l’inspection du contenu TCP
# puisse avoir lieu (avec une durée maximale de 5 seconde)
tcp-request inspect-delay 5s
# nous acceptons d’établir la connexion uniquement si la requête est un
# “Client Hello” (donc de hello_type 1), c’est à dire le premier message
# envoyé par un client pour établir une connexion SSL (et contenant les
# informations de SNI)
# cette condition permet également de s’assurer que suffisamment
# d’octets ont été accumulés avant de tenter d’accéder aux informations
# de SNI pour effectuer le routage
tcp-request content accept if { req_ssl_hello_type 1 }
# la chaîne de caractère testée pour le matching
# est req.ssl_sni, c’est à dire le champ SNI qu’a trouvé HAProxy
use_backend nginxdev if { req_ssl_sni -i dev-app1.domaine.tld }
use_backend nginxdev if { req_ssl_sni -i dev-app2.domaine.tld }
# Les adresses non routées vers un backend spécifique sont routées par défaut
# vers le backend nginxprod
default_backend nginxprod
backend nginxprod
mode tcp
option ssl-hello-chk
server nginxprod IP_NGINX_PROD:443 send-proxy-v2 check
backend nginxdev
mode tcp
option ssl-hello-chk
server nginxdev IP_NGINX_DEV:443 send-proxy-v2 check
/etc/nginx/nginx.conf :
http {
(...)
# Afin d'obtenir l'IP réelle du client et non celle du serveur faisant tourner Haproxy
set_real_ip_from IP_HAPROXY;
real_ip_header proxy_protocol;
(...)
}
# /etc/nginx/sites-enabled/001-app1.domaine.tld :
server {
listen 443 ssl http2 proxy_protocol;
server_name app1.domaine.tld;
(...)
Let's Encrypt : afin de générer les certificats Let's Encrypt, j'utilise Certbot avec le challenge DNS.
Grâce à la mise en place du SNI avec Haproxy, je peux dorénavant travailler sereinement sur mes expérimentations sans peur de casser mon environnement en production. Je peux également travailler avec des noms de domaine de portée globale afin de bénéficier des certificats Let's Encrypt et de travailler au plus proche de l'environnement de prod.
Source :