Gérer dynamiquement l’accès à ses environnements avec HAProxy et le SNI

Le problème

Lorsqu’on pratique l’Infrastructure as Code, il est bien pratique de disposer d’un grand nombre d’environnements différents sur lesquels est déployée notre application afin d’en tester différents aspects fonctionnels ou techniques.

On peut ainsi souhaiter disposer d’environnements sur lesquels l’application est déployée à différents niveaux de stabilité (dernier commit de la branche principale vs dernière release), d’environnements qui sont régulièrement reconstruits afin de détecter d’éventuelles régressions dans le code d’infrastructure ou encore d’environnements permettant de tester indépendamment chaque nouvelle fonctionnalité avant de l’intégrer à la branche principale.

Ces environnements sont le plus souvent provisionnés et installés à l’aide de solutions IaaS (AWS, Azure, GCE, OpenStack, …) et d’outils d’Infrastructure as Code (Capistrano, Chef, Puppet, Ansible, …). Ce qui rend l’ajout, la suppression ou la modification d’un environnement quasi gratuit. D’autant plus que c’est le plus souvent l’usine de développement qui se charge de ces opérations.

Une fois ces environnements en place, il faut également les rendre accessibles depuis Internet.

Les solutions

Pour cela la première solution est de simplement accéder aux points d’entrée des différents environnements par leur IP.

Infra as Code _ Gérer dynamiquement l'accès à ses environnements avec HAProxy et le SNI

Cependant dans le cas où l’application développée est basée sur HTTP (site web, API REST, …)  l’en-tête ‘Host’ est souvent importante pour déterminer son comportement. Il est donc souhaitable, notamment pour en faciliter l’accès par des navigateurs conventionnels, d’accéder à l’application par un nom de domaine connu de l’application, donc connu lors de son déploiement.

Pour cela, il est par exemple possible de contrôler un service DNS lors du déploiement afin d’associer le ou les noms de domaines souhaités à l’adresse IP de l’environnement.

Infra as Code _ Gérer dynamiquement l'accès à ses environnements avec HAProxy et le SNI-2

La limite de cette solution se trouve essentiellement dans les temps de propagation des mises à jour DNS et les durées de vie des caches DNS qui ne permettent pas d’avoir un contrôle instantané sur les associations DNS->IP.

Afin d’éviter cette limitation, il est possible de mettre en place un wildcard DNS (*.mon.projet.net) pointant vers une IP unique sur laquelle est installé un reverse-proxy HTTP dont la configuration sera modifiée afin de diriger les requêtes destinées aux différents environnements en fonction de leur en-tête ‘Host’.

On pourra par exemple utiliser HAProxy dans son mode reverse-proxy HTTP dans ce but.

Infra as Code _ Gérer dynamiquement l'accès à ses environnements avec HAProxy et le SNI-3-bis

Quid du SSL ?

Se pose alors la question du SSL. En effet, il n’est pas rare qu’une application HTTP ait besoin d’être sécurisée (dés que des comptes utilisateurs sont gérés, il doit y avoir une sécurisation de la connexion), on utilise alors HTTPS, c’est à dire HTTP à travers SSL/TLS. Ce protocole de chiffrement vient se placer entre le protocole TCP et le protocole HTTP et permet d’établir une connexion sécurisée entre le client et le serveur et d’authentifier le serveur auprès du client. Ainsi, seul le serveur authentifié est capable de déchiffrer le contenu HTTP qui lui est envoyé par le client et vice-versa.

Or c’est dans ce contenu HTTP que se trouve l’en-tête ‘Host’ qui permet au reverse-proxy d’effectuer son routage. Pour accéder à cette en-tête, le reverse-proxy n’a donc pas d’autre choix que d' »usurper » l’identité des points d’entrée des environnements en établissant une connexion SSL/TLS avec le client à leur place. Le reverse-proxy établit ensuite lui même une deuxième connexion SSL/TLS avec le point d’entrée de l’environnement visé, à travers laquelle il transmet le contenu HTTP. Le client n’établit donc pas de connexion SSL/TLS avec ce point d’entrée, ce qui l’empêche de tester sa configuration SSL/TLS et rends également impossible l’authentification par certificat client qui se fait lors de l’établissement d’une connexion SSL/TLS entre client et serveur.

Infra as Code _ Gérer dynamiquement l'accès à ses environnements avec HAProxy et le SNI-4

Au cours d’une communication HTTPS relayée par un reverse-proxy HTTP, le client n’établit jamais de connexion SSL/TLS directe avec le serveur.

