<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Projet on Mook’s Lab Chronicles</title><link>https://blog.homeblack.fr/formats/projet/</link><description>Recent content in Projet on Mook’s Lab Chronicles</description><generator>Hugo -- gohugo.io</generator><language>fr-FR</language><lastBuildDate>Sat, 15 Aug 2026 23:30:00 +0200</lastBuildDate><atom:link href="https://blog.homeblack.fr/formats/projet/index.xml" rel="self" type="application/rss+xml"/><item><title>Le script que j’en avais marre de lancer : la naissance de WarpgateSH</title><link>https://blog.homeblack.fr/p/warpgatesh-synchroniser-acces-ssh-warpgate/</link><pubDate>Tue, 11 Aug 2026 11:57:53 +0200</pubDate><guid>https://blog.homeblack.fr/p/warpgatesh-synchroniser-acces-ssh-warpgate/</guid><description>&lt;img src="https://blog.homeblack.fr/p/warpgatesh-synchroniser-acces-ssh-warpgate/cover.png" alt="Featured image of post Le script que j’en avais marre de lancer : la naissance de WarpgateSH" /&gt;&lt;p&gt;Au travail, j’utilise Teleport. Les accès sont là quand j’en ai besoin, l’expérience est fluide et je n’ai pas à réfléchir au fichier SSH qui se cache derrière. À force de profiter de ce confort, une question a commencé à me trotter dans la tête : pourquoi est-ce que je n’avais pas quelque chose d’aussi naturel dans mon homelab ?&lt;/p&gt;
&lt;p&gt;Chez moi, Warpgate faisait déjà très bien la partie importante du travail. Il filtrait les accès à mes serveurs, s’intégrait à Authentik pour le SSO (Single Sign-On) et me permettait de gérer précisément qui pouvait atteindre quoi. Je ne cherchais pas un énième jump host ni une énorme plateforme. Je voulais quelque chose de simple, avec juste assez de contrôle pour mon usage.&lt;/p&gt;
&lt;p&gt;Le petit caillou dans la chaussure se trouvait sur mon Mac.&lt;/p&gt;
&lt;h2 id="le-script-qui-fonctionnait-quand-je-pensais-à-le-lancer"&gt;Le script qui fonctionnait… quand je pensais à le lancer
&lt;/h2&gt;&lt;p&gt;J’avais écrit un script capable d’interroger Warpgate et de générer les alias nécessaires dans ma configuration SSH. Une fois exécuté, le résultat était exactement celui que je voulais :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh dmz-nextcloud-01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;La même cible fonctionnait aussi dans &lt;code&gt;scp&lt;/code&gt;, &lt;code&gt;rsync&lt;/code&gt;, Ansible et les autres outils qui savent parler à OpenSSH. Sur le papier, le problème était réglé.&lt;/p&gt;
&lt;p&gt;Dans la vraie vie, il restait une étape assez importante : il fallait penser à lancer le script.&lt;/p&gt;
&lt;p&gt;Quand une machine apparaissait dans Warpgate, elle n’apparaissait pas magiquement sur mon poste. Quand les accès évoluaient, mon fichier local ne suivait pas tout seul. J’avais automatisé la génération, mais pas le moment où elle devait se produire. C’était pas ouf.&lt;/p&gt;
&lt;p&gt;C’est cette petite friction, répétée suffisamment souvent, qui a déclenché le projet. Je ne voulais plus d’une commande supplémentaire à retenir après chaque changement d’infrastructure. Je voulais que Warpgate reste la source de vérité et que mon Mac suive naturellement.&lt;/p&gt;
&lt;h2 id="teleport-ma-donné-lidée-warpgate-lui-a-donné-sa-forme"&gt;Teleport m’a donné l’idée, Warpgate lui a donné sa forme
&lt;/h2&gt;&lt;p&gt;Mon expérience de Teleport au travail m’a montré à quel point une CLI capable d’interagir directement avec le serveur pouvait rendre les accès beaucoup plus fluides. Je ne voulais pourtant pas reproduire Teleport fonctionnalité par fonctionnalité. Le contexte n’était pas le même et mon besoin était beaucoup plus modeste.&lt;/p&gt;
&lt;p&gt;Pour mon homelab, j’avais choisi &lt;a class="link" href="https://github.com/warp-tech/warpgate" target="_blank" rel="noopener"
&gt;Warpgate&lt;/a&gt; plutôt que Teleport Community Edition. La première raison était simple : Warpgate est entièrement open source. Il s’intégrait également très bien avec mon Authentik existant, sans m’obliger à revoir toute mon architecture d’authentification.&lt;/p&gt;
&lt;p&gt;Surtout, son périmètre correspondait à ce que je cherchais. Je voulais filtrer les connexions, gérer les autorisations avec suffisamment de granularité et ajouter une couche de contrôle devant les serveurs, tout en gardant quelque chose que je pouvais comprendre et exploiter tranquillement pendant mon temps libre.&lt;/p&gt;
&lt;p&gt;Petit &lt;em&gt;shout-out&lt;/em&gt;, donc, au projet Warpgate et à ses mainteneurs. WarpgateSH existe parce que leur outil m’a donné une base suffisamment agréable pour avoir envie de prolonger l’expérience jusque sur le poste utilisateur.&lt;/p&gt;
&lt;h2 id="dabord-une-idée-puis-beaucoup-de-questions"&gt;D’abord une idée, puis beaucoup de questions
&lt;/h2&gt;&lt;p&gt;Au début, j’imaginais presque un petit utilitaire : récupérer les cibles autorisées, écrire un fichier SSH et passer à autre chose. Évidemment, le projet a commencé à poser des questions dès que j’ai essayé de lui donner une forme sérieuse.&lt;/p&gt;
&lt;p&gt;Est-ce que la CLI devait être le produit principal ou seulement un complément à une application macOS ? Fallait-il gérer plusieurs instances Warpgate ? Que faire si une synchronisation échouait ? Comment conserver des alias agréables sans écraser une entrée déjà présente dans mon fichier SSH ? Et comment éviter de concevoir quelque chose de tellement lié à mon homelab que personne d’autre ne pourrait l’utiliser ?&lt;/p&gt;
&lt;p&gt;Je n’avais pas de calendrier à tenir. Le vrai défi était là : prendre le temps de réfléchir à l’architecture avant de transformer mon premier script en un second script, simplement plus joli.&lt;/p&gt;
&lt;p&gt;Une décision s’est imposée assez vite. La CLI resterait l’interface principale, et l’application graphique viendrait l’accompagner. Même avec toutes les fenêtres du monde fermées, je devais toujours pouvoir taper :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh une-cible
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;OpenSSH continuerait donc d’établir les connexions. WarpgateSH se contenterait de maintenir les informations locales à jour en arrière-plan. Cette séparation correspondait bien à mon objectif : améliorer mon quotidien sans remplacer les outils qui fonctionnaient déjà.&lt;/p&gt;
&lt;h2 id="rust-sur-macos-avec-linux-dans-un-coin-de-la-tête"&gt;Rust sur macOS, avec Linux dans un coin de la tête
&lt;/h2&gt;&lt;p&gt;Le choix de la technologie a été l’autre gros morceau de réflexion. J’ai une petite affinité avec Rust, et ce projet lui allait plutôt bien : une CLI, un agent qui tourne en arrière-plan, des fichiers à manipuler proprement et l’envie d’obtenir un binaire solide.&lt;/p&gt;
&lt;p&gt;Rust me donnait aussi une base portable. Je voulais commencer sur macOS, parce que c’est mon poste quotidien et le meilleur endroit pour éprouver réellement l’outil. Mais j’affectionne également Linux et je ne voulais pas découvrir dans six mois que toute l’architecture dépendait d’un détail impossible à transposer.&lt;/p&gt;
&lt;p&gt;Le cœur, la CLI et l’agent sont donc écrits en Rust. Pour le compagnon graphique, j’ai choisi Tauri avec React. Cela me permet de construire une application légère pour macOS sans fermer la porte à une version Linux plus tard.&lt;/p&gt;
&lt;p&gt;Il y a eu des tâtonnements, bien sûr. Le nom provisoire &lt;code&gt;warpctl&lt;/code&gt; était déjà utilisé ailleurs. &lt;code&gt;wsh&lt;/code&gt;, que je trouvais pratique, entrait lui aussi en collision avec d’autres outils. Le projet a fini par devenir &lt;strong&gt;WarpgateSH&lt;/strong&gt;, avec la commande &lt;code&gt;warpgatesh&lt;/code&gt;. Un peu plus long à taper, mais beaucoup plus simple à retrouver sur GitHub — et nettement moins susceptible de marcher sur les plates-bandes du voisin.&lt;/p&gt;
&lt;h2 id="le-moment-où-le-prototype-est-devenu-réel"&gt;Le moment où le prototype est devenu réel
&lt;/h2&gt;&lt;p&gt;Le vrai déclic n’est pas arrivé pendant une discussion d’architecture ni devant une suite de tests verte.&lt;/p&gt;
&lt;p&gt;Il est arrivé lorsque j’ai créé un jeton personnel depuis l’interface de Warpgate, que je l’ai donné au prototype et que la liste de mes serveurs est apparue. Les 41 cibles associées à mon compte étaient là, récupérées directement depuis Warpgate, sans utiliser le compte administrateur et sans relancer mon ancien script.&lt;/p&gt;
&lt;p&gt;À ce moment-là, l’idée avait enfin dépassé le stade du README.&lt;/p&gt;
&lt;p&gt;J’ai pu lancer une connexion avec la CLI :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warpgatesh dmz-nextcloud-01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Puis avec OpenSSH, exactement comme avant :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh dmz-nextcloud-01
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;C’était le résultat que je cherchais depuis le début. Warpgate gérait les accès, l’agent synchronisait les changements et mes habitudes dans le terminal ne bougeaient pas d’un millimètre.&lt;/p&gt;
&lt;p&gt;Le compagnon macOS a ajouté le dernier morceau visible. Je pouvais voir l’état de l’agent, rechercher une cible, lancer une connexion et observer les actualisations sans revenir au terminal. Voir la liste évoluer dans l’interface pendant que l’agent travaillait en arrière-plan a rendu le projet beaucoup plus concret. Ce n’était plus seulement un outil que je développais : c’était déjà quelque chose que je pouvais utiliser.&lt;/p&gt;
&lt;h2 id="le-résultat-pour-linstant"&gt;Le résultat, pour l’instant
&lt;/h2&gt;&lt;p&gt;WarpgateSH n’est pas encore une application grand public parfaitement emballée. La distribution macOS, la signature pour les autres utilisateurs et les derniers détails du compagnon demandent encore du travail. Linux viendra ensuite, après une vraie période d’utilisation quotidienne sur mon Mac.&lt;/p&gt;
&lt;p&gt;Mais la promesse initiale est déjà tenue : je n’ai plus besoin de lancer manuellement mon script pour retrouver les accès Warpgate sur mon poste. Je peux utiliser la CLI quand j’en ai envie, rester avec OpenSSH quand je préfère, et consulter l’application lorsque j’ai besoin d’une vue plus visuelle.&lt;/p&gt;
&lt;p&gt;Le plus amusant, c’est que tout est parti d’une tâche qui prenait seulement quelques secondes. Lancer un script n’était pas un drame. C’était simplement le genre de petite corvée qui finit par rappeler, à chaque répétition, qu’elle ne devrait probablement plus exister.&lt;/p&gt;
&lt;p&gt;Le projet est public et disponible ici : &lt;strong&gt;&lt;a class="link" href="https://github.com/M0okz/warpgatesh" target="_blank" rel="noopener"
&gt;github.com/M0okz/warpgatesh&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Si WarpgateSH peut éviter à d’autres utilisateurs de maintenir leurs alias SSH à la main, le petit agacement de départ aura finalement servi à quelque chose.&lt;/p&gt;
&lt;p&gt;À l’occasion, j’essaierai de vous faire un petit billet sur mon « flow » complet : du déploiement d’une ressource dans mon homelab jusqu’à la première connexion au serveur.&lt;/p&gt;</description></item><item><title>Le pipeline qui publie ce blog sans toucher à la prod</title><link>https://blog.homeblack.fr/p/deploiement-automatique-hugo-gitlab-cicd-warpgate/</link><pubDate>Sat, 25 Apr 2026 23:00:00 +0200</pubDate><guid>https://blog.homeblack.fr/p/deploiement-automatique-hugo-gitlab-cicd-warpgate/</guid><description>&lt;img src="https://blog.homeblack.fr/p/deploiement-automatique-hugo-gitlab-cicd-warpgate/cover.png" alt="Featured image of post Le pipeline qui publie ce blog sans toucher à la prod" /&gt;&lt;p&gt;Au début, publier ce blog tenait dans une commande &lt;code&gt;rsync&lt;/code&gt; lancée depuis mon Mac. Ce n’était pas compliqué. C’était même suffisamment simple pour que je repousse l’opération, puis que j’oublie quel article était réellement en ligne.&lt;/p&gt;
&lt;p&gt;Le dépôt avançait, le serveur attendait et la production finissait parfois avec un temps de retard sur ce que j’avais sous les yeux.&lt;/p&gt;
&lt;p&gt;J’ai donc écrit un premier pipeline GitLab CI/CD avec deux étapes : construire le site, puis copier &lt;code&gt;public/&lt;/code&gt; sur &lt;code&gt;dmz-hugo-01&lt;/code&gt;. Quarante lignes de YAML, une clé SSH et, pensais-je, une petite soirée tranquille.&lt;/p&gt;
&lt;p&gt;Le pipeline a fonctionné. Après plusieurs petites leçons.&lt;/p&gt;
&lt;h2 id="warpgate-avait-un-deux-points-de-trop-pour-rsync"&gt;Warpgate avait un deux-points de trop pour rsync
&lt;/h2&gt;&lt;p&gt;Le serveur Hugo n’est pas directement accessible depuis le runner. Le déploiement passe par &lt;a class="link" href="https://github.com/warp-tech/warpgate" target="_blank" rel="noopener"
&gt;Warpgate&lt;/a&gt;, qui encode la cible SSH dans le nom d’utilisateur :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;compte:serveur-cible
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;C’est parfaitement compréhensible pour OpenSSH. &lt;code&gt;rsync&lt;/code&gt;, lui, voit le premier &lt;code&gt;:&lt;/code&gt; et pense avoir trouvé la séparation entre un hôte et un chemin distant.&lt;/p&gt;
&lt;p&gt;Ma première destination ressemblait à ceci :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;compte:dmz-hugo-01@warpgate:/var/www/blog.homeblack.fr/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Le résultat était surtout une erreur difficile à relier au véritable problème. La solution a été de sortir le nom d’utilisateur de la destination et de le passer explicitement à SSH avec &lt;code&gt;-l&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -rlptDz --delete-delay &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -e &lt;span class="s2"&gt;&amp;#34;ssh -i /tmp/hugo_deploy_key -p &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WARPGATE_PORT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -l &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_USER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; public/ &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WARPGATE_HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;La destination redevient un banal &lt;code&gt;hôte:chemin&lt;/code&gt;, tandis que SSH reçoit toujours l’identité Warpgate complète.&lt;/p&gt;
&lt;p&gt;J’utilisais initialement mon propre compte pour cette connexion. Le pipeline possède maintenant un compte dédié, &lt;code&gt;gitlab-ci-hugo&lt;/code&gt;, limité à cette cible. C’est plus lisible dans les journaux et le déploiement ne dépend plus de mon identité personnelle.&lt;/p&gt;
&lt;h2 id="la-clé-ssh-nétait-pas-le-problème-que-je-croyais"&gt;La clé SSH n’était pas le problème que je croyais
&lt;/h2&gt;&lt;p&gt;Ensuite est arrivé le classique :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Load key &amp;#34;/tmp/hugo_deploy_key&amp;#34;: error in libcrypto
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;J’ai d’abord accusé le format de la clé. L’historique du dépôt raconte une histoire moins élégante : variable GitLab de type fichier, retours chariot, essais de copie, différences entre Alpine et Debian… J’ai corrigé plusieurs symptômes avant de stabiliser le chemin complet.&lt;/p&gt;
&lt;p&gt;Le job de déploiement utilise désormais Debian Bookworm et transporte la clé sous forme encodée en base64. Elle est décodée dans un fichier temporaire avec des permissions strictes :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_DEPLOY_KEY&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; base64 -d &amp;gt; /tmp/hugo_deploy_key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; /tmp/hugo_deploy_key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ce n’est pas un mécanisme de chiffrement : la confidentialité dépend toujours de la variable CI stockée dans GitLab. Le base64 évite simplement qu’un copier-coller ou un retour à la ligne transforme la clé avant qu’OpenSSH puisse la lire.&lt;/p&gt;
&lt;h2 id="deux-étapes-ne-suffisaient-plus"&gt;Deux étapes ne suffisaient plus
&lt;/h2&gt;&lt;p&gt;Le premier pipeline savait construire et déployer. Il ne savait pas dire qu’un article avait une description trop longue, une couverture absente ou un lien local cassé. Il pouvait donc automatiser très efficacement la publication d’une erreur.&lt;/p&gt;
&lt;p&gt;Le pipeline actuel possède trois étapes :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;stages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;validate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;deploy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;La validation exécute les tests du dépôt et le validateur de contenu :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;validate-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;validate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;python:3.14-alpine&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;python3 -m unittest discover -s scripts -p &amp;#34;test_*.py&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;python3 scripts/validate_content.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ce contrôle vérifie notamment le front matter, les titres SEO, les descriptions, les couvertures, les liens locaux et quelques motifs sensibles. Il impose aussi le format éditorial choisi pour chaque article : carnet de maintenance, retour d’expérience ou projet.&lt;/p&gt;
&lt;p&gt;Le build utilise ensuite une version précise de Hugo, active &lt;code&gt;--panicOnWarning&lt;/code&gt; et conserve &lt;code&gt;public/&lt;/code&gt; comme artefact pendant deux heures. Le job de déploiement récupère cet artefact : il ne reconstruit pas discrètement un site différent de celui qui vient d’être validé.&lt;/p&gt;
&lt;h2 id="une-branche-construit-seule-main-déploie"&gt;Une branche construit, seule main déploie
&lt;/h2&gt;&lt;p&gt;Le changement le plus important n’est pas une commande. C’est la frontière entre vérifier et publier.&lt;/p&gt;
&lt;p&gt;Une branche ou une merge request lance la validation et le build. Cela suffit pour savoir que l’article est publiable, mais aucun fichier n’est envoyé au serveur. Le job &lt;code&gt;deploy&lt;/code&gt; possède sa propre règle : il ne s’exécute que sur la branche principale.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interruptible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resource_group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;production&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;resource_group&lt;/code&gt; empêche deux déploiements de production de se marcher dessus. Le job n’est pas interruptible afin qu’un nouveau commit ne coupe pas un &lt;code&gt;rsync&lt;/code&gt; déjà engagé. À l’inverse, les validations et builds ordinaires peuvent être annulés lorsqu’un commit plus récent les rend inutiles.&lt;/p&gt;
&lt;p&gt;Les règles globales évitent également de créer deux pipelines identiques lorsqu’une branche possède déjà une merge request ouverte. Ce sont de petits garde-fous, mais ils ont fait passer la CI d’un script distant à un véritable chemin de publication.&lt;/p&gt;
&lt;h2 id="le-dernier-contrôle-se-fait-sur-le-vrai-site"&gt;Le dernier contrôle se fait sur le vrai site
&lt;/h2&gt;&lt;p&gt;Le transfert conserve les permissions utiles, retarde les suppressions avec &lt;code&gt;--delete-delay&lt;/code&gt; et active &lt;code&gt;StrictHostKeyChecking&lt;/code&gt; contre le fichier &lt;code&gt;known_hosts&lt;/code&gt; préparé par le job.&lt;/p&gt;
&lt;p&gt;Une fois &lt;code&gt;rsync&lt;/code&gt; terminé, le pipeline interroge directement l’URL de production :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --fail --silent --show-error --retry &lt;span class="m"&gt;5&lt;/span&gt; --retry-delay &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://blog.homeblack.fr/&amp;#34;&lt;/span&gt; &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ce test ne prouve pas que chaque paragraphe est magnifique. Il confirme au moins que le chemin GitLab → artefact Hugo → Warpgate → Nginx aboutit encore à un site qui répond.&lt;/p&gt;
&lt;h2 id="ce-que-cela-a-changé-quand-jécris"&gt;Ce que cela a changé quand j’écris
&lt;/h2&gt;&lt;p&gt;Aujourd’hui, je ne « déploie » plus le blog depuis mon poste. Je pousse une branche, la CI contrôle le contenu et construit le site. Quand la modification rejoint &lt;code&gt;main&lt;/code&gt;, le même pipeline publie l’artefact validé puis vérifie la page réelle.&lt;/p&gt;
&lt;p&gt;Le système reste volontairement simple. &lt;code&gt;rsync&lt;/code&gt; écrit directement dans le répertoire servi par Nginx : je n’ai ni répertoire de releases ni bascule atomique vers une version précédente. Si un article doit être retiré, le rollback passe encore par Git et un nouveau pipeline.&lt;/p&gt;
&lt;p&gt;Pour un blog personnel, ce compromis me convient. Le point important n’était pas d’empiler les outils, mais d’enlever une commande manuelle tout en gardant une limite nette : tester partout, déployer seulement après la fusion.&lt;/p&gt;
&lt;p&gt;Je pensais automatiser un &lt;code&gt;rsync&lt;/code&gt;. J’ai surtout fini par définir ce que « prêt à publier » voulait dire.&lt;/p&gt;</description></item><item><title>J’ai doublé mon firewall pour pouvoir en éteindre un</title><link>https://blog.homeblack.fr/p/opnsense-ha-carp-fw-core-p1-fw-core-p2/</link><pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.homeblack.fr/p/opnsense-ha-carp-fw-core-p1-fw-core-p2/</guid><description>&lt;img src="https://blog.homeblack.fr/p/opnsense-ha-carp-fw-core-p1-fw-core-p2/cover.png" alt="Featured image of post J’ai doublé mon firewall pour pouvoir en éteindre un" /&gt;&lt;p&gt;Mon premier OPNsense fonctionnait très bien. C’était justement le problème.&lt;/p&gt;
&lt;p&gt;Tout le cœur du réseau reposait sur &lt;code&gt;fw-core-p1&lt;/code&gt; : le LAN, huit VLANs, l’accès Internet et les routes entre mes différentes zones. Tant que la VM tournait, je n’avais aucune raison d’y penser. Dès qu’une mise à jour demandait un redémarrage, la question revenait : qu’est-ce qui reste accessible si ce firewall s’arrête ?&lt;/p&gt;
&lt;p&gt;Pas grand-chose.&lt;/p&gt;
&lt;p&gt;Je ne cherchais pas à transformer mon homelab en infrastructure bancaire. Je voulais simplement pouvoir maintenir un firewall sans couper volontairement tout ce qui passait derrière lui. C’est comme ça que &lt;code&gt;fw-core-p2&lt;/code&gt; est arrivé.&lt;/p&gt;
&lt;h2 id="une-seconde-vm-ne-fait-pas-encore-un-cluster"&gt;Une seconde VM ne fait pas encore un cluster
&lt;/h2&gt;&lt;p&gt;Le plus facile aurait été de cloner &lt;code&gt;fw-core-p1&lt;/code&gt;, de démarrer la copie et d’appeler ça de la haute disponibilité. Deux firewalls mal coordonnés savent surtout produire deux fois plus de problèmes.&lt;/p&gt;
&lt;p&gt;J’ai donc commencé par leur donner la même forme. Les deux VM vivent sur deux nœuds Proxmox différents et possèdent exactement le même ordre d’interfaces : le WAN, le LAN, les huit VLANs du homelab, puis une dernière interface réservée à la synchronisation.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pve-forum pve-fuji
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fw-core-p1 fw-core-p2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MASTER ─── VLAN 999 SYNC ─── BACKUP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ pfsync │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ XML-RPC │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └────── 9 VIP CARP partagées ──┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Cette symétrie n’est pas seulement jolie dans Terraform. &lt;code&gt;pfsync&lt;/code&gt; associe les états aux interfaces : si leur ordre ou leur nom diffère entre les deux machines, la réplication ne raconte plus la même histoire des deux côtés.&lt;/p&gt;
&lt;h2 id="carp-pfsync-et-xml-rpc-ne-font-pas-le-même-travail"&gt;CARP, pfsync et XML-RPC ne font pas le même travail
&lt;/h2&gt;&lt;p&gt;J’avais tendance à ranger tout cela dans la boîte « HA ». En pratique, trois mécanismes distincts coopèrent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CARP&lt;/strong&gt; (&lt;em&gt;Common Address Redundancy Protocol&lt;/em&gt;) détermine quel firewall porte les adresses IP virtuelles. Les machines du réseau utilisent ces VIP comme passerelles et n’ont pas besoin de savoir si &lt;code&gt;p1&lt;/code&gt; ou &lt;code&gt;p2&lt;/code&gt; répond derrière.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pfsync&lt;/strong&gt; réplique la table d’états. Quand &lt;code&gt;p2&lt;/code&gt; reprend le trafic, il connaît déjà les connexions suivies par &lt;code&gt;p1&lt;/code&gt; au lieu de repartir avec une feuille blanche.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;XML-RPC&lt;/strong&gt; synchronise la configuration depuis le primaire : règles, alias, NAT et objets partagés. Le second nœud reste ainsi un vrai pair, pas une vieille copie que j’espère encore correcte six mois plus tard.&lt;/p&gt;
&lt;p&gt;La &lt;a class="link" href="https://docs.opnsense.org/manual/how-tos/carp.html" target="_blank" rel="noopener"
&gt;documentation HA d’OPNsense&lt;/a&gt; recommande justement une interface dédiée à &lt;code&gt;pfsync&lt;/code&gt;. J’ai appliqué ce principe avec le VLAN 999, utilisé également pour la synchronisation de configuration.&lt;/p&gt;
&lt;p&gt;Dans Terraform, ce choix tient dans quelques lignes :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# vtnet10 -&amp;gt; SYNC (pfsync + xmlrpc HA)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;network_device&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; bridge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;fw_core_lan_bridge&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; vlan_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;fw_core_sync_vlan&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;virtio&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ce petit &lt;code&gt;vtnet10&lt;/code&gt; est pourtant l’une des pièces les plus importantes du montage. Le trafic utilisateur et les échanges qui permettent au cluster de rester cohérent ne se disputent pas la même interface logique.&lt;/p&gt;
&lt;h2 id="le-moment-où-neuf-vip-ont-changé-de-côté"&gt;Le moment où neuf VIP ont changé de côté
&lt;/h2&gt;&lt;p&gt;Une interface verte dans OPNsense ne prouve pas grand-chose. Le cluster est devenu réel le jour où j’ai placé &lt;code&gt;fw-core-p1&lt;/code&gt; en maintenance et regardé &lt;code&gt;fw-core-p2&lt;/code&gt; reprendre les neuf VIP.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fw-core-p1 : BACKUP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fw-core-p2 : MASTER
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;VIP CARP : 9/9 sur p2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Le DNS continuait de répondre et mes routes applicatives restaient accessibles. En quittant le mode maintenance, &lt;code&gt;p1&lt;/code&gt; a repris son rôle par préemption et &lt;code&gt;p2&lt;/code&gt; est retourné attendre en secours.&lt;/p&gt;
&lt;p&gt;C’est un test très simple, mais il change complètement la confiance que j’accorde au montage. Avant lui, j’avais deux VM configurées pour faire du CARP. Après lui, j’avais un cluster que je pouvais réellement utiliser pendant une intervention.&lt;/p&gt;
&lt;h2 id="la-haute-disponibilité-synchronise-aussi-les-erreurs"&gt;La haute disponibilité synchronise aussi les erreurs
&lt;/h2&gt;&lt;p&gt;Le piège le plus intéressant est arrivé plus tard. Une VIP utilisait un pair CARP unicast propre à &lt;code&gt;p1&lt;/code&gt;. XML-RPC a parfaitement synchronisé cette valeur vers &lt;code&gt;p2&lt;/code&gt;, qui s’est alors mis à envoyer ses annonces… à sa propre adresse.&lt;/p&gt;
&lt;p&gt;La synchronisation n’était pas cassée. Elle exécutait fidèlement une configuration qui n’aurait pas dû être identique sur les deux nœuds.&lt;/p&gt;
&lt;p&gt;J’ai raconté cette petite surprise dans &lt;a class="link" href="https://blog.homeblack.fr/p/revision-cluster-opnsense/" &gt;ma révision du cluster OPNsense&lt;/a&gt;. Elle m’a rappelé qu’un cluster ne rend pas une configuration correcte par magie. Il propage aussi très efficacement les mauvaises idées.&lt;/p&gt;
&lt;p&gt;Le retour au multicast CARP a supprimé cette exception locale et rendu les deux configurations réellement partageables. Depuis, mon test ne consiste plus seulement à vérifier que les deux firewalls affichent du vert : je provoque la bascule et je contrôle ce qui continue à fonctionner.&lt;/p&gt;
&lt;h2 id="ce-que-ce-second-firewall-a-réellement-changé"&gt;Ce que ce second firewall a réellement changé
&lt;/h2&gt;&lt;p&gt;Je n’ai pas supprimé tous les points de panne. Les deux VM dépendent toujours du réseau physique, des bridges Proxmox et de ce qui se trouve en amont. La haute disponibilité du firewall ne rend pas magiquement le reste du homelab redondant.&lt;/p&gt;
&lt;p&gt;Elle a en revanche retiré un blocage très concret : je peux éteindre un OPNsense sans considérer la coupure comme une étape normale de la maintenance.&lt;/p&gt;
&lt;p&gt;Le bénéfice n’est pas spectaculaire quand tout va bien. &lt;code&gt;fw-core-p2&lt;/code&gt; passe l’essentiel de son temps en BACKUP, ce qui est exactement ce que je lui demande. Sa valeur apparaît quand je mets à jour &lt;code&gt;p1&lt;/code&gt;, quand je redémarre une VM ou quand je veux vérifier une modification sans jouer toute la connectivité du homelab sur un seul clic.&lt;/p&gt;
&lt;p&gt;Au fond, j’ai doublé mon firewall pour pouvoir enfin en éteindre un sereinement. C’était tout le projet.&lt;/p&gt;</description></item></channel></rss>