<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Haproxy on Vezpi Lab</title>
        <link>https://blog.vezpi.com/fr/tags/haproxy/</link>
        <description>Recent content in Haproxy on Vezpi Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>fr</language>
        <lastBuildDate>Sun, 20 Sep 2026 18:54:54 +0000</lastBuildDate><atom:link href="https://blog.vezpi.com/fr/tags/haproxy/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Installer HAProxy sur OPNsense pour remplacer Caddy</title>
            <link>https://blog.vezpi.com/fr/post/install-haproxy-opnsense-replace-caddy/</link>
            <pubDate>Sat, 19 Sep 2026 00:00:00 +0000</pubDate>
            <guid>https://blog.vezpi.com/fr/post/install-haproxy-opnsense-replace-caddy/</guid>
            <description>&lt;img src=&#34;https://blog.vezpi.com/en/post/install-haproxy-opnsense-replace-caddy/cover-23.webp&#34; alt=&#34;Featured image of post Installer HAProxy sur OPNsense pour remplacer Caddy&#34; /&gt;&lt;h2 id=&#34;introduction&#34;&gt;Introduction&#xA;&lt;/h2&gt;&lt;p&gt;Mon cluster OPNsense est la porte d’entrée de mon homelab. Il reçoit le trafic HTTPS, décide de sa destination, puis le transmet soit aux services derrière Traefik, soit directement aux interfaces d’infrastructure comme Proxmox, TrueNAS et OPNsense lui-même.&lt;/p&gt;&#xA;&lt;p&gt;Jusqu’à récemment, ce travail était assuré par Caddy. Il était simple à configurer et, surtout, il gérait les certificats sans rien me demander. Un élément d’infrastructure merveilleusement silencieux.&lt;/p&gt;&#xA;&lt;p&gt;Puis j’ai mis OPNsense à niveau vers la version 26.1. Les requêtes vers les services gérés par Traefik ont commencé à échouer aléatoirement avec &lt;code&gt;SSL_ERROR_INTERNAL_ERROR_ALERT&lt;/code&gt;. Pas toutes les requêtes, ni tous les services, mais suffisamment souvent pour rendre la configuration peu fiable.&lt;/p&gt;&#xA;&lt;p&gt;Je voulais également recréer un endpoint Kubernetes que j’avais auparavant géré avec HAProxy. Plutôt que d’essayer de convaincre Caddy de mieux se comporter, j’ai donc profité de l’occasion pour le remplacer par HAProxy.&lt;/p&gt;&#xA;&lt;p&gt;Voici l’histoire de cette migration, avec le moment où un simple reverse proxy devient un ensemble de dispatchers TCP, de maps, de certificats et d’un peu de configuration HAProxy en mode libre.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ce-que-jattends-du-reverse-proxy&#34;&gt;Ce que j’attends du reverse proxy&#xA;&lt;/h2&gt;&lt;p&gt;La configuration répond à deux besoins de routage différents.&lt;/p&gt;&#xA;&lt;p&gt;La plupart des applications tournent sur &lt;code&gt;dockerVM&lt;/code&gt; (une VM qui exécute Docker, au cas où ce ne serait pas évident), derrière Traefik. Pour ces services, OPNsense doit inspecter le Server Name Indication TLS, puis transmettre la connexion chiffrée à Traefik. Traefik reste responsable de ses propres certificats TLS.&lt;/p&gt;&#xA;&lt;p&gt;D’autres services sont des interfaces d’infrastructure auxquelles je veux seulement accéder depuis les réseaux internes ou via mon VPN :&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Proxmox&lt;/li&gt;&#xA;&lt;li&gt;TrueNAS&lt;/li&gt;&#xA;&lt;li&gt;L’interface Web d’OPNsense&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Pour ces services, HAProxy termine TLS et transmet le trafic HTTP au backend approprié. Je dois également faire fonctionner la configuration sur les deux nœuds de mon cluster OPNsense HA.&lt;/p&gt;&#xA;&lt;p&gt;Dans ma configuration précédente, Caddy gérait ces deux types de trafic grâce à ses fonctions de reverse proxy et de proxy Layer4. La configuration originale est décrite dans mon article sur &lt;a class=&#34;link&#34; href=&#34;https://blog.vezpi.com/fr/post/opnsense-ha-full-configuration/&#34; &gt;ma configuration OPNsense HA&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;préparer-haproxy-avant-la-bascule&#34;&gt;Préparer HAProxy avant la bascule&#xA;&lt;/h2&gt;&lt;p&gt;J’installe le plugin communautaire &lt;code&gt;os-haproxy&lt;/code&gt; sur le nœud maître OPNsense et je construis la nouvelle configuration alors que Caddy sert encore le trafic de production. Cela rend la migration moins stressante. En théorie, du moins.&lt;/p&gt;&#xA;&lt;p&gt;Je commence par définir les serveurs réels et les pools de backend. &lt;code&gt;dockerVM&lt;/code&gt; reçoit un backend TCP pour le TLS passthrough et un backend HTTP pour les challenges ACME. Les services d’infrastructure reçoivent des backends HTTP avec TLS activé vers leurs serveurs en amont.&lt;/p&gt;&#xA;&lt;p&gt;Le backend Proxmox contient les trois nœuds Proxmox, tandis que TrueNAS et l’interface Web locale d’OPNsense disposent chacun de leur propre backend.&lt;/p&gt;&#xA;&lt;p&gt;⚠️ Une petite surprise apparaît pendant les tests : les interfaces TrueNAS et Proxmox restent bloquées sur leur écran de chargement pendant un long moment. Désactiver HTTP/2 dans leurs pools de backend résout le problème. Ce n’est pas une correction particulièrement spectaculaire, mais c’est exactement le genre de détail qui fait durer une migration plus longtemps que prévu.&lt;/p&gt;&#xA;&lt;h2 id=&#34;un-port-plusieurs-types-de-https&#34;&gt;Un port, plusieurs types de HTTPS&#xA;&lt;/h2&gt;&lt;p&gt;Au début, je teste les services d’infrastructure sur des ports différents. Cela fonctionne, mais ce n’est pas l’objectif. Je veux que &lt;code&gt;proxmox.vezpi.com&lt;/code&gt; et &lt;code&gt;nas.vezpi.com&lt;/code&gt; partagent le même port HTTPS public.&lt;/p&gt;&#xA;&lt;p&gt;HAProxy peut sélectionner un backend HTTP grâce à l’en-tête &lt;code&gt;Host&lt;/code&gt; une fois TLS terminé. Cela me donne un frontend pour les services d’infrastructure, mais ne résout pas le TLS passthrough vers Traefik. Un flux TLS ne peut pas être terminé par HAProxy tout en étant transmis tel quel.&lt;/p&gt;&#xA;&lt;p&gt;💡 La solution est un dispatcher TCP sur le port &lt;code&gt;443&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;HAProxy inspecte le TLS ClientHello, lit le nom d’hôte SNI et prend une première décision de routage sans terminer la connexion :&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Les domaines gérés par Traefik vont directement vers &lt;code&gt;dockerVM&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Les domaines d’infrastructure vont vers un frontend HAProxy local qui gère le TLS offloading et sélectionne ensuite le backend HTTP final.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Le dispatcher a besoin d’un délai d’inspection afin de laisser à HAProxy le temps de lire le ClientHello :&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-plaintext&#34; data-lang=&#34;plaintext&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tcp-request inspect-delay 5s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cela suffit pour conserver le TLS intact jusqu’à Traefik, tout en permettant à HAProxy de router lui-même les interfaces d’infrastructure.&lt;/p&gt;&#xA;&lt;h2 id=&#34;garder-les-services-internes-internes&#34;&gt;Garder les services internes… internes&#xA;&lt;/h2&gt;&lt;p&gt;Exposer Proxmox ou TrueNAS sur internet serait une manière très efficace de me créer du travail pour plus tard. Le dispatcher doit donc également appliquer des règles d’accès.&lt;/p&gt;&#xA;&lt;p&gt;Je définis mes réseaux internes, y compris le réseau VPN, dans une ACL. Les requêtes vers les noms d’hôte internes sont rejetées lorsque leur source ne correspond pas à cette ACL. Le même modèle fonctionne aussi pour les services internes hébergés sur &lt;code&gt;dockerVM&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Au début, je crée une condition et une règle pour chaque nom d’hôte via l’interface du plugin. Cela convient pour deux ou trois services. Mon homelab possède cependant environ trente URLs, et je n’ai aucune envie de créer, nommer et maintenir une petite forêt de règles chaque fois que j’en ajoute une.&lt;/p&gt;&#xA;&lt;p&gt;Les maps sont mieux adaptées. Chaque nom d’hôte correspond à une politique plutôt qu’à un backend directement, par exemple :&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-plaintext&#34; data-lang=&#34;plaintext&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cloud.vezpi.com      EXTERNAL_DOCKERVM&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git.vezpi.com        EXTERNAL_DOCKERVM&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pdf.vezpi.com        INTERNAL_DOCKERVM&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;proxmox.vezpi.com    INTERNAL_INFRA&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nas.vezpi.com        INTERNAL_INFRA&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Le dispatcher TCP lit cette map et route le trafic en fonction de la politique :&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-plaintext&#34; data-lang=&#34;plaintext&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tcp-request inspect-delay 5s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;acl FROM_INTERNAL src 192.168.13.0/24 192.168.88.0/24 10.13.37.0/24 192.168.66.0/24&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;acl SERVICE_DEFINED req.ssl_sni,lower,map_dom(/tmp/haproxy/mapfiles/6a97dc93222c69.76162835.txt) -m found&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;acl SERVICE_EXTERNAL_DOCKERVM req.ssl_sni,lower,map_dom(/tmp/haproxy/mapfiles/6a97dc93222c69.76162835.txt) -m str EXTERNAL_DOCKERVM&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;acl SERVICE_INTERNAL_DOCKERVM req.ssl_sni,lower,map_dom(/tmp/haproxy/mapfiles/6a97dc93222c69.76162835.txt) -m str INTERNAL_DOCKERVM&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;acl SERVICE_INTERNAL_INFRA req.ssl_sni,lower,map_dom(/tmp/haproxy/mapfiles/6a97dc93222c69.76162835.txt) -m str INTERNAL_INFRA&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tcp-request content reject if !SERVICE_DEFINED&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tcp-request content reject if SERVICE_INTERNAL_DOCKERVM !FROM_INTERNAL&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;tcp-request content reject if SERVICE_INTERNAL_INFRA !FROM_INTERNAL&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;use_backend BP_DOCKERVM_HTTPS if SERVICE_EXTERNAL_DOCKERVM || SERVICE_INTERNAL_DOCKERVM&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;use_backend BP_INFRA_FORWARDER if SERVICE_INTERNAL_INFRA&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Le nom du fichier est généré par le plugin et n’est pas particulièrement mémorable. C’est à ce moment que la configuration devient moins agréable qu’avec Caddy. L’interface graphique du plugin ne permet pas d’utiliser directement une map pour l’inspection SNI ; j’utilise donc le champ &lt;code&gt;Option pass-through&lt;/code&gt; du frontend pour écrire moi-même les directives HAProxy.&lt;/p&gt;&#xA;&lt;p&gt;Cela fonctionne bien, mais j’aurais préféré rester entièrement dans les conditions et les règles proposées par l’interface graphique.&lt;/p&gt;&#xA;&lt;h2 id=&#34;gérer-le-http-et-les-certificats&#34;&gt;Gérer le HTTP et les certificats&#xA;&lt;/h2&gt;&lt;p&gt;HAProxy doit également gérer le trafic HTTP sur le port &lt;code&gt;80&lt;/code&gt;. Pour les requêtes classiques, il n’est pas nécessaire d’exposer un service HTTP. L’exception importante concerne le chemin des challenges ACME, qui doit atteindre Traefik pour les domaines qu’il gère.&lt;/p&gt;&#xA;&lt;p&gt;Le dispatcher HTTP vérifie que l’hôte demandé est défini dans la même map, n’accepte que &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt; pour les domaines hébergés sur Docker, puis transmet la requête au backend HTTP de &lt;code&gt;dockerVM&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Caddy gérait auparavant mes certificats. HAProxy ne le fait pas, j’installe donc le plugin &lt;code&gt;os-acme-client&lt;/code&gt; sur OPNsense.&lt;/p&gt;&#xA;&lt;p&gt;J’utilise un challenge DNS-01 OVH. Le client ACME reçoit des identifiants ayant la permission de gérer les enregistrements TXT de ma zone DNS, puis émet les certificats pour les services d’infrastructure :&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;proxmox.vezpi.com&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;cerbere.vezpi.com&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;nas.vezpi.com&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Je commence par émettre un certificat de test auprès de l’autorité de certification staging de Let’s Encrypt, puis je passe à l’autorité de production. Une automation redémarre HAProxy lorsque les certificats sont renouvelés, et le plugin installe automatiquement une vérification quotidienne du renouvellement.&lt;/p&gt;&#xA;&lt;p&gt;Traefik continue de gérer ses propres certificats. HAProxy n’a besoin de certificats que pour les services où il termine lui-même TLS.&lt;/p&gt;&#xA;&lt;h2 id=&#34;la-migration-proprement-dite&#34;&gt;La migration proprement dite&#xA;&lt;/h2&gt;&lt;p&gt;Une fois HAProxy prêt, la bascule finale est courte et volontairement ennuyeuse.&lt;/p&gt;&#xA;&lt;p&gt;Je me connecte au maître OPNsense via son adresse IP plutôt que par le nom d’hôte actuellement servi par Caddy. Je déplace ensuite le dispatcher TCP HAProxy de son port de test vers &lt;code&gt;443&lt;/code&gt;, et le dispatcher HTTP vers &lt;code&gt;80&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;À ce moment-là, je sais que désactiver Caddy va temporairement supprimer l’accès à tous les services proxifiés. Je désactive donc Caddy, j’applique la configuration HAProxy et je commence à vérifier les chemins importants.&lt;/p&gt;&#xA;&lt;p&gt;Les services internes restent inaccessibles depuis l’extérieur et accessibles via le VPN. Les services publics derrière Traefik sont de nouveau disponibles. Enfin, je supprime la règle du firewall qui exposait le port de test.&lt;/p&gt;&#xA;&lt;p&gt;🎯 La migration fonctionne et l’erreur aléatoire &lt;code&gt;SSL_ERROR_INTERNAL_ERROR_ALERT&lt;/code&gt; a disparu.&lt;/p&gt;&#xA;&lt;h2 id=&#34;synchroniser-le-nœud-de-secours&#34;&gt;Synchroniser le nœud de secours&#xA;&lt;/h2&gt;&lt;p&gt;Le reverse proxy fait partie de ma configuration OPNsense hautement disponible, le nœud de secours a donc lui aussi besoin de HAProxy.&lt;/p&gt;&#xA;&lt;p&gt;J’installe et je démarre &lt;code&gt;os-haproxy&lt;/code&gt; sur le nœud de secours. De retour sur le maître, j’ajoute &lt;code&gt;HAProxy Load Balancer&lt;/code&gt; aux services synchronisés par la haute disponibilité OPNsense, puis je lance une synchronisation complète.&lt;/p&gt;&#xA;&lt;p&gt;Le client ACME ne prend pas en charge la HA dans cette configuration. Ce compromis me convient, car il ne renouvelle les certificats que tous les deux mois environ.&lt;/p&gt;&#xA;&lt;p&gt;✅ Après avoir testé à la fois un switchover et un failover, les services restent accessibles. Ce genre de test est beaucoup plus relaxant une fois que le trafic passe réellement par le nouveau proxy.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&#xA;&lt;/h2&gt;&lt;p&gt;HAProxy résout le problème SSL qui m’a poussé à abandonner Caddy et me fournit le routage au niveau TCP dont j’ai besoin dans mon homelab. Le dispatcher basé sur des maps rend également l’ajout de nouveaux noms d’hôte raisonnablement simple, qu’il s’agisse de services publics, de services internes sur &lt;code&gt;dockerVM&lt;/code&gt; ou de services d’infrastructure.&lt;/p&gt;&#xA;&lt;p&gt;Cette migration rend cependant le compromis très clair. Caddy est beaucoup plus simple à configurer et sa gestion des certificats est merveilleusement pratique. HAProxy est plus flexible, mais le plugin OPNsense demande davantage de composants, notamment lorsque l’on mélange inspection SNI, TLS passthrough, TLS offloading, maps et ACL.&lt;/p&gt;&#xA;&lt;p&gt;Pour l’instant, cette complexité supplémentaire en vaut la peine. Mes services sont de nouveau accessibles sans erreurs SSL aléatoires, les règles d’accès sont conservées et la configuration résiste à un failover du firewall.&lt;/p&gt;&#xA;&lt;p&gt;C’est plutôt un bon résultat pour quelque chose qui a commencé avec une erreur agaçante dans un navigateur.&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