Ce mode de fonctionnement “en coupure” est souvent rencontré car il permet, lorsque le reverse-proxy communique directement en HTTP avec le serveur, de ne plus avoir à se soucier du SSL/TLS côté serveur applicatif, le reverse-proxy se chargeant de cet aspect de la communication avec le client. C’est notamment la méthode qui est choisie par la solution de PaaS OpenShift ; OpenShift propose une brique OpenShift Router qui a justement pour rôle d’effectuer les routage des requêtes entrant en HTTP et HTTPS vers les applications concernées. Cette solution a en effet beaucoup de sens dans le contexte d’un PaaS au sein duquel les applications sont très peu coûteuse à déployer et très volatiles.

Then the IETF said: “Let there be SNI”, and there was SNI

Heureusement, une solution à ce problème existe déjà : le SNI (Server Name Indication), une extension TLS qui permet justement au client d’annoncer le nom de domaine qu’il souhaite atteindre dès qu’il initie la connexion SSL. Cette extension est maintenant supportée par tous les navigateurs récents (Internet Explorer sous Windows XP n’en fait pas partie…) et par la majorité des librairies TLS, bien que certaines librairies HTTP(S) ne le supportent pas encore.

Grâce à cette extension, un reverse-proxy peut donc, en théorie, directement transmettre le flux SSL du client au serveur visé sans l’interrompre pour lire les en-têtes HTTP.

HAProxy en mode TCP est capable de faire exactement cela (testé avec HAProxy en version 1.5.4) :

# deux reverse-proxies sont configurés dans ce fichier :

# le premier est un reverse-proxy HTTP simple gérant le trafic arrivant
# en HTTP, donc sur le port 80
listen http_reverseproxy *:80
    mode http

    # le reverse-proxy HTTP utilise le header ‘host' pour son routage
    # “-m end” est une condition de matching indiquant que la chaîne
    # de caractères testée (“req.hdr(host)”) doit finir par la chaîne de
    # caractères donnée en paramètre (“demo.mon.projet.net”)
    use-server demo if { req.hdr(host) -m end demo.mon.projet.net }
    use-server dev if { req.hdr(host) -m end dev.mon.projet.net }

    # il faut également indiquer à quoi correspondent les serveurs indiqués
    # dans les directives “use-server”
    # “weight 0” attribue un poids nul à ces serveurs afin qu’ils ne
    # puissent pas recevoir de trafic autre que celui routé par les
    # directives “use-server”
    server demo  <adresse point d’entrée DEMO>:80 weight 0
    server dev  <adresse point d’entrée DEMO>:80 weight 0

    # un serveur par défaut peut être configuré, c’est le seul à ne pas
    # avoir un poids nul (le poids par défaut est de 1). Il est ici
    # désactivé, la requête ne sera donc pas transmise si elle ne
    # correspond à aucun des environnements
    server http_default 127.0.0.1:8080 disabled

# le second est un reverse-proxy TCP qui base son routage sur le SNI et
# gère le trafic arrivant en HTTPS, donc sur le port 443
listen ssl_reverseproxy *:443
    # nous fonctionnons cette fois ci en mode TCP
    mode tcp

    # 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 }

    # cette fois ci 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-server demo if { req.ssl_sni -m end demo.mon.projet.net }
    use-server dev if { req.ssl_sni -m end dev.mon.projet.net }

    server demo <adresse point d’entrée DEMO> weight 0
    server dev <adresse point d’entrée DEV> weight 0

    server ssl_default 127.0.0.1:4433 disabled

Ainsi un reverse-proxy HAProxy placé sur le nom de domaine wildcard est capable de distribuer les requêtes HTTP et HTTPS sur les différents environnements sans interrompre la continuité du SSL entre les clients et les points d’entrée des environnements comme le ferait une reverse-proxy HTTP/HTTPS. À la place 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.

Cette configuration peut ensuite être intégrée à un outil de gestion de configuration afin d’être mise à jour automatiquement au fil des créations, suppressions et modifications des environnements.

Cette solution a toutefois des limites qu’il faut garder à l’esprit :

  • Elle ne résout rien pour les clients ne supportant pas le SNI, mais un fallback est possible.
  • La reconfiguration d’HAProxy est une opération très rapide, mais pas parfaitement transparente : il y a un risque de perte de trafic et de reset de certaines connexions (plus d’explications et des propositions de solutions dans cet article).