Utilisation du SNI avec Haproxy

Réseau reverse-proxy nginx haproxy sni

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-logo

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

haproxy-SNI

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 :

Article précédent Article suivant