Je suis en pleine refonte de mon infra perso auto-hébergée. Outre la partie de rationalisation des ressources matérielles, j’ai aussi repris pas mal de choses au niveau de mes VMs. L’une de ces choses est la mise en place d’un reverse proxy. Mon choix s’est assez vite porté sur HA Proxy car c’est une solution à la fois à l’état de l’art et bénéficiant d’un recul important (il me semble que le projet fête ses 20 ans).

Description du besoin et des contraintes

Les flux web transitant par le parefeu sont redirigés vers le reverse proxy. Le but principal ici est de permettre à deux serveurs web, tous deux en écoute sur le port 443, d’être accessibles depuis l’extérieur et cela de manière transparente pour les utilisateurs. Le choix de l’un ou l’autre des serveurs backend est fonction du sous-domaine que l’utilisateur souhaite atteindre. Un choix par défaut doit aussi être proposé.

À noter que la gestion des certificats n’est pas assurée par le reverse proxy mais directement par les serveurs. Actuellement, l’une des deux machines gère les certificats Let’s encrypt (demandes et renouvellements périodiques). Les certificats relatifs aux sous-domaines portés par le second serveur sont mis à jour périodiquement depuis le premier serveur par le biais d’un script exécuté dans une tâche cron. Ce choix est très discutable pour plusieurs raisons et on va dire qu’il est historique :D .

Point intéressant ici, les deux serveurs web de backend sont respectivement Apache2 et Nginx. Autre point à prendre en compte, je ne l’ai pas précisé mais toutes les commandes sont exécutées sur des conteneurs LXC Debian 11 à jour au moment où j’écris ces lignes.

Description de l’architecture cible

			Internet
			   |
			Box opérateur
			   |
			Parefeu
			   |
			Reverse proxy
			{HA Proxy}
			/	\
	   Serveur Web 1	Serveur Web 2
	     {apache2}		  {nginx}

Notions importantes

Frontend

Le frontend est l’interface du reverse proxy sur laquelle arrivent les requêtes externes (dans le cas présent, les requêtes HTTP et HTTPS). Il peut y en avoir plusieurs et un frontend peut écouter sur plusieurs ports.

Backend

J’ai employé le terme précédemment. Le backend rassemble le ou les serveurs vers lesquels sont redirigées les requêtes traitées par le frondend. Comme je ne prévois pas de faire de répartition de charge, chacun de mes backends ne fait référence qu’à un seul serveur.

Dans le cas présent, c’est ce qui permet de décider sur quel backend les requêtes arrivant sur le frontend doivent être redirigées. Les différents backends peuvent donc avoir des adresses IP différentes, c’est tout l’intérêt dans mon cas d’usage.

HTTP vs TCP

Ces deux protocoles travaillent sur des couches différentes du modèle OSI. HTTP travaille au niveau de la couche 7 (application) et TCP travaille au niveau de la couche 4 (transport). À chacun de ces protocoles correspond un mode dans HA Proxy.

Pour rappel, j’ai indiqué que les certificats étaient gérés côté serveurs de backend. Pour permettre cela, il est donc nécessaire de se placer en mode proxy transparent, afin de laisser passer le trafic en l’état. Le reverse proxy ne déchiffre alors pas le trafic. Ainsi il faut utiliser le mode TCP pour le frontend et les backends. Cette approche minimise le rôle du reverse proxy mais induit quelques effets de bord dans mon cas d’usage.

SNI (Server Name Indication)

Dans une requête HTTP, Le nom de domaine est accessible en clair dans le header HTTP correspondant (Host). Cette information peut donc être utilisée par le reverse proxy pour aiguiller les paquets lorsque le trafic n’est pas chiffré.

La question est plus complexe lorsqu’il s’agit de traiter des paquets HTTPS lorsque HA Proxy est en mode TCP. Le reverse proxy est en mode transparent et n’a donc pas accès aux informations encapsulées par TLS.

C’est là qu’intervient SNI. SNI est une extension du protocole TLS (la notion est donc sans objet en HTTP). Cela permet d’inclure l’information du nom de domaine lors du handshake TLS. Grâce à cette information, le reverse proxy est en mesure d’aiguiller le trafic vers le bon backend.

Proxy protocol

J’ai découvert le proxy protocol à cette occasion. Une explication, même si elle est probablement imparfaite, s’impose.

Comme je travaille en mode TCP dans HA Proxy, il n’est pas possible de rajouter d’entête HTTP (notamment X-Forwarded-For) aux paquets. Cela ne pose pas de soucis en mode HTTP où les header http sont parsés (il suffit dans ce cas de rajouter option forwardfor une fois le mode http activé).

En conséquence ici, au niveau des serveurs web, l’adresse IP source visible est celle du reverse proxy et non l’adresse IP publique source d’où provient la requête. Cela n’est pas bloquant en soit mais les logs ne contiennent alors plus la bonne information. Dans la mesure où je traite mes logs, je ne peux pas me satisfaire de cette situation.

C’est là qu’intervient le proxy protocol. Ce protocole est implémenté dans HA Proxy (si je ne dis pas de bêtises, il me semble même que HA Proxy en est à l’origine). Pour que la bonne informations d’adresse IP source soit passée côté serveur, ce dernier doit lui aussi être en mesure de comprendre les messages envoyés par ce biais. C’est le cas de Apache2 et de Nginx, moyennant une petite adaptation de configuration.

Scripts

HA Proxy

On peut voir ci-dessous le bout de configuration intéressant par rapport au besoin évoqué précédemment. Deux SNI sont donnés de façon explicite. Les requêtes dont le SNI se termine par celui indiqué sont renvoyés vers la backend correspondant. Les autres sont renvoyées vers le backend par défaut.

/etc/haproxy/haproxy.cfg

....

# Proxys to the webserver backend port 443
#---------------------------------------------------------------------
frontend main_ssl
	bind :443 transparent
	mode tcp
	option tcplog
	# Wait for a client hello for at most 5 seconds
	tcp-request inspect-delay 5s
	tcp-request content accept if { req_ssl_hello_type 1 }

	use_backend foo_ssl if { req_ssl_sni -m end foo.sujets-libres.fr }
	use_backend bar_ssl if { req_ssl_sni -m end bar.sujets-libres.fr }
		
	default_backend default

backend foo_ssl
	mode tcp
	balance roundrobin
	server foo_server <ADRESSE_IP_SERVEUR_2>:443 send-proxy check

backend bar_ssl
	mode tcp
	balance roundrobin
	server bar_server <ADRESSE_IP_SERVEUR_1>:443 send-proxy check

backend default
	mode tcp
	balance roundrobin
	server blop_server <ADRESSE_IP_SERVEUR_1>:443 send-proxy check

J’ai perdu un peu de temps car j’avais omis la ligne suivante :

tcp-request content accept if { req_ssl_hello_type 1 }

Dans ce cas, les SNI n’était pas évalués correctement et toutes les requêtes étaient redirigées vers le backend par défaut.

Une fois cela en place, redémarrer le service HA Proxy :

# systemctl start haproxy.service

Serveur web Apache2

Rien de complexe ici. Il faut simplement activer le module apache2 permettant d’interpréter les paquets envoyés via proxy protocol :

# a2enmod remoteip

Pour chaque hôte virtuel, activer proxy protocol :

/etc/apache2/sites-available/bar-le-ssl.conf

<VirtualHost *:443>
	DocumentRoot /CHEMIN/VERS/BAR/www/
	ServerName bar.sujets-libres.fr
	
	RemoteIPProxyProtocol On
	
	...

</VirtualHost>
	

Recharger apache2 :

# systemctl restart apache2

Serveur web Nginx

Proxy protocol est activé par défaut. Pour chaque site, il faut simplement indiquer ce qui suit dans la partie server :

/etc/nginx/sites-available/foo

server {
  listen 443 ssl proxy_protocol;
  listen [::]:443 ssl proxy_protocol;
  server_name foo.sujets-libres.fr;

  set_real_ip_from <ADRESSE_IP_REVERSE_PROXY>/32;
  real_ip_header proxy_protocol;
  
 ...
 
}

Reste ensuite à redémarrer le service nginx :

# systemctl restart nginx.service

Conclusion

L’infra est en place depuis quelques jours et fonctionne bien. Je prévois quand même de revoir ma manière de gérer les certificats TLS. Le sujet n’est donc pas totalement plié…