<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Infrastructure on Mook’s Lab Chronicles</title><link>https://blog.homeblack.fr/categories/infrastructure/</link><description>Recent content in Infrastructure on Mook’s Lab Chronicles</description><generator>Hugo -- gohugo.io</generator><language>fr-FR</language><lastBuildDate>Sat, 19 Sep 2026 16:06:02 +0200</lastBuildDate><atom:link href="https://blog.homeblack.fr/categories/infrastructure/index.xml" rel="self" type="application/rss+xml"/><item><title>Mon bastion Warpgate : des clés SSH au simple alias</title><link>https://blog.homeblack.fr/p/bastion-warpgate-cles-ssh/</link><pubDate>Sat, 19 Sep 2026 16:06:02 +0200</pubDate><guid>https://blog.homeblack.fr/p/bastion-warpgate-cles-ssh/</guid><description>&lt;img src="https://blog.homeblack.fr/p/bastion-warpgate-cles-ssh/cover.png" alt="Featured image of post Mon bastion Warpgate : des clés SSH au simple alias" /&gt;&lt;p&gt;Dans mon terminal, accéder à un serveur tient en une seule ligne :&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;Pas d’adresse IP à chercher, pas de serveur de rebond à saisir, aucune clé personnelle à copier sur la cible. Pourtant, sous la surface, une chaîne complète s&amp;rsquo;active : OpenSSH, Traefik, mon bastion Warpgate, Authentik et un fichier de configuration alimenté par WarpgateSH.&lt;/p&gt;
&lt;p&gt;Dans &lt;a class="link" href="https://blog.homeblack.fr/p/warpgatesh-synchroniser-acces-ssh-warpgate/" &gt;le billet consacré à WarpgateSH&lt;/a&gt;, j’expliquais comment un script lourd à maintenir avait donné naissance à un outil dédié. Il me restait à présenter l&amp;rsquo;autre pan du système : le bastion lui-même, la gestion des clés SSH et son intégration dans mon système d’information — à l’échelle d’un homelab.&lt;/p&gt;
&lt;p&gt;Je vais remonter cette commande étape par étape, depuis mon terminal jusqu’au serveur. Pour en reproduire le principe, je partirais simplement d’une VM et d’une machine de test.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Les exemples s’appuient sur mon infrastructure et le code de WarpgateSH au 19 septembre 2026 ; la version de Warpgate vérifiée le 10 septembre était la 0.27.5. Les valeurs &lt;code&gt;example.net&lt;/code&gt;, &lt;code&gt;alice&lt;/code&gt; et &lt;code&gt;srv-demo&lt;/code&gt; sont fictives et réservées aux démonstrations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="un-bastion-pour-quoi-faire-"&gt;Un bastion, pour quoi faire ?
&lt;/h2&gt;&lt;p&gt;Un bastion sert de point de passage contrôlé vers les ressources à administrer. Au lieu d’autoriser chaque poste à joindre directement chaque serveur, je centralise les flux sur un service unique. Il contrôle les identités, applique les droits et conserve des journaux d’accès.&lt;/p&gt;
&lt;p&gt;Warpgate remplit cette fonction chez moi. Je lui déclare les &lt;strong&gt;cibles&lt;/strong&gt; (les serveurs autorisés) et les &lt;strong&gt;rôles&lt;/strong&gt; (qui a le droit d&amp;rsquo;y accéder), tout en conservant mon client SSH habituel.&lt;/p&gt;
&lt;p&gt;À la différence d&amp;rsquo;un rebond classique via &lt;code&gt;ProxyJump&lt;/code&gt;, la connexion SSH directe depuis mon poste &lt;strong&gt;s’arrête au bastion&lt;/strong&gt;. Warpgate ouvre ensuite sa propre session vers la machine cible avec ses propres identifiants. Je n&amp;rsquo;ai donc jamais besoin d’y transférer mon agent SSH local.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.homeblack.fr/p/bastion-warpgate-cles-ssh/parcours-ssh.png"
width="1600"
height="850"
srcset="https://blog.homeblack.fr/p/bastion-warpgate-cles-ssh/parcours-ssh_hu_9e0bd21345f903d8.png 480w, https://blog.homeblack.fr/p/bastion-warpgate-cles-ssh/parcours-ssh_hu_e7022d0448aa93b1.png 1024w"
loading="lazy"
alt="Deux connexions distinctes : le poste s’authentifie auprès de Warpgate, puis Warpgate utilise sa clé pour joindre le serveur. Authentik valide l’identité humaine."
class="gallery-image"
data-flex-grow="188"
data-flex-basis="451px"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Vue logique : le réseau et le proxy de transport sont volontairement omis pour faire ressortir les deux authentifications.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Cette étanchéité constitue le point clé du montage : &lt;strong&gt;mon identité sur le bastion et le compte Linux de la cible sont deux entités totalement distinctes.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="clé-privée-clé-publique--qui-garde-quoi-"&gt;Clé privée, clé publique : qui garde quoi ?
&lt;/h2&gt;&lt;p&gt;Une paire de clés SSH repose sur deux éléments liés mathématiquement :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;la &lt;strong&gt;clé privée&lt;/strong&gt;, gardée secrète par le client qui prouve son identité ;&lt;/li&gt;
&lt;li&gt;la &lt;strong&gt;clé publique&lt;/strong&gt;, déposée sur le serveur qui vérifie cette identité.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lors de la connexion, le client signe le défi d&amp;rsquo;authentification avec sa clé privée. Le serveur valide cette signature via la clé publique correspondante, sans que la clé privée ne transite sur le réseau.&lt;/p&gt;
&lt;p&gt;Sur un serveur OpenSSH standard, les clés autorisées sont listées dans &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; au niveau du compte utilisateur Linux (&lt;code&gt;alice&lt;/code&gt;, &lt;code&gt;root&lt;/code&gt;, etc.).&lt;/p&gt;
&lt;p&gt;Pour illustrer la génération manuelle, je prends cette commande :&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-keygen -t ed25519 -f ~/.ssh/id_ed25519_bastion -C &lt;span class="s2"&gt;&amp;#34;poste-perso&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;Cette commande produit deux fichiers (décrits dans le &lt;a class="link" href="https://man.openbsd.org/ssh-keygen" target="_blank" rel="noopener"
&gt;manuel &lt;code&gt;ssh-keygen&lt;/code&gt;&lt;/a&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;/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;~/.ssh/id_ed25519_bastion ← Fichier privé local
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~/.ssh/id_ed25519_bastion.pub ← Clé publique à déployer côté serveur
&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;h3 id="chez-moi-les-clés-passent-par-lagent-bitwarden"&gt;Chez moi, les clés passent par l’agent Bitwarden
&lt;/h3&gt;&lt;p&gt;Je ne définis pas de mot de passe directement sur le fichier de clé : je délègue sa protection au coffre fort de Bitwarden et son utilisation à son &lt;strong&gt;agent SSH&lt;/strong&gt;. OpenSSH sollicite l’agent pour signer l’authentification sans lire de fichier privé sur mon disque. Le &lt;a class="link" href="https://bitwarden.com/help/ssh-agent/" target="_blank" rel="noopener"
&gt;guide SSH Bitwarden&lt;/a&gt; détaille sa mise en œuvre.&lt;/p&gt;
&lt;p&gt;Dans mon inventaire Ansible, la connexion pointe vers le socket de l&amp;rsquo;agent :&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;-o IdentityAgent=~/.bitwarden-ssh-agent.sock
&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;Une précision importante : stocker une clé non chiffrée dans Bitwarden protège sa copie dans le coffre, mais ne sécurise pas le fichier initial resté sur le disque s&amp;rsquo;il n&amp;rsquo;a pas été supprimé.&lt;/p&gt;
&lt;p&gt;Pour accéder au bastion via SSH, j’enregistre &lt;strong&gt;ma clé publique dans mon profil Warpgate&lt;/strong&gt;. Mon agent Bitwarden produit la signature ; le bastion la vérifie avec ma clé publique. Une fois authentifié, Warpgate initie la connexion vers la cible finale en utilisant &lt;strong&gt;sa propre clé privée&lt;/strong&gt;. J’autorise donc la clé publique du bastion dans le fichier &lt;code&gt;authorized_keys&lt;/code&gt; du serveur, et non ma clé personnelle.&lt;/p&gt;
&lt;p&gt;Ce flux par clé sert principalement aux comptes de service. Pour mes sessions d&amp;rsquo;administration courantes, je valide mon identité dans le navigateur via Authentik.&lt;/p&gt;
&lt;h3 id="ed25519-rsa-ecdsa--trois-algorithmes-distincts"&gt;Ed25519, RSA, ECDSA : trois algorithmes distincts
&lt;/h3&gt;&lt;p&gt;Ces appellations désignent des algorithmes utilisés pour les signatures d’authentification SSH :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Ce que j’en retiens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ed25519&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;C’est mon choix pour les systèmes récents et le type de clé déployé par mon socle Ansible.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RSA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Je le réserverais aux besoins de rétrocompatibilité, avec des signatures SHA-2 comme l’expliquent les &lt;a class="link" href="https://www.openssh.com/txt/release-8.8" target="_blank" rel="noopener"
&gt;notes OpenSSH 8.8&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ECDSA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Variante basée sur des courbes elliptiques standardisées, disponible nativement dans OpenSSH.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;ed25519-sk&lt;/code&gt; / &lt;code&gt;ecdsa-sk&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Déclinaisons exploitant une clé de sécurité matérielle (FIDO2/YubiKey).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Pour une nouvelle installation, j’écarte les algorithmes obsolètes comme DSA.&lt;/p&gt;
&lt;h3 id="et-la-clé-du-serveur-"&gt;Et la clé du serveur ?
&lt;/h3&gt;&lt;p&gt;SSH contrôle également l&amp;rsquo;authenticité de l&amp;rsquo;hôte distant pour éviter les interceptions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;authorized_keys&lt;/code&gt; contrôle &lt;strong&gt;qui peut entrer&lt;/strong&gt; ;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;known_hosts&lt;/code&gt; enregistre &lt;strong&gt;les serveurs de confiance&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le poste client valide l&amp;rsquo;empreinte du bastion, puis le bastion valide celle du serveur cible. À la première connexion, je recommande de comparer l’empreinte proposée avec celle relevée depuis la console de la VM. Pour afficher cette dernière sur la cible, j’utiliserais cette commande :&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;sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
&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;em&gt;Note d&amp;rsquo;usage : ne pas comparer cette empreinte revient à accepter aveuglément l&amp;rsquo;identité du serveur. C&amp;rsquo;est un point d&amp;rsquo;amélioration que je prévois d&amp;rsquo;automatiser entièrement dans mon lab.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="déploiement-dans-le-homelab"&gt;Déploiement dans le homelab
&lt;/h2&gt;&lt;p&gt;Mon bastion tourne sur une VM Proxmox dédiée (&lt;code&gt;dmz-bastion-01&lt;/code&gt;), provisionnée via Terraform (2 vCPU, 2 Gio de RAM, 30 Gio de stockage).&lt;/p&gt;
&lt;p&gt;Ansible déploie Docker et configure l&amp;rsquo;environnement. Voici un extrait de mon fichier Docker Compose :&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;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&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;services&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;warpgate&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;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;warpgate&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;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&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;ports&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="s2"&gt;&amp;#34;2222:2222&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="s2"&gt;&amp;#34;8888:8888&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="nt"&gt;volumes&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;./data:/data&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;Le port &lt;code&gt;2222&lt;/code&gt; reçoit les connexions SSH et le port &lt;code&gt;8888&lt;/code&gt; héberge l&amp;rsquo;interface d&amp;rsquo;administration HTTPS. Le dossier &lt;code&gt;./data&lt;/code&gt; conserve l&amp;rsquo;état persistant de l&amp;rsquo;application.&lt;/p&gt;
&lt;h2 id="traefik-en-frontal--un-nom-deux-parcours"&gt;Traefik en frontal : un nom, deux parcours
&lt;/h2&gt;&lt;p&gt;Traefik écoute sur &lt;code&gt;bastion.int.homeblack.fr&lt;/code&gt; et redirige les requêtes vers le bastion :&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;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&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;Portail HTTPS (SSO)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Navigateur → bastion.int.homeblack.fr:443
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Traefik → Warpgate:8888 (HTTPS)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Session SSH
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OpenSSH → bastion.int.homeblack.fr:2222
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Traefik (Relais TCP) → Warpgate:2222
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Serveur cible:22
&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;strong&gt;Traefik achemine le trafic vers Warpgate ; Warpgate gère les accès aux cibles.&lt;/strong&gt; Traefik ignore tout des machines finales et se contente de relayer les flux.&lt;/p&gt;
&lt;h3 id="reverse-proxy-https-pour-le-portail"&gt;Reverse proxy HTTPS pour le portail
&lt;/h3&gt;&lt;p&gt;Le portail web passe par un reverse proxy standard. L&amp;rsquo;inventaire Ansible définit le routage :&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-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&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;bastion&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="nt"&gt;host&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;bastion.int.homeblack.fr&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="nt"&gt;upstream_url&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;https://10.60.0.17:8888&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;Les liens d’approbation et le retour Authentik passent par le navigateur, via l’accès HTTPS du portail.&lt;/p&gt;
&lt;h3 id="relais-tcp-dédié-pour-le-flux-ssh"&gt;Relais TCP dédié pour le flux SSH
&lt;/h3&gt;&lt;p&gt;SSH ne traitant pas le protocole HTTP, Traefik utilise un &lt;strong&gt;routeur TCP&lt;/strong&gt;. La configuration statique déclare d&amp;rsquo;abord le point d&amp;rsquo;entrée :&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-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;entryPoints&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;warpgate-ssh&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;address&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;:2222&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;La configuration dynamique (chargée via le provider &lt;code&gt;file&lt;/code&gt;) définit ensuite le routage :&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;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&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;tcp&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;routers&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;warpgate-ssh&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;entryPoints&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;warpgate-ssh&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;rule&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;HostSNI(`*`)&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="nt"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;warpgate-ssh&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&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;services&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;warpgate-ssh&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;loadBalancer&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;proxyProtocol&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;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&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;servers&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;address&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;10.60.0.17:2222&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;La règle &lt;code&gt;HostSNI(`*`)&lt;/code&gt; accepte tous les flux TCP bruts sans interception TLS (détails dans la &lt;a class="link" href="https://doc.traefik.io/traefik/reference/routing-configuration/tcp/routing/rules-and-priority/" target="_blank" rel="noopener"
&gt;doc Traefik TCP&lt;/a&gt;). Le chiffrement SSH reste géré de bout en bout entre le client et Warpgate.&lt;/p&gt;
&lt;h3 id="conservation-des-ip-sources-avec-proxy-protocol"&gt;Conservation des IP sources avec PROXY Protocol
&lt;/h3&gt;&lt;p&gt;Lorsqu&amp;rsquo;un proxy transfère une connexion, le serveur final ne voit que l&amp;rsquo;IP du proxy.&lt;/p&gt;
&lt;p&gt;L’activation du &lt;strong&gt;PROXY protocol v2&lt;/strong&gt; permet à Traefik de transmettre l’IP d’origine du client dans un en-tête ajouté au début du flux TCP. Côté Warpgate (&lt;code&gt;ansible/inventory/group_vars/bastion.yml&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;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;warpgate_ssh_external_host&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;bastion.int.homeblack.fr&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="nt"&gt;warpgate_ssh_external_port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2222&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="nt"&gt;warpgate_ssh_proxy_protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="nt"&gt;warpgate_ssh_allowed_sources&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="s2"&gt;&amp;#34;10.40.0.28/32&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# IP de Traefik autorisée&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;h3 id="segmentation-réseau"&gt;Segmentation réseau
&lt;/h3&gt;&lt;p&gt;Dans mon architecture, Traefik réside sur le réseau &lt;code&gt;TRUST&lt;/code&gt; et Warpgate sur le réseau &lt;code&gt;DMZ&lt;/code&gt;. Le flux traverse le pare-feu inter-vlan. Pour une nouvelle installation, je privilégierais un reverse proxy dans le même segment réseau que le service, afin d’éviter ce passage entre zones.&lt;/p&gt;
&lt;p&gt;J’intègre aussi le filtrage des accès directs depuis les postes à la mise en place du bastion, avec l’objectif de faire passer les sessions d’administration par Warpgate.&lt;/p&gt;
&lt;h2 id="comment-les-serveurs-valident-le-bastion"&gt;Comment les serveurs valident le bastion
&lt;/h2&gt;&lt;p&gt;Une fois authentifié sur Warpgate, ce dernier initie la connexion SSH vers le serveur cible. La cible doit donc connaître la clé publique du bastion.&lt;/p&gt;
&lt;p&gt;Mon rôle Ansible &lt;code&gt;ssh-keys&lt;/code&gt; déploie automatiquement la clé publique du bastion (&lt;code&gt;warpgate-target&lt;/code&gt;) dans le fichier &lt;code&gt;authorized_keys&lt;/code&gt; du compte système distant :&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy authorized_keys&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;ansible.posix.authorized_key&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;user&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;{{ ssh_user }}&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="nt"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;present&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;key&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;{{ item }}&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="nt"&gt;loop&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;{{ ssh_public_keys }}&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;Côté Warpgate, les paramètres de la cible (IP, port, utilisateur Linux) sont injectés via Terraform.&lt;/p&gt;
&lt;p&gt;Pour me connecter à Nextcloud, la commande sous-jacente générée équivaut à :&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 -p &lt;span class="m"&gt;2222&lt;/span&gt; -l &lt;span class="s1"&gt;&amp;#39;gregory.narcin:dmz-nextcloud-01&amp;#39;&lt;/span&gt; bastion.int.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 paramètre &lt;code&gt;-l&lt;/code&gt; transmet l&amp;rsquo;instruction : « Connecte l&amp;rsquo;utilisateur Warpgate &lt;code&gt;gregory.narcin&lt;/code&gt; à la cible &lt;code&gt;dmz-nextcloud-01&lt;/code&gt; ». Après validation des droits, Warpgate ouvre la session SSH vers la VM avec son propre compte d&amp;rsquo;administration.&lt;/p&gt;
&lt;h2 id="sso-avec-authentik-et-accès-automatisés"&gt;SSO avec Authentik et accès automatisés
&lt;/h2&gt;&lt;p&gt;J’utilise &lt;a class="link" href="https://blog.homeblack.fr/p/authentik-sso-homelab/" &gt;Authentik&lt;/a&gt; comme fournisseur d’identité via &lt;strong&gt;OIDC&lt;/strong&gt;, OpenID Connect, pour l’authentification unique (&lt;strong&gt;SSO&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;Pour une connexion humaine, l’option &lt;strong&gt;In-browser auth&lt;/strong&gt; affiche une URL et un code de validation dans le terminal. J’ouvre le lien, compare le code affiché et approuve la demande après authentification auprès d’Authentik. OpenSSH attend cette validation via la méthode &lt;code&gt;keyboard-interactive&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Pour raccorder Warpgate à Authentik, je configure une application cliente et son URL de redirection, par exemple &lt;code&gt;https://bastion.example.net/@warpgate/api/sso/return&lt;/code&gt;. Je sélectionne ensuite &lt;em&gt;In-browser auth&lt;/em&gt; dans la politique d’authentification SSH de mon compte, comme l’explique le &lt;a class="link" href="https://warpgate.null.page/sso/" target="_blank" rel="noopener"
&gt;guide SSO Warpgate&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Je gère la correspondance des rôles avec Ansible :&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-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;warpgate_oidc_roles_claim&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;groups&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="nt"&gt;warpgate_oidc_role_mappings&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;warpgate-operators&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;ops&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;Les appartenances aux groupes Authentik sont ainsi converties en rôles d’accès dans Warpgate.&lt;/p&gt;
&lt;p&gt;Pour mes automatisations Ansible et mes pipelines CI/CD, je garde des comptes de service avec des clés SSH dédiées, comme &lt;code&gt;svc-ansible&lt;/code&gt; : je ne veux pas qu’un traitement attende une validation dans mon navigateur.&lt;/p&gt;
&lt;h2 id="déclaration-des-cibles-avec-terraform"&gt;Déclaration des cibles avec Terraform
&lt;/h2&gt;&lt;p&gt;Je déclare mes cibles Warpgate avec Terraform pour éviter de les ajouter manuellement dans l’interface :&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;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&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="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;warpgate_target&amp;#34; &amp;#34;ssh&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ssh_targets&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;key&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;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;ssh_options&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ip_address&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;ssh_username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; allow_insecure_algos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;public_key_auth&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;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;Mon parcours de création suit cet ordre : &lt;strong&gt;VM Terraform → Cible Warpgate Terraform → Socle Ansible → Synchronisation du client&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;J’exclus le bastion lui-même de cette liste et conserve un accès d’urgence indépendant pour pouvoir intervenir si Warpgate tombe en panne.&lt;/p&gt;
&lt;h2 id="automatisation-des-alias-avec-warpgatesh"&gt;Automatisation des alias avec WarpgateSH
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/M0okz/warpgatesh" target="_blank" rel="noopener"
&gt;WarpgateSH&lt;/a&gt; interroge l’API de Warpgate pour générer automatiquement la configuration SSH locale et les alias associés.&lt;/p&gt;
&lt;p&gt;Après avoir installé la CLI (&lt;a class="link" href="https://github.com/M0okz/warpgatesh/blob/main/docs/getting-started.md" target="_blank" rel="noopener"
&gt;guide de démarrage&lt;/a&gt;), je configure un profil ; voici les commandes avec les valeurs de démonstration :&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-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warpgatesh profile add lab https://bastion.example.net
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warpgatesh profile default lab
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warpgatesh profile ssh-auth lab in-browser
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warpgatesh agent install
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warpgatesh sync
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warpgatesh ls
&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;L&amp;rsquo;outil ajoute un appel d&amp;rsquo;inclusion dans &lt;code&gt;~/.ssh/config&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;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Include ~/.ssh/warpgatesh/config
&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 fichier généré (&lt;code&gt;~/.ssh/warpgatesh/config&lt;/code&gt;) contient les entrées de ce type :&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;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&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-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Profile lab
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Host srv-demo srv-demo.lab
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HostName &amp;#34;bastion.example.net&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Port 2222
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; User &amp;#34;alice:srv-demo&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; UserKnownHostsFile ~/.ssh/warpgatesh/known_hosts/lab
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; StrictHostKeyChecking yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; PreferredAuthentications keyboard-interactive
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; KbdInteractiveAuthentication yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; PasswordAuthentication no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; PubkeyAuthentication no
&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;Explication des directives :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Host&lt;/code&gt; : définit l&amp;rsquo;alias court et l&amp;rsquo;alias qualifié.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HostName&lt;/code&gt; : pointe vers le bastion.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;User&lt;/code&gt; : passe le sélecteur &lt;code&gt;utilisateur:cible&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PreferredAuthentications&lt;/code&gt; : impose l&amp;rsquo;authentification interactive pour déclencher la validation SSO par navigateur.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je peux ensuite utiliser la commande raccourcie :&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 srv-demo
&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;h2 id="diagnostic-et-retours-dexpérience"&gt;Diagnostic et retours d&amp;rsquo;expérience
&lt;/h2&gt;&lt;p&gt;J’ai rencontré des incohérences entre mes alias SSH : certains forçaient le mode &lt;code&gt;keyboard-interactive&lt;/code&gt;, tandis que d’autres autorisaient aussi les clés publiques.&lt;/p&gt;
&lt;p&gt;Pour comprendre ce qu’OpenSSH utilise réellement, je consulte sa configuration effective avec &lt;code&gt;ssh -G&lt;/code&gt;, sans ouvrir de connexion :&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 -G srv-demo &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;/^(hostname|port|user|preferredauthentications|pubkeyauthentication) /&amp;#39;&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;Je peux ainsi repérer les conflits avec d’éventuelles directives &lt;code&gt;Host *&lt;/code&gt; globales.&lt;/p&gt;
&lt;p&gt;Pour valider un montage similaire, je vérifierais quatre points :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;L’accès aux cibles autorisées.&lt;/li&gt;
&lt;li&gt;Le refus d’accès à une cible non attribuée.&lt;/li&gt;
&lt;li&gt;Le compte Linux obtenu à l’arrivée, avec &lt;code&gt;whoami&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;La présence de la session dans les traces d’audit de Warpgate.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="bilan"&gt;Bilan
&lt;/h2&gt;&lt;p&gt;Avec ce montage, je centralise mes accès SSH sans intervenir sur chaque serveur à l’arrivée ou au départ d’un utilisateur. Je garde mes accès automatisés séparés grâce aux comptes de service dédiés.&lt;/p&gt;
&lt;p&gt;En contrepartie, mon bastion devient un composant critique de l’infrastructure. Je dois sauvegarder régulièrement sa clé privée d’administration et ses données d’audit, et en restreindre strictement les accès. C’est bien ce qui se passe en coulisses, et même un peu plus… mais si je raconte tout ici, ce billet va finir avec un sommaire en trois volumes.&lt;/p&gt;
&lt;p&gt;Selon moi, le plus simple est d’avancer par étapes : je commencerais par valider le flux SSH sur une seule cible, puis j’intégrerais le SSO OIDC avant d’automatiser la gestion des cibles et des alias sur mon poste.&lt;/p&gt;</description></item><item><title>De Beszel à Zabbix : pourquoi j’ai refait toute ma supervision</title><link>https://blog.homeblack.fr/p/de-beszel-a-zabbix/</link><pubDate>Tue, 15 Sep 2026 22:21:47 +0200</pubDate><guid>https://blog.homeblack.fr/p/de-beszel-a-zabbix/</guid><description>&lt;img src="https://blog.homeblack.fr/p/de-beszel-a-zabbix/cover.png" alt="Featured image of post De Beszel à Zabbix : pourquoi j’ai refait toute ma supervision" /&gt;&lt;p&gt;Au départ, je voulais simplement savoir si mes machines allaient bien. Un peu de CPU, de mémoire, de stockage, quelques températures et une alerte quand quelque chose sortait des clous. &lt;a class="link" href="https://github.com/henrygd/beszel" target="_blank" rel="noopener"
&gt;Beszel&lt;/a&gt; répondait très bien à ce besoin.&lt;/p&gt;
&lt;p&gt;Son interface était claire, l’installation rapide et, surtout, il était stable. Je pouvais l’ouvrir, prendre la température du homelab en quelques secondes et repartir faire autre chose. Pour un outil de supervision, cette discrétion est une vraie qualité.&lt;/p&gt;
&lt;p&gt;Puis mon besoin a évolué en même temps que mon lab. Je ne voulais plus seulement savoir si une VM consommait trop de mémoire. Je voulais savoir si le service qui tournait dessus fonctionnait, si un conteneur avait redémarré, si une sauvegarde était récente, si un mail avait réellement effectué tout son trajet ou encore si les DNS répondaient correctement.&lt;/p&gt;
&lt;p&gt;J’ai ajouté Pulse, essayé Checkmk (« bad idea :&amp;rsquo;) »), multiplié les tests et fini par arriver sur Zabbix. En quelques semaines, j’ai refait presque tout mon système de supervision.&lt;/p&gt;
&lt;p&gt;Ce n’est pas l’histoire d’une succession de mauvais outils. C’est plutôt celle d’un besoin qui a grandi vite, porté par la curiosité, jusqu’au moment où maintenir plusieurs réponses partielles est devenu plus pénible que de reprendre le problème depuis le début.&lt;/p&gt;
&lt;h2 id="beszel-faisait-exactement-ce-que-je-lui-demandais"&gt;Beszel faisait exactement ce que je lui demandais
&lt;/h2&gt;&lt;p&gt;J’ai beaucoup aimé Beszel pour sa simplicité et sa légèreté. Il remontait les métriques essentielles de mes machines et de mes conteneurs Docker sans transformer l’installation en projet à part entière. L’interface allait droit au but et je n’avais pas besoin de passer une soirée dans la documentation pour comprendre la mise en place ou l’exploitation.&lt;/p&gt;
&lt;p&gt;Ce serait donc injuste de raconter que je l’ai abandonné parce qu’il était instable ou limité dans l’absolu. Il faisait bien son travail. Dans mon périmètre, il ne tenait plus ma cadence insatiable.&lt;/p&gt;
&lt;p&gt;Une machine peut avoir 20 % de CPU, beaucoup de mémoire disponible et un disque presque vide tout en rendant un service parfaitement inutilisable. Le processus peut être arrêté. Le certificat peut avoir expiré. Le DNS peut répondre sans fournir la bonne donnée. Un serveur mail peut accepter une connexion SMTP sans être capable de livrer un message jusque dans la boîte de réception.&lt;/p&gt;
&lt;p&gt;Je commençais donc à vouloir surveiller ce qui se passait &lt;strong&gt;dans&lt;/strong&gt; les VM, pas seulement autour d’elles.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/rcourtman/pulse" target="_blank" rel="noopener"
&gt;Pulse&lt;/a&gt; apportait de son côté une lecture pertinente de Proxmox et de son environnement. Là encore, le problème n’était pas la qualité des données. Le problème était que je venais d’ajouter un outil supplémentaire à installer, mettre à jour, sauvegarder et comprendre.&lt;/p&gt;
&lt;p&gt;J’avais Beszel pour certaines ressources, Pulse pour une autre partie de l’infrastructure, Grafana pour mes dashboards et plusieurs sondes qui vivaient encore ailleurs. Chaque écran avait une raison d’exister. Leur addition commençait pourtant à raconter une histoire assez différente : pour comprendre un incident, il fallait déjà savoir quel outil ouvrir.&lt;/p&gt;
&lt;p&gt;La friction qui a lancé la suite est venue de là. Je ne cherchais pas à collectionner les dashboards. Je voulais réduire le nombre de systèmes capables de décider qu’il y avait un problème.&lt;/p&gt;
&lt;h2 id="checkmk-ma-montré-ce-qui-me-manquait"&gt;Checkmk m’a montré ce qui me manquait
&lt;/h2&gt;&lt;p&gt;Je n’avais encore jamais vraiment utilisé &lt;a class="link" href="https://checkmk.com/product/checkmk-community" target="_blank" rel="noopener"
&gt;Checkmk&lt;/a&gt;. C’était donc l’occasion de tester une solution plus large (et probablement sur-vendue), capable de découvrir une machine et de transformer automatiquement ce qu’elle expose en services supervisés.&lt;/p&gt;
&lt;p&gt;La première impression a été plutôt bonne. L’agent et les modules fournis de base remontaient beaucoup de choses sans que j’aie à tout construire à la main. CPU, mémoire, disques, interfaces, services systemd, conteneurs Docker : je retrouvais enfin les ressources et les services dans la même plateforme.&lt;/p&gt;
&lt;p&gt;C’est cette profondeur prête à l’emploi qui m’a convaincu : je pouvais partir d’une VM, descendre jusqu’à ses services et ajouter mes propres contrôles lorsque le standard ne suffisait plus.&lt;/p&gt;
&lt;p&gt;J’ai ainsi intégré des sondes DNS, des contrôles Proxmox et plusieurs scénarios mail. Je ne voulais pas seulement vérifier que les ports 25 ou 993 étaient ouverts. Je voulais établir une session chiffrée, m’authentifier, envoyer un message identifié, le retrouver en IMAP, mesurer son délai et nettoyer la boîte après le test.&lt;/p&gt;
&lt;p&gt;À ce moment-là, ma supervision commençait réellement à représenter le fonctionnement du homelab.&lt;/p&gt;
&lt;p&gt;Elle a aussi commencé à faire du bruit. Les interfaces TAP éphémères de Proxmox disparaissaient, les jobs du runner provoquaient des pics de mémoire et certains seuils réagissaient à des variations sans conséquence. J’ai raconté séparément comment j’ai &lt;a class="link" href="https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/" &gt;repris en main ces alertes Checkmk&lt;/a&gt;, car cette étape m’a appris une chose importante : collecter beaucoup d’informations ne suffit pas, il faut encore décider lesquelles méritent de m’interrompre.&lt;/p&gt;
&lt;h2 id="puis-loutil-a-commencé-à-me-fatiguer"&gt;Puis l’outil a commencé à me fatiguer
&lt;/h2&gt;&lt;p&gt;Plus je poussais Checkmk, plus son ergonomie me ralentissait. L’interface me donnait parfois l’impression d’avoir été dessinée par un maçon : tout était solide, mais il fallait connaître le plan du bâtiment pour retrouver la bonne porte.&lt;/p&gt;
&lt;p&gt;La création de dashboards a été le point le plus visible. J’avais déjà Grafana, où je pouvais construire une vue exactement comme je l’imaginais. Dans Checkmk, obtenir une présentation sur mesure me demandait beaucoup plus d’efforts pour un résultat qui me convenait moins.&lt;/p&gt;
&lt;p&gt;La performance a aussi pesé dans la balance. Sur ma VM équipée de quatre vCPU, l’expérience restait plus lourde que ce que j’attendais pour la taille de mon homelab. Les métriques étaient bien conservées, mais leur consultation et leur chargement me semblaient lents. Dans mon usage, les contrôles restaient essentiellement calés sur la cadence standard d’une minute, alors que j’avais déjà une collecte plus fine dans VictoriaMetrics pour alimenter Grafana.&lt;/p&gt;
&lt;p&gt;Une partie de cette différence vient du &lt;a class="link" href="https://docs.checkmk.com/latest/en/cse.html" target="_blank" rel="noopener"
&gt;découpage des éditions&lt;/a&gt;. Checkmk Community est bien entièrement open source, mais elle utilise le cœur Nagios. Le Checkmk Micro Core, plus efficace, ainsi que plusieurs fonctions avancées de graphes et de connexion aux bases de métriques externes appartiennent aux éditions commerciales.&lt;/p&gt;
&lt;p&gt;Le SSO (Single Sign-On) a fini d’illustrer ce décalage. &lt;a class="link" href="https://docs.checkmk.com/latest/en/saml.html" target="_blank" rel="noopener"
&gt;L’intégration SAML native&lt;/a&gt; est elle aussi commerciale. En Community, j’aurais dû ajouter une couche d’authentification Apache ou monter une intégration LDAP avec Authentik uniquement pour Checkmk.&lt;/p&gt;
&lt;p&gt;Ce n’était pas impossible. C’était juste franchement pénible, en 2026, de réserver une simple connexion SSO aux versions payantes. Pour les ACL ou le RBAC, à la limite… mais la connexion ?&lt;/p&gt;
&lt;p&gt;Je n’ai rien contre les logiciels payants. Je préfère même une proposition claire, gratuite ou payante, à une succession de limites qui apparaissent lorsque je commence à exploiter sérieusement l’outil. Avec Checkmk, j’avais choisi Community pour sa promesse open source, mais plusieurs améliorations qui répondaient précisément à mes frustrations me renvoyaient vers une autre édition.&lt;/p&gt;
&lt;p&gt;J’ai donc profité de ce moment pour vérifier si un autre outil correspondait mieux à la direction prise par mon homelab.&lt;/p&gt;
&lt;h2 id="revenir-à-zabbix-mais-pas-par-nostalgie"&gt;Revenir à Zabbix, mais pas par nostalgie
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://www.zabbix.com/" target="_blank" rel="noopener"
&gt;Zabbix&lt;/a&gt; n’était pas une découverte totale. Je l’avais déjà utilisé auparavant pour du pro et je connaissais une partie de ses forces : une grosse communauté, beaucoup de modèles, une architecture éprouvée et une grande liberté pour construire des contrôles sur mesure.&lt;/p&gt;
&lt;p&gt;Son &lt;a class="link" href="https://www.zabbix.com/documentation/current/en/manual/concepts/agent2" target="_blank" rel="noopener"
&gt;Agent 2&lt;/a&gt; a beaucoup compté dans mon choix. Il reprend les contrôles système classiques et ajoute une architecture de plugins pour des composants comme Docker, systemd, SMART ou différentes bases de données. Son empreinte restait raisonnable pour mes machines, surtout face à l’alternative qui consistait à multiplier les agents spécialisés.&lt;/p&gt;
&lt;p&gt;Surtout, Zabbix me laissait plusieurs chemins pour une même donnée : agent actif, requête HTTP, SNMP, élément dépendant, découverte bas niveau, script externe ou &lt;code&gt;UserParameter&lt;/code&gt;. Cette variété peut rendre la prise en main dense, mais elle correspond bien à ma manière de travailler. Si un contrôle n’existe pas, je peux le construire, le versionner et le rattacher au même hôte que le reste de sa supervision.&lt;/p&gt;
&lt;p&gt;La plateforme reste &lt;a class="link" href="https://github.com/zabbix/zabbix/blob/master/COPYING" target="_blank" rel="noopener"
&gt;open source sous licence AGPL&lt;/a&gt;. Je pouvais utiliser le SSO SAML, ajuster la rétention et créer mes modèles sans changer d’édition. Grafana gardait son rôle de cockpit : VictoriaMetrics alimentait les vues d’infrastructure, et Zabbix Agent 2 lui fournissait les métriques des conteneurs sans ajouter cAdvisor.&lt;/p&gt;
&lt;p&gt;Sur le papier, le choix était séduisant. Il restait à prouver que Zabbix pouvait reprendre les contrôles qui rendaient Checkmk réellement utile chez moi.&lt;/p&gt;
&lt;h2 id="une-migration-en-parallèle"&gt;Une migration en parallèle
&lt;/h2&gt;&lt;p&gt;J’ai fait fonctionner Zabbix et Checkmk en parallèle le temps de reprendre les contrôles indispensables : Proxmox, Docker, systemd, OPNsense, DNS et les boucles mail. Une fois la couverture et les notifications vérifiées, Zabbix est devenu la plateforme de référence le 13 août 2026 et les agents Checkmk ont été retirés progressivement. J’ai conservé une sauvegarde protégée de la VM Checkmk pour pouvoir revenir en arrière si nécessaire.&lt;/p&gt;
&lt;h2 id="ce-que-cette-migration-a-réellement-changé"&gt;Ce que cette migration a réellement changé
&lt;/h2&gt;&lt;p&gt;Aujourd’hui, je n’attends pas d’un seul outil qu’il fasse tout.&lt;/p&gt;
&lt;p&gt;Grafana reste mon premier écran lorsque je veux prendre la température du homelab. Ses dashboards sont directs, visuels et construits autour de mes questions.
Zabbix collecte les états, découvre les services et décide quand une anomalie devient un problème. Argus récupère dans Zabbix les versions réellement déployées. Plus récemment, CairnOps est venu ajouter une couche d’hypervision en utilisant Zabbix comme source.&lt;/p&gt;
&lt;p&gt;Je sais désormais où regarder une tendance, où chercher un événement et quel système porte la décision d’alerter. Ajouter un outil reste possible, mais seulement s’il possède un rôle qui n’existe pas déjà ailleurs.&lt;/p&gt;
&lt;p&gt;Beszel m’a rappelé la valeur d’une supervision simple.
Checkmk m’a montré l’intérêt de descendre jusqu’aux services et aux contrôles métier.
Zabbix m’a finalement donné la souplesse nécessaire pour réunir ces besoins sans abandonner les outils spécialisés qui fonctionnaient déjà bien.&lt;/p&gt;
&lt;p&gt;Dans les prochains articles, je ferai le tour de cette architecture actuelle : Grafana comme cockpit, Zabbix derrière les alertes, puis les outils qui gravitent autour, notamment Argus, Uptime Kuma, LogChef et PatchMon. CairnOps aura droit à son propre récit, parce que son arrivée commence déjà à changer ma manière de lire l’état global du homelab.&lt;/p&gt;</description></item><item><title>J’ai enfin activé Hyperscan sur mes deux OPNsense</title><link>https://blog.homeblack.fr/p/hyperscan-suricata-opnsense/</link><pubDate>Mon, 24 Aug 2026 15:20:00 +0200</pubDate><guid>https://blog.homeblack.fr/p/hyperscan-suricata-opnsense/</guid><description>&lt;img src="https://blog.homeblack.fr/p/hyperscan-suricata-opnsense/cover.png" alt="Featured image of post J’ai enfin activé Hyperscan sur mes deux OPNsense" /&gt;&lt;p&gt;Tout est parti d’un article publié sur le blog de Suricata : &lt;a class="link" href="https://suricata.io/2025/10/07/faster-suricata-startups-with-hyperscan-caching/" target="_blank" rel="noopener"
&gt;&lt;em&gt;Faster Suricata startups with Hyperscan caching&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Le sujet était le cache MPM (&lt;em&gt;Multi-Pattern Matching&lt;/em&gt;) introduit avec Suricata 8. Au lieu de recompiler toutes les bases de motifs Hyperscan à chaque démarrage ou rechargement des règles, Suricata peut les conserver sur disque et les réutiliser. L’article annonçait un temps de démarrage réduit jusqu’à 50 %.&lt;/p&gt;
&lt;p&gt;En le lisant, je me suis surtout posé une question beaucoup plus basique : est-ce qu’Hyperscan tournait réellement sur mes firewalls ?&lt;/p&gt;
&lt;p&gt;La réponse était non.&lt;/p&gt;
&lt;p&gt;Suricata fonctionnait déjà sur mes deux OPNsense, les événements remontaient bien dans VictoriaLogs et mon cluster HA faisait son travail. Mais le moteur de recherche de motifs capable de profiter des instructions vectorielles du processeur n’était pas actif.&lt;/p&gt;
&lt;p&gt;Le verrou ne se trouvait pas vraiment dans Suricata. Mes deux VM utilisaient encore le modèle de CPU générique &lt;code&gt;kvm64&lt;/code&gt;, pratique pour la compatibilité, mais trop conservateur pour exposer les instructions que je voulais utiliser. Avant de jouer avec le cache, il fallait donc commencer par activer le moteur lui-même.&lt;/p&gt;
&lt;h2 id="donner-le-bon-cpu-virtuel-à-suricata"&gt;Donner le bon CPU virtuel à Suricata
&lt;/h2&gt;&lt;p&gt;J’ai passé &lt;code&gt;fw-core-p1&lt;/code&gt; et &lt;code&gt;fw-core-p2&lt;/code&gt; en &lt;code&gt;x86-64-v3&lt;/code&gt;. FreeBSD voyait alors correctement AVX2, AES et FMA, et je pouvais enfin demander explicitement Hyperscan à Suricata :&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-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;cpu&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86-64-v3&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;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-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;opnsense_ids_mpm_algo&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;hs&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;Le &lt;code&gt;hs&lt;/code&gt; sélectionne Hyperscan comme moteur MPM. C’est la partie de Suricata qui cherche plusieurs motifs à la fois dans le trafic et écarte rapidement les signatures qui ne peuvent pas correspondre. La &lt;a class="link" href="https://docs.opnsense.org/manual/ips.html" target="_blank" rel="noopener"
&gt;documentation OPNsense&lt;/a&gt; le présente d’ailleurs comme le meilleur choix sur les plateformes compatibles.&lt;/p&gt;
&lt;p&gt;Le changement posait tout de même une question importante : mes hôtes Proxmox mélangent des processeurs Intel et AMD. J’ai donc fait migrer une VM de test d’Intel vers AMD, puis de nouveau vers Intel, avant de considérer &lt;code&gt;x86-64-v3&lt;/code&gt; comme un socle suffisamment portable chez moi.&lt;/p&gt;
&lt;p&gt;Je n’ai pas touché aux règles de placement HA. Les deux firewalls restent volontairement séparés sur deux hôtes différents.&lt;/p&gt;
&lt;h2 id="le-test-qui-comptait-vraiment"&gt;Le test qui comptait vraiment
&lt;/h2&gt;&lt;p&gt;Voir &lt;code&gt;mpm-algo = hs&lt;/code&gt; sur les deux nœuds était une bonne nouvelle, mais cela ne disait encore rien sur le comportement du cluster pendant une vraie bascule.&lt;/p&gt;
&lt;p&gt;J’ai donc laissé &lt;code&gt;p2&lt;/code&gt; reprendre les neuf VIP CARP. Pendant qu’il était MASTER avec Hyperscan actif, la sortie Internet continuait de fonctionner. Je n’ai observé aucune perte de paquets pendant le failover, avec une latence maximale de 13 ms.&lt;/p&gt;
&lt;p&gt;Après le test, la situation normale était revenue : &lt;code&gt;p1&lt;/code&gt; MASTER, &lt;code&gt;p2&lt;/code&gt; BACKUP et neuf VIP au bon endroit. Le moteur de détection avait changé sans perturber le cluster.&lt;/p&gt;
&lt;h2 id="seize-heures-plus-tard"&gt;Seize heures plus tard
&lt;/h2&gt;&lt;p&gt;Le premier contrôle montrait déjà Suricata autour de 356 à 359 Mio de mémoire, contre environ 548 Mio auparavant. J’ai laissé tourner avant de regarder de nouveau les compteurs.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mesure&lt;/th&gt;
&lt;th&gt;Après environ 16 heures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU Suricata&lt;/td&gt;
&lt;td&gt;0 à 0,1 % sur les deux nœuds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mémoire sur P1&lt;/td&gt;
&lt;td&gt;233 Mio, soit −57,5 % par rapport au repère de 548 Mio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mémoire sur P2&lt;/td&gt;
&lt;td&gt;356 Mio, stable depuis le premier contrôle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture sur P1&lt;/td&gt;
&lt;td&gt;51 044 239 paquets, 4 669 drops, soit 0,0091 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture sur P2&lt;/td&gt;
&lt;td&gt;1 078 301 paquets, aucun drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Événements TLS&lt;/td&gt;
&lt;td&gt;22 056 événements centralisés dans VictoriaLogs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Les 4 669 drops de capture observés sur &lt;code&gt;p1&lt;/code&gt; n’avaient pas augmenté depuis plus de deux heures lors du dernier relevé. Aucun drop Suricata n’apparaissait dans les échantillons contrôlés et la qualité de capture restait supérieure à 99,99 %.&lt;/p&gt;
&lt;p&gt;Surtout, l’enrichissement n’avait pas disparu au passage : 926 SNI, 370 empreintes JA4, 55 empreintes de certificats et 23 émetteurs distincts remontaient toujours dans VictoriaLogs.&lt;/p&gt;
&lt;p&gt;Ce n’est pas un benchmark. &lt;code&gt;p1&lt;/code&gt; et &lt;code&gt;p2&lt;/code&gt; n’ont ni le même rôle ni le même volume de trafic, et je ne vais pas attribuer toute la baisse à une seule option. La tendance observée reste néanmoins assez nette : CPU négligeable, mémoire en baisse et aucune dégradation visible de la détection ou du failover.&lt;/p&gt;
&lt;p&gt;La configuration est maintenant décrite dans Terraform et Ansible. Le plan Terraform ne montrait aucun drift, le second passage Ansible terminait avec &lt;code&gt;changed=0&lt;/code&gt;, et les sept tests du rôle passaient avec le &lt;em&gt;syntax-check&lt;/em&gt; et &lt;code&gt;ansible-lint&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Assez ironiquement, un article consacré au cache m’aura d’abord fait découvrir que je n’utilisais même pas le moteur. Le cache pourra venir ensuite. Pour l’instant, Hyperscan tourne enfin sur mes deux firewalls — et c’était déjà le vrai chantier.&lt;/p&gt;</description></item><item><title>Je voulais juste mettre à jour mes OPNsense</title><link>https://blog.homeblack.fr/p/revision-cluster-opnsense/</link><pubDate>Sat, 15 Aug 2026 20:00:00 +0200</pubDate><guid>https://blog.homeblack.fr/p/revision-cluster-opnsense/</guid><description>&lt;img src="https://blog.homeblack.fr/p/revision-cluster-opnsense/cover.png" alt="Featured image of post Je voulais juste mettre à jour mes OPNsense" /&gt;&lt;p&gt;J’étais parti pour une petite révision tranquille de mes deux OPNsense. Une sauvegarde, une mise à jour du premier firewall, quelques contrôles, puis la même chose sur le second.&lt;/p&gt;
&lt;p&gt;Le premier venait de terminer. J’ai lancé la mise à jour du second, puis contrôlé l’état du cluster pendant qu’elle avançait.&lt;/p&gt;
&lt;h2 id="un-cluster-qui-fonctionnait-presque"&gt;Un cluster qui fonctionnait… presque
&lt;/h2&gt;&lt;p&gt;Mon réseau repose sur deux firewalls, &lt;code&gt;fw-core-p1&lt;/code&gt; et &lt;code&gt;fw-core-p2&lt;/code&gt;, avec neuf adresses IP virtuelles CARP. En temps normal, &lt;code&gt;p1&lt;/code&gt; porte les neuf VIP et &lt;code&gt;p2&lt;/code&gt; attend sagement en secours.&lt;/p&gt;
&lt;p&gt;Après la mise à jour du premier nœud, l’état était un peu plus créatif :&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;8 VIP : p1 BACKUP / p2 MASTER
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;VIP LAN : p1 MASTER / p2 MASTER
&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éseau fonctionnait encore. Les interfaces répondaient, pfsync synchronisait toujours les connexions et rien ne semblait cassé à la maison. Mais la VIP du LAN était portée par les deux firewalls en même temps.&lt;/p&gt;
&lt;p&gt;C’était le genre de problème assez discret pour passer inaperçu, jusqu’au jour où il décide de ne plus l’être.&lt;/p&gt;
&lt;p&gt;Je n’ai rien tenté au milieu de l’opération. &lt;code&gt;p1&lt;/code&gt; a repris les neuf VIP avant le redémarrage du second, puis j’ai simplement surveillé le trafic jusqu’à son retour. Quelques minutes plus tard, les deux nœuds étaient bien en OPNsense &lt;code&gt;26.7.2_2&lt;/code&gt; sur FreeBSD 15.1 et tous les services répondaient.&lt;/p&gt;
&lt;p&gt;La mise à jour était terminée. Restait à comprendre ce petit double-MASTER.&lt;/p&gt;
&lt;h2 id="p2-discutait-avec-lui-même"&gt;P2 discutait avec lui-même
&lt;/h2&gt;&lt;p&gt;Le CARP du LAN utilisait l’unicast. Chaque firewall devait envoyer ses annonces directement à l’adresse réelle de l’autre :&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;P1 devait cibler 10.0.0.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;P2 devait cibler 10.0.0.2
&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;Dans la configuration réelle, les deux ciblaient &lt;code&gt;10.0.0.3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Pour &lt;code&gt;p1&lt;/code&gt;, c’était correct. Pour &lt;code&gt;p2&lt;/code&gt;, beaucoup moins : il envoyait ses annonces CARP à sa propre adresse.&lt;/p&gt;
&lt;p&gt;Le plus amusant, c’est que la synchronisation HA faisait exactement ce que je lui demandais. &lt;code&gt;p1&lt;/code&gt; copiait sa configuration vers &lt;code&gt;p2&lt;/code&gt;, y compris ce pair unicast qui aurait justement dû être différent sur chaque machine. Corriger uniquement le second nœud n’aurait servi à rien : la synchronisation suivante aurait remis la mauvaise valeur.&lt;/p&gt;
&lt;p&gt;Mes huit autres VIP fonctionnaient déjà avec le multicast CARP classique. J’ai donc vérifié que les annonces envoyées vers &lt;code&gt;224.0.0.18&lt;/code&gt; traversaient bien le bridge Proxmox et arrivaient sur &lt;code&gt;p2&lt;/code&gt;. C’était le cas.&lt;/p&gt;
&lt;p&gt;J’ai supprimé le pair unicast du LAN et laissé CARP revenir à son fonctionnement normal :&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;P1 ─┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 224.0.0.18
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;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;Cette fois, les deux firewalls pouvaient partager exactement la même configuration sans que l’un d’eux finisse par se parler à lui-même.&lt;/p&gt;
&lt;h2 id="le-petit-test-qui-évite-les-grandes-suppositions"&gt;Le petit test qui évite les grandes suppositions
&lt;/h2&gt;&lt;p&gt;Une fois la correction appliquée, j’ai placé &lt;code&gt;p1&lt;/code&gt; en maintenance. &lt;code&gt;p2&lt;/code&gt; a repris les neuf VIP, le DNS a continué de répondre et mes routes applicatives sont restées accessibles. En quittant le mode maintenance, &lt;code&gt;p1&lt;/code&gt; est redevenu MASTER par préemption et &lt;code&gt;p2&lt;/code&gt; est retourné en BACKUP.&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fw-core-p1 : MASTER 9/9
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fw-core-p2 : BACKUP 9/9
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;demotion : 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pfsync : synchronisé
&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;Au final, la mise à jour vers la &lt;a class="link" href="https://docs.opnsense.org/releases/CE_26.7.html" target="_blank" rel="noopener"
&gt;série OPNsense 26.7&lt;/a&gt; s’est passée sans incident. Elle m’a surtout donné une bonne excuse pour regarder un peu plus loin que le voyant vert.&lt;/p&gt;
&lt;p&gt;Je voulais faire une petite révision. J’ai retiré une adresse dans un champ, relancé un vrai failover et mon cluster est maintenant plus simple qu’avant.&lt;/p&gt;
&lt;p&gt;C’est déjà pas mal pour une matinée de maintenance.&lt;/p&gt;</description></item><item><title>J’allais ajouter un nœud Ceph. Mes backups partaient en même temps</title><link>https://blog.homeblack.fr/p/ceph-backups-pbs/</link><pubDate>Fri, 14 Aug 2026 12:40:31 +0200</pubDate><guid>https://blog.homeblack.fr/p/ceph-backups-pbs/</guid><description>&lt;img src="https://blog.homeblack.fr/p/ceph-backups-pbs/cover.png" alt="Featured image of post J’allais ajouter un nœud Ceph. Mes backups partaient en même temps" /&gt;&lt;p&gt;Toutes les deux heures, mon cluster Ceph avait un petit passage à vide. Les VM devenaient moins réactives, les latences grimpaient et BlueStore finissait parfois par signaler des opérations lentes.&lt;/p&gt;
&lt;p&gt;Ma première question a été assez classique : est-ce que le scheduler mClock était mal adapté à mon petit cluster ? Un retour à WPQ pouvait-il arranger les choses ? Et si le problème venait simplement du fait que trois nœuds et trois OSD ne suffisaient plus, fallait-il ajouter un quatrième PVE ?&lt;/p&gt;
&lt;p&gt;Cette dernière option ne m’enchantait pas. Un nœud supplémentaire aurait apporté de la capacité et réparti la charge, mais aussi un nouveau coût pour corriger un problème que je ne comprenais pas encore.&lt;/p&gt;
&lt;p&gt;J’ai donc commencé par regarder ce qui se passait réellement au moment des ralentissements. Le coupable n’était pas caché dans un obscur réglage Ceph : mes trois sauvegardes Proxmox Backup Server partaient exactement en même temps.&lt;/p&gt;
&lt;h2 id="avec-trois-osd-le-plus-lent-donne-le-tempo"&gt;Avec trois OSD, le plus lent donne le tempo
&lt;/h2&gt;&lt;p&gt;Mon cluster repose sur trois nœuds Proxmox et un OSD NVMe par nœud :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Nœud&lt;/th&gt;
&lt;th&gt;OSD&lt;/th&gt;
&lt;th&gt;SSD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pve-forum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;osd.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WD Black SN770 2 To&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pve-dell&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;osd.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crucial P310 2 To&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pve-fuji&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;osd.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crucial P3 Plus 2 To&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Le pool des VM utilise une réplication par trois. Chaque écriture doit donc être confirmée par les trois OSD. Ce choix me donne la tolérance aux pannes que je veux, mais il transforme naturellement le disque le plus lent en limite commune.&lt;/p&gt;
&lt;p&gt;Le cluster n’était pourtant ni plein ni déséquilibré. Au moment du diagnostic, il était occupé à environ 33 %, ses 129 groupes de placement étaient &lt;code&gt;active+clean&lt;/code&gt; et aucun recovery n’avait eu lieu durant les trente jours observés. Le réseau Ceph en 2,5 Gb/s ne montrait pas non plus d’erreurs ni de pertes.&lt;/p&gt;
&lt;p&gt;En revanche, les latences maximales des OSD racontaient une tout autre histoire :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OSD&lt;/th&gt;
&lt;th style="text-align: right"&gt;Latence commit maximale sur 30 jours&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;osd.0&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;3,2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;osd.1&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;567 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;osd.2&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;9,5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Le Crucial P3 Plus de &lt;code&gt;pve-fuji&lt;/code&gt; était le plus préoccupant sur la durée, tandis que l’alerte active pouvait très bien pointer ponctuellement vers le WD de &lt;code&gt;pve-forum&lt;/code&gt;. Ce n’était pas un OSD isolé qui souffrait en permanence : les trois SSD grand public encaissaient mal certaines rafales de petites écritures synchrones.&lt;/p&gt;
&lt;h2 id="wpq-était-une-piste-séduisante-mais-pas-la-bonne"&gt;WPQ était une piste séduisante, mais pas la bonne
&lt;/h2&gt;&lt;p&gt;Le cluster utilisait déjà mClock avec son profil &lt;code&gt;balanced&lt;/code&gt;. J’ai envisagé de repasser à la &lt;em&gt;Weighted Priority Queue&lt;/em&gt; (WPQ), l’ancien scheduler, parce que le changement est souvent cité lorsque l’on cherche à réduire des latences Ceph.&lt;/p&gt;
&lt;p&gt;Le souci, c’est que mon cluster n’était pas en train de faire du recovery ou du backfill. Les profils mClock savent répartir les ressources entre les opérations clientes, la récupération et les travaux de fond. Ici, les écritures des VM et celles provoquées par leurs sauvegardes appartenaient toutes à la même famille : des opérations clientes.&lt;/p&gt;
&lt;p&gt;Passer à &lt;code&gt;high_client_ops&lt;/code&gt; n’aurait donc pas appris à Ceph à préférer une VM interactive à sa propre sauvegarde. Revenir à WPQ aurait surtout changé la façon d’ordonner une saturation que je continuais à provoquer moi-même.&lt;/p&gt;
&lt;p&gt;La &lt;a class="link" href="https://docs.ceph.com/en/squid/rados/configuration/mclock-config-ref/" target="_blank" rel="noopener"
&gt;documentation mClock de Ceph&lt;/a&gt; recommande d’ailleurs de rester prudent avec les réglages de bas niveau. Avant d’ajuster le scheduler, il fallait d’abord vérifier ce que je lui demandais d’absorber.&lt;/p&gt;
&lt;h2 id="le-graphique-qui-revenait-toutes-les-deux-heures"&gt;Le graphique qui revenait toutes les deux heures
&lt;/h2&gt;&lt;p&gt;Mon job PBS global était configuré toutes les deux heures, sans limite de bande passante. À chaque déclenchement, les sauvegardes des VM hébergées sur les trois nœuds partaient en parallèle : lecture des disques Ceph, création des snapshots et envoi vers PBS.&lt;/p&gt;
&lt;p&gt;La corrélation avec les lenteurs était difficile à ignorer. Sur les 48 heures examinées :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;les 563 événements &lt;code&gt;aio_submit retries&lt;/code&gt; étaient tous apparus dans les trente minutes suivant le départ d’un backup ;&lt;/li&gt;
&lt;li&gt;800 des 1 138 avertissements BlueStore étaient concentrés dans cette même fenêtre ;&lt;/li&gt;
&lt;li&gt;les files NVMe avaient atteint une profondeur de 47 sur &lt;code&gt;pve-forum&lt;/code&gt; et de 20 sur &lt;code&gt;pve-fuji&lt;/code&gt; pendant un incident.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ceph ne manquait pas de capacité. Il recevait simplement, à intervalles réguliers, trois grosses tapes sur l’épaule au même moment 😂.&lt;/p&gt;
&lt;p&gt;Un quatrième PVE aurait probablement réduit la charge moyenne de chaque OSD. Il aurait aussi augmenté la capacité brute et les performances agrégées. Mais il n’aurait pas rendu raisonnables trois sauvegardes illimitées lancées ensemble. J’aurais payé du matériel pour mieux encaisser une mauvaise planification.&lt;/p&gt;
&lt;h2 id="quarante-minutes-entre-chaque-sauvegarde"&gt;Quarante minutes entre chaque sauvegarde
&lt;/h2&gt;&lt;p&gt;J’ai remplacé le job global par trois jobs liés chacun à un nœud. Ils restent exécutés toutes les deux heures, mais avec quarante minutes d’écart :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Nœud&lt;/th&gt;
&lt;th&gt;Horaire&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pve-forum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;heures paires à &lt;code&gt;:00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pve-dell&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;heures paires à &lt;code&gt;:40&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pve-fuji&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;heures impaires à &lt;code&gt;:20&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Dans la syntaxe du planificateur Proxmox, cela donne :&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;pve-forum */2:00
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pve-dell */2:40
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pve-fuji 1/2:20
&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 ajouté la même limite à chaque job :&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;bwlimit: 102400
&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 valeur est exprimée en Kio/s, soit 100 Mio/s. Le but n’était pas de rendre les sauvegardes artificiellement lentes, mais de leur laisser assez de débit sans leur permettre de prendre tout ce que les OSD pouvaient fournir pendant une pointe.&lt;/p&gt;
&lt;p&gt;Je n’ai modifié ni la destination PBS, ni le mode snapshot, ni les exclusions existantes. Ce petit périmètre était important : une seule hypothèse changeait à la fois, donc le résultat restait lisible.&lt;/p&gt;
&lt;h2 id="le-poc-a-calmé-la-pointe-pas-effacé-le-problème"&gt;Le PoC a calmé la pointe, pas effacé le problème
&lt;/h2&gt;&lt;p&gt;Après avoir vérifié qu’aucun backup n’était encore actif, j’ai lancé manuellement le nouveau job de &lt;code&gt;pve-dell&lt;/code&gt;. La sauvegarde complète s’est terminée en 43 secondes, avec des transferts ramenés autour de 80 à 104 Mio/s.&lt;/p&gt;
&lt;p&gt;Sur ce passage, les logs ont compté cinq retries AIO, contre 22 pendant la fenêtre globale précédente. La latence commit Ceph a culminé à 47 ms, puis les 129 groupes de placement sont restés &lt;code&gt;active+clean&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Cette comparaison reste indicative. Le PoC était fortement dédupliqué et un seul passage ne remplace pas plusieurs jours de métriques. Il a toutefois validé deux points utiles : le plafond était réellement appliqué et l’étalement réduisait déjà la violence de la pointe.&lt;/p&gt;
&lt;p&gt;Le cluster n’est pas devenu parfait pour autant. Ceph continue ponctuellement de remonter un &lt;code&gt;HEALTH_WARN&lt;/code&gt; pour des opérations BlueStore lentes. Les trois OSD sont toujours des SSD grand public sans protection contre les coupures de courant, et le P3 Plus en QLC reste le maillon qui m’inspire le moins confiance sous charge soutenue. La &lt;a class="link" href="https://docs.ceph.com/en/squid/start/hardware-recommendations/" target="_blank" rel="noopener"
&gt;documentation matérielle de Ceph&lt;/a&gt; rappelle justement que les SSD clients peuvent voir leurs performances s’effondrer une fois leur cache épuisé, là où les modèles entreprise apportent notamment la protection contre les pertes d’alimentation.&lt;/p&gt;
&lt;p&gt;Au moment où j’écris ces lignes, je prévois donc encore de remplacer progressivement les deux SSD les plus irréguliers, en commençant par celui de &lt;code&gt;pve-fuji&lt;/code&gt;. Mais ce sera une amélioration matérielle posée sur une charge déjà mieux organisée, pas un achat destiné à masquer le symptôme.&lt;/p&gt;
&lt;h2 id="ce-que-jen-retiens"&gt;Ce que j’en retiens
&lt;/h2&gt;&lt;p&gt;Je suis parti d’une question sur WPQ et de l’idée, un peu coûteuse, d’ajouter un quatrième nœud. La première amélioration utile a finalement tenu dans trois horaires et une limite de débit.&lt;/p&gt;
&lt;p&gt;Le plus intéressant n’est pas que 100 Mio/s soit une valeur magique — elle ne l’est pas. C’est d’avoir trouvé le rythme réel du problème avant de toucher au scheduler ou au matériel. Les ralentissements revenaient toutes les deux heures parce que mes backups revenaient toutes les deux heures. Une fois la corrélation posée noir sur blanc, la solution est devenue beaucoup moins mystérieuse.&lt;/p&gt;
&lt;p&gt;Pour ceux qui se demandent déjà vers quoi je compte me tourner, je remplacerais progressivement les SSD concernés par des &lt;a class="link" href="https://www.amazon.fr/gp/product/B0B9C4DKKG/ref=ox_sc_act_title_1?smid=A1X6FK5RDHNB96&amp;amp;th=1" target="_blank" rel="noopener"
&gt;Samsung 990 PRO 2 To, référence &lt;code&gt;MZ-V9P2T0BW&lt;/code&gt;&lt;/a&gt;. Cela reste du matériel grand public, pas l’équivalent d’un NVMe datacenter avec protection contre les coupures, mais c’est le compromis que je retiendrais aujourd’hui pour mon homelab.&lt;/p&gt;
&lt;p&gt;Je vais maintenant laisser tourner cette configuration, comparer plusieurs fenêtres de sauvegarde et observer si les futurs SSD changent encore le profil des latences. Le quatrième PVE, lui, peut rester pour l’instant dans la boutique.&lt;/p&gt;</description></item><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>Authentik, la porte d’entrée de mon homelab — et le ménage qui lui manquait</title><link>https://blog.homeblack.fr/p/authentik-sso-homelab/</link><pubDate>Fri, 31 Jul 2026 15:06:16 +0200</pubDate><guid>https://blog.homeblack.fr/p/authentik-sso-homelab/</guid><description>&lt;img src="https://blog.homeblack.fr/p/authentik-sso-homelab/cover.png" alt="Featured image of post Authentik, la porte d’entrée de mon homelab — et le ménage qui lui manquait" /&gt;&lt;p&gt;Je voulais gagner quelques millisecondes sur mes connexions. En ouvrant la VM d’Authentik, j’ai surtout trouvé un petit musée de son histoire : un conteneur Redis devenu inutile, deux fichiers d’environnement presque identiques et des images Docker accumulées.&lt;/p&gt;
&lt;p&gt;Comme le service tournait bien, j’avais laissé cette sédimentation vivre tranquillement. Pas idéal pour celui qui garde la porte de presque tout mon homelab. Cette maintenance était donc l’occasion de faire le ménage, mais aussi de présenter enfin Authentik sur ce blog.&lt;/p&gt;
&lt;h2 id="bien-plus-quune-jolie-page-de-connexion"&gt;Bien plus qu’une jolie page de connexion
&lt;/h2&gt;&lt;p&gt;SSO signifie &lt;em&gt;Single Sign-On&lt;/em&gt;, ou authentification unique. Le raccourci habituel consiste à dire : « un seul compte pour toutes mes applications ». C’est vrai, mais ce n’est pas le point le plus important.&lt;/p&gt;
&lt;p&gt;Une application raccordée à Authentik ne reçoit pas mon mot de passe. Elle délègue l’authentification à un fournisseur d’identité — un &lt;em&gt;Identity Provider&lt;/em&gt; ou IdP — puis fait confiance au jeton ou à l’assertion qu’il lui retourne. Je peux donc centraliser la double authentification, les groupes, les règles d’accès et le cycle de vie des comptes sans recopier la même logique dans chaque service.&lt;/p&gt;
&lt;p&gt;Au 31 juillet 2026, mon instance contient 21 applications. La majorité utilise OpenID Connect (OIDC), notamment GitLab, Grafana, Warpgate, Technitium et PatchMon. Wazuh passe par SAML. Pour OpenVSCode, qui n’a pas le même raccordement natif, nginx demande directement à l’outpost Proxy d’Authentik si la requête peut continuer.&lt;/p&gt;
&lt;p&gt;Cette souplesse est l’une des raisons pour lesquelles Authentik s’est imposé chez moi. Je privilégie OIDC lorsqu’il est disponible, SAML lorsque l’application l’impose et le &lt;a class="link" href="https://docs.goauthentik.io/add-secure-apps/providers/proxy/create-proxy-provider/" target="_blank" rel="noopener"
&gt;provider Proxy&lt;/a&gt; pour les services qui ne savent rien faire de tout cela.&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GitLab ─────────────── OIDC ──────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Grafana ────────────── OIDC ──────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Warpgate ───────────── OIDC ──────┤
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Wazuh ──────────────── SAML ──────┼──&amp;gt; Authentik ──&amp;gt; identité, MFA, groupes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OpenVSCode ── nginx / forward auth ┘
&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 confort est évident : une seule session ouvre plusieurs outils et je n’administre plus une collection de comptes locaux oubliés. Le revers l’est tout autant : Authentik est devenu une dépendance critique. Une panne n’arrête pas forcément les sessions déjà ouvertes, mais elle peut bloquer toute nouvelle connexion. Une compromission aurait, elle, un rayon d’action particulièrement généreux.&lt;/p&gt;
&lt;p&gt;Je le traite donc comme une brique de sécurité, pas comme un tableau de bord de plus.&lt;/p&gt;
&lt;h2 id="pourquoi-authentik-plutôt-que-keycloak-"&gt;Pourquoi Authentik plutôt que Keycloak ?
&lt;/h2&gt;&lt;p&gt;J’ai longtemps hésité avec Keycloak. Il avait pour lui l’ancienneté, une documentation abondante et un écosystème déjà installé. &lt;a class="link" href="https://api.github.com/repos/keycloak/keycloak" target="_blank" rel="noopener"
&gt;Son dépôt public remonte à 2013&lt;/a&gt;, alors que &lt;a class="link" href="https://goauthentik.io/blog/2023-11-1-happy-birthday-to-us/" target="_blank" rel="noopener"
&gt;le premier commit d’Authentik date de 2018&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Choisir Authentik revenait donc à miser sur le plus jeune. Son interface et ses flows me parlaient davantage, son proxy intégré convenait bien au mélange d’applications modernes et de vieux services de mon homelab, et je me sentais plus à l’aise avec un cœur Python/Django qu’avec une JVM.&lt;/p&gt;
&lt;p&gt;Ce n’est pas un verdict contre Java : Keycloak fonctionne sur Quarkus et propose un &lt;a class="link" href="https://www.keycloak.org/server/configuration" target="_blank" rel="noopener"
&gt;build optimisé&lt;/a&gt;. Authentik n’est pas spécialement léger non plus avec son serveur, son worker, PostgreSQL et ses éventuels outposts. Mon choix était surtout pratique : lequel aurais-je envie de comprendre et de dépanner pendant plusieurs années ? Pour moi, la réponse était Authentik.&lt;/p&gt;
&lt;h2 id="un-projet-qui-a-mûri"&gt;Un projet qui a mûri
&lt;/h2&gt;&lt;p&gt;Les premières versions que j’ai suivies demandaient davantage de prudence : migrations sensibles, interface mouvante et notes de version à lire religieusement. Depuis la création d’Authentik Security en 2022, le projet a changé d’échelle et adopté un modèle &lt;em&gt;open core&lt;/em&gt;, avec les protocoles essentiels toujours disponibles dans &lt;a class="link" href="https://goauthentik.io/pricing/" target="_blank" rel="noopener"
&gt;l’édition open source&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Au 31 juillet 2026, &lt;a class="link" href="https://api.github.com/repos/goauthentik/authentik" target="_blank" rel="noopener"
&gt;le dépôt GitHub&lt;/a&gt; affichait 22 563 étoiles et une activité le jour même. La &lt;a class="link" href="https://github.com/goauthentik/authentik/releases/tag/version/2026.5.6" target="_blank" rel="noopener"
&gt;version 2026.5.6&lt;/a&gt; datait du 22 juillet. Ces chiffres ne prouvent pas la qualité du logiciel, mais ils confirment un développement suivi. Après plusieurs années de mises à jour, c’est surtout cette continuité qui me rassure.&lt;/p&gt;
&lt;h2 id="redis-était-parti-mon-compose-ne-le-savait-pas"&gt;Redis était parti, mon Compose ne le savait pas
&lt;/h2&gt;&lt;p&gt;Authentik a déplacé ses tâches vers PostgreSQL en 2025.8, puis le cache, les sessions Proxy et les WebSockets en 2025.10. Redis pouvait disparaître. Mon ancien Compose continuait pourtant à lancer ce quatrième conteneur, presque inactif mais toujours présent dans les dépendances et les variables d’environnement.&lt;/p&gt;
&lt;p&gt;L’architecture actuelle repose sur trois rôles : PostgreSQL conserve les données, le serveur traite les requêtes et le worker s’occupe des tâches asynchrones. Les &lt;a class="link" href="https://docs.goauthentik.io/releases/2025.10" target="_blank" rel="noopener"
&gt;notes de version 2025.10&lt;/a&gt; préviennent toutefois que PostgreSQL reçoit environ 50 % de connexions supplémentaires. J’ai supprimé une dépendance, pas sa charge de travail.&lt;/p&gt;
&lt;p&gt;Après le ménage, la liste des services est devenue très lisible :&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-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gp"&gt;$&lt;/span&gt; docker compose -f /opt/authentik/docker-compose.yml config --services
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;postgresql
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;server
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;worker
&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;J’en ai profité pour fusionner &lt;code&gt;.env&lt;/code&gt; et &lt;code&gt;authentik.env&lt;/code&gt;, retirer les variables Redis, ajouter le montage &lt;code&gt;/data&lt;/code&gt; et porter la mémoire partagée du serveur et du worker à 512 Mio. Avant le déploiement Ansible, j’ai vérifié la sauvegarde Proxmox Backup Server et la lisibilité d’un dump PostgreSQL. Le second passage du playbook n’a produit aucun changement.&lt;/p&gt;
&lt;h2 id="les-logs-avaient-aussi-besoin-dune-limite"&gt;Les logs avaient aussi besoin d’une limite
&lt;/h2&gt;&lt;p&gt;La VM ne dispose que d’un disque de 20 Gio. Avant l’intervention, 13 Gio étaient occupés et le stack produisait environ 19 000 lignes de logs par jour sans aucune rotation.&lt;/p&gt;
&lt;p&gt;J’ai conservé le pilote &lt;code&gt;json-file&lt;/code&gt;. Vector consomme l’API de logs Docker et Wazuh lit les fichiers JSON ; changer de pilote aurait donc cassé silencieusement une partie de l’observabilité. En revanche, chaque conteneur possède maintenant une rotation bornée :&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;logging&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;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;json-file&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;options&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;max-size&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;20m&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="nt"&gt;max-file&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;5&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;Après inventaire des images actives et nettoyage des seules images inutilisées, l’occupation est passée de 13 Gio à 4,6 Gio : environ 8,4 Gio récupérés, sans toucher aux volumes.&lt;/p&gt;
&lt;h2 id="le-vrai-goulot-nétait-pas-docker"&gt;Le vrai goulot n’était pas Docker
&lt;/h2&gt;&lt;p&gt;J’étais venu chercher quelques millisecondes dans le bridge Docker. Il n’en ajoutait que 0,1 à 0,2, tandis que la route publique répondait autour de 152 ms. Passer en mode &lt;code&gt;host&lt;/code&gt; aurait surtout échangé de l’isolation contre un gain invisible.&lt;/p&gt;
&lt;p&gt;La piste utile se trouvait un étage plus bas. La VM tournait sur &lt;code&gt;pve-fuji&lt;/code&gt;, un nœud Intel déjà chargé, avec parfois 5 à 8 % de temps CPU volé. Je l’ai migrée à chaud vers &lt;code&gt;pve-forum&lt;/code&gt;, un nœud AMD plus disponible. La latence a immédiatement chuté… puis la VM a perdu le réseau avant de se figer. À cet instant, mon petit benchmark venait de se transformer en exercice de reprise.&lt;/p&gt;
&lt;p&gt;Après retour sur &lt;code&gt;pve-fuji&lt;/code&gt; et redémarrage forcé, PostgreSQL et les trois conteneurs sont repartis sans perte. La bonne méthode était une migration à froid : arrêt propre par Proxmox HA, déplacement de la VM éteinte, puis redémarrage sur &lt;code&gt;pve-forum&lt;/code&gt;. L’interruption a duré environ 2 min 40.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Étape&lt;/th&gt;
&lt;th style="text-align: right"&gt;Route publique moyenne observée&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;état initial sur &lt;code&gt;pve-fuji&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;151,6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose modernisé, retour sur &lt;code&gt;pve-fuji&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;105,8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;migration à froid vers &lt;code&gt;pve-forum&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;68,9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ces mesures appartiennent à mon environnement, pas à un benchmark universel d’Authentik. Elles montrent surtout que le placement de la VM pesait bien plus que le réseau Docker — rien de très surprenant dans mon homelab bricolé de toutes pièces :D&lt;/p&gt;
&lt;h2 id="une-première-porte-ouverte"&gt;Une première porte ouverte
&lt;/h2&gt;&lt;p&gt;Ce billet n’avait pas vocation à devenir un guide d’Authentik. Mon blog vient de démarrer et je n’avais encore jamais présenté le service qui protège pourtant 21 de mes applications. Cette maintenance était simplement la bonne occasion de lui ouvrir la porte.&lt;/p&gt;
&lt;p&gt;Je reviendrai dans un autre article sur mon organisation autour d’Authentik : les flows, les groupes, la double authentification, le raccordement des applications et les cas où j’utilise le proxy plutôt qu’OIDC ou SAML. Pour cette fois, je voulais simplement raconter comment un conteneur Redis devenu inutile m’a donné le prétexte de présenter le gardien de mon homelab.&lt;/p&gt;</description></item><item><title>12 Mo de SQLite, 28 Go d’écritures par jour : le piège des Query Logs de Technitium</title><link>https://blog.homeblack.fr/p/technitium-query-logs-sqlite/</link><pubDate>Thu, 30 Jul 2026 11:00:00 +0200</pubDate><guid>https://blog.homeblack.fr/p/technitium-query-logs-sqlite/</guid><description>&lt;img src="https://blog.homeblack.fr/p/technitium-query-logs-sqlite/cover.png" alt="Featured image of post 12 Mo de SQLite, 28 Go d’écritures par jour : le piège des Query Logs de Technitium" /&gt;&lt;p&gt;Ma VM DNS écrivait près de 28 Go par jour sur Ceph. Avec un tel chiffre, je m’attendais à trouver un volume anormal de requêtes, un service bavard ou un buffer Vector qui débordait.&lt;/p&gt;
&lt;p&gt;La réalité tenait dans une base SQLite de 12 Mo.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;trust-dns-01&lt;/code&gt; résout environ deux à trois requêtes par seconde. Ce n’est ni une attaque ni une charge impressionnante. Pourtant, le stockage travaillait suffisamment pour que la VM passe bien plus de temps à attendre ses entrées-sorties qu’à calculer.&lt;/p&gt;
&lt;h2 id="le-dns-nétait-pas-débordé"&gt;Le DNS n’était pas débordé
&lt;/h2&gt;&lt;p&gt;Le 30 juillet 2026, j’ai repris les métriques sur les dernières 24 heures et l’état réel de la machine :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mesure&lt;/th&gt;
&lt;th style="text-align: right"&gt;Valeur observée&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;trafic DNS&lt;/td&gt;
&lt;td style="text-align: right"&gt;environ 2,5 requêtes/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU actif, hors attente I/O&lt;/td&gt;
&lt;td style="text-align: right"&gt;6,3 % en moyenne&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attente I/O (&lt;em&gt;iowait&lt;/em&gt;)&lt;/td&gt;
&lt;td style="text-align: right"&gt;29,1 % en moyenne&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;données écrites sur le disque&lt;/td&gt;
&lt;td style="text-align: right"&gt;27,89 Go en 24 h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;occupation du disque&lt;/td&gt;
&lt;td style="text-align: right"&gt;8,8 Go sur 20 Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;La nuance entre CPU actif et &lt;em&gt;iowait&lt;/em&gt; est importante. Un graphique calculé simplement comme &lt;code&gt;100 - idle&lt;/code&gt; donne l’impression que le processeur est occupé à 35 %. En réalité, sur cette VM limitée à un vCPU, près de 29 points correspondent au temps passé à attendre le stockage.&lt;/p&gt;
&lt;p&gt;Le serveur DNS lui-même ne manque donc pas de puissance. Il attend surtout Ceph.&lt;/p&gt;
&lt;h2 id="une-petite-base-qui-réécrit-sans-arrêt"&gt;Une petite base qui réécrit sans arrêt
&lt;/h2&gt;&lt;p&gt;Technitium possède une application nommée &lt;code&gt;Query Logs (Sqlite)&lt;/code&gt;. Elle permet de retrouver précisément quel client a demandé quel domaine, à quelle heure et avec quelle réponse.&lt;/p&gt;
&lt;p&gt;Sur mon instance, sa configuration contient notamment :&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;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&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-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;enableLogging&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;maxLogDays&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;maxLogRecords&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;enableVacuum&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;useInMemoryDb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;sqliteDbPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;querylogs.db&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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 base fait environ 12 Mo et conserve autour de 10 000 entrées. Pendant une vérification, elle contenait 12 102 lignes couvrant un peu plus de 80 minutes, soit environ 2,5 requêtes par seconde.&lt;/p&gt;
&lt;p&gt;Ce léger dépassement est normal : le plafond n’est pas appliqué à chaque insertion. Le nettoyage se déclenche toutes les quinze minutes. Entre deux passages, la table continue de grossir.&lt;/p&gt;
&lt;p&gt;Dit comme cela, rien d’inquiétant. Le détail qui change tout se trouve dans les réglages effectifs de SQLite :&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journal_mode = delete
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;synchronous = 2
&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 valeur &lt;code&gt;2&lt;/code&gt; correspond au mode &lt;code&gt;FULL&lt;/code&gt;. Avec le journal &lt;code&gt;DELETE&lt;/code&gt;, SQLite crée un journal de rollback pour la transaction, synchronise les données, puis supprime le journal une fois le commit terminé. Ces choix privilégient la durabilité, comme l’explique la &lt;a class="link" href="https://sqlite.org/pragma.html#pragma_journal_mode" target="_blank" rel="noopener"
&gt;documentation officielle de SQLite sur les modes de journalisation&lt;/a&gt; et la &lt;a class="link" href="https://sqlite.org/pragma.html#pragma_synchronous" target="_blank" rel="noopener"
&gt;synchronisation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ici, chaque nouvelle requête ajoute une ligne et met à jour treize index en plus de la clé primaire. L’application consomme les requêtes disponibles sans attendre de remplir un gros lot ; à faible trafic, elle peut donc committer très peu de lignes à la fois. Lorsque la limite est dépassée, les anciennes lignes sont aussi supprimées. La base reste petite, mais elle tourne en permanence : insertions, index, suppressions, journal, synchronisations.&lt;/p&gt;
&lt;p&gt;Sur un disque local, cette activité passerait probablement inaperçue. Sur un stockage distribué comme Ceph, une succession de petites écritures synchrones coûte beaucoup plus cher que sa taille apparente. Le fichier &lt;code&gt;querylogs.db-journal&lt;/code&gt;, présent et ouvert pendant le diagnostic, était le petit témoin de cette mécanique.&lt;/p&gt;
&lt;p&gt;Deux mesures supplémentaires renforcent le diagnostic : le compteur du processus Technitium indiquait environ 15,5 Go écrits en 23 heures et, sur un échantillon de quinze secondes, 32 nouvelles requêtes ont accompagné 2,97 Mo d’écritures processus.&lt;/p&gt;
&lt;p&gt;Je garde tout de même une réserve : Technitium écrit aussi ses statistiques et ses journaux, tandis que la métrique des 27,89 Go couvre toute la VM. Query Logs est la cause principale la mieux étayée, mais la preuve définitive viendra de la comparaison sur 24 heures après sa désactivation. Sans cet avant/après, lui attribuer chaque octet serait aller un peu vite.&lt;/p&gt;
&lt;h2 id="lespace-disque-racontait-une-autre-histoire"&gt;L’espace disque racontait une autre histoire
&lt;/h2&gt;&lt;p&gt;L’amplification d’écriture n’explique pas à elle seule les 8,8 Go déjà occupés. Les plus gros consommateurs évitables étaient ailleurs :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Données&lt;/th&gt;
&lt;th style="text-align: right"&gt;Taille observée&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sauvegardes Technitium&lt;/td&gt;
&lt;td style="text-align: right"&gt;3,7 Gio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;statistiques DNS&lt;/td&gt;
&lt;td style="text-align: right"&gt;568 Mio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;journaux systemd&lt;/td&gt;
&lt;td style="text-align: right"&gt;623 Mio&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Le script de sauvegarde actuel supprime les archives âgées de plus de 30 jours. Cela laisse tout de même 36 fichiers, car une sauvegarde quotidienne pèse désormais entre 100 et 123 Mo et quelques sauvegardes manuelles s’ajoutent au lot.&lt;/p&gt;
&lt;p&gt;Ce sont donc deux problèmes distincts :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query Logs use le stockage avec beaucoup de petites écritures ;&lt;/li&gt;
&lt;li&gt;les sauvegardes, statistiques et journaux consomment réellement l’espace disponible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Supprimer la base SQLite ne libérerait qu’une douzaine de mégaoctets. Pour faire maigrir la VM, il faut aussi agir sur les rétentions.&lt;/p&gt;
&lt;h2 id="garder-les-requêtes-sans-garder-sqlite"&gt;Garder les requêtes, sans garder SQLite
&lt;/h2&gt;&lt;p&gt;Ma première idée était de désactiver complètement les journaux détaillés et de conserver seulement les statistiques agrégées dans Grafana. C’était efficace, mais je perdais une fonction à laquelle je tiens : pouvoir rechercher les requêtes DNS dans VictoriaLogs.&lt;/p&gt;
&lt;p&gt;Le compromis que je prépare est donc le suivant :&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Technitium Log Exporter
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ HTTP local
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Vector
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ NDJSON compressé
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VictoriaLogs
&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;L’application officielle &lt;a class="link" href="https://github.com/TechnitiumSoftware/DnsServer/tree/master/Apps/LogExporterApp" target="_blank" rel="noopener"
&gt;Log Exporter de Technitium&lt;/a&gt; peut expédier les requêtes par HTTP ou Syslog sans les écrire dans un fichier local. Vector recevra les lots en mémoire, extraira les champs utiles — client, nom demandé, type, code de réponse et temps de traitement — puis les enverra vers VictoriaLogs.&lt;/p&gt;
&lt;p&gt;Vector n’est pas seulement là pour faire joli au milieu du schéma. Le sink HTTP utilisé par Technitium produit des lots JSON enveloppés par Serilog, alors que l’endpoint &lt;code&gt;/insert/jsonline&lt;/code&gt; de VictoriaLogs attend &lt;a class="link" href="https://docs.victoriametrics.com/victorialogs/data-ingestion/#json-stream-api" target="_blank" rel="noopener"
&gt;un objet JSON par ligne&lt;/a&gt;. Il faut donc convertir et normaliser les événements.&lt;/p&gt;
&lt;p&gt;Je préfère un buffer mémoire borné pour ce flux. Cela évite de recréer une nouvelle source d’écritures locales, au prix d’un compromis assumé : une panne ou un redémarrage peut faire perdre quelques requêtes. Pour mon homelab, la visibilité opérationnelle compte davantage qu’une conservation forensique parfaitement exhaustive.&lt;/p&gt;
&lt;p&gt;Ces données restent sensibles. Une suite de requêtes DNS raconte rapidement les habitudes d’un réseau. Les noms demandés et les adresses clientes resteront des champs recherchables, mais pas des champs de &lt;em&gt;stream&lt;/em&gt; à forte cardinalité, et leur accès devra être plus restreint que celui des journaux système ordinaires.&lt;/p&gt;
&lt;h2 id="la-suite-avec-un-vrai-avantaprès"&gt;La suite, avec un vrai avant/après
&lt;/h2&gt;&lt;p&gt;Au moment où j’écris ces lignes, aucune modification de production n’a encore été appliquée. Le déploiement suivra volontairement plusieurs étapes :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;activer Log Exporter et le flux Vector tout en gardant temporairement SQLite ;&lt;/li&gt;
&lt;li&gt;comparer les événements pendant 30 à 60 minutes ;&lt;/li&gt;
&lt;li&gt;mesurer pendant 24 à 72 heures le volume ingéré, la RAM et les écritures Ceph ;&lt;/li&gt;
&lt;li&gt;désactiver Query Logs SQLite seulement après cette validation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Je conserverai ensuite la base quelques jours comme possibilité de retour arrière. Si les écritures chutent comme prévu, j’aurai enfin le chiffre qui manque à ce diagnostic : le gain réel, pas seulement le coupable le plus crédible.&lt;/p&gt;
&lt;p&gt;Je pensais devoir redimensionner un serveur DNS. En réalité, il fallait surtout l’empêcher de tenir son journal intime sur Ceph.&lt;/p&gt;</description></item><item><title>Trop d’alertes, plus aucune confiance : comment j’ai repris en main Checkmk</title><link>https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/</link><pubDate>Wed, 29 Jul 2026 16:00:00 +0200</pubDate><guid>https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/</guid><description>&lt;img src="https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/cover.png" alt="Featured image of post Trop d’alertes, plus aucune confiance : comment j’ai repris en main Checkmk" /&gt;&lt;p&gt;Une interface réseau disparaît à la fin d’un job. La mémoire du runner grimpe pendant un build. Une sonde mail dépasse son délai une seule fois. Un disque rappelle toutes les quelques minutes qu’il est toujours plein.&lt;/p&gt;
&lt;p&gt;Pris séparément, chacun de ces événements mérite d’être observé. Transformés avec la même urgence en notifications, ils finissent surtout par m’apprendre à ne plus les regarder.&lt;/p&gt;
&lt;p&gt;C’est le piège classique de la supervision : au début, on veut tout voir. Puis on ajoute des contrôles, des machines et des seuils jusqu’au moment où le système fonctionne techniquement, mais où le signal utile se retrouve noyé dans le bruit.&lt;/p&gt;
&lt;p&gt;J’ai donc repris mes règles Checkmk une par une. Mon objectif n’était pas d’obtenir un joli écran entièrement vert. Je voulais qu’une notification signifie de nouveau : « il faut regarder maintenant ».&lt;/p&gt;
&lt;h2 id="le-bruit-na-pas-une-cause-unique"&gt;Le bruit n’a pas une cause unique
&lt;/h2&gt;&lt;p&gt;La mauvaise solution aurait été de relever tous les seuils ou de désactiver les services pénibles. C’est tentant, rapide et particulièrement efficace pour fabriquer une fausse tranquillité.&lt;/p&gt;
&lt;p&gt;À la place, j’ai classé chaque alerte dans l’une de ces familles :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Exemple dans mon homelab&lt;/th&gt;
&lt;th&gt;Réponse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;état attendu&lt;/td&gt;
&lt;td&gt;interface TAP supprimée avec une VM éphémère&lt;/td&gt;
&lt;td&gt;ignorer précisément ce service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;incident transitoire&lt;/td&gt;
&lt;td&gt;pic de mémoire pendant un job CI&lt;/td&gt;
&lt;td&gt;demander une seconde confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;problème réel mais répétitif&lt;/td&gt;
&lt;td&gt;disque toujours en alerte&lt;/td&gt;
&lt;td&gt;conserver l’état, espacer les notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seuil mal adapté&lt;/td&gt;
&lt;td&gt;résolution DNS correcte mais un peu variable&lt;/td&gt;
&lt;td&gt;recalibrer avec une mesure cohérente&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Cette distinction paraît évidente après coup. Dans la pratique, elle oblige à répondre à une question parfois inconfortable : qu’est-ce qui justifie réellement de m’interrompre ?&lt;/p&gt;
&lt;h2 id="les-interfaces-tap--du-bruit-parfaitement-normal"&gt;Les interfaces TAP : du bruit parfaitement normal
&lt;/h2&gt;&lt;p&gt;Mes nœuds Proxmox créent des interfaces TAP pour connecter les interfaces réseau virtuelles des VM. Leur nom suit une forme comme :&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;tap&amp;lt;vmid&amp;gt;i&amp;lt;nic&amp;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;Ces interfaces vivent aussi longtemps que la VM ou la charge CI correspondante. Lorsqu’une machine temporaire s’arrête, son interface disparaît. Checkmk peut alors interpréter cette disparition comme un changement d’état à signaler.&lt;/p&gt;
&lt;p&gt;Le problème ne venait donc pas d’une panne réseau. Le cycle de vie normal d’une ressource éphémère était traité comme celui d’un port physique.&lt;/p&gt;
&lt;p&gt;J’ai commencé par faire découvrir les interfaces Proxmox avec leur description Linux stable plutôt qu’avec un numéro générique du type &lt;code&gt;Interface 12&lt;/code&gt;. Cela évite qu’un bridge, un VLAN ou une nouvelle interface récupère le service d’un ancien périphérique.&lt;/p&gt;
&lt;p&gt;J’ai ensuite ajouté une règle très ciblée :&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;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;ruleset&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ignored_services&amp;#34;&lt;/span&gt;&lt;span class="p"&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;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Homeblack - ignore ephemeral Proxmox TAP interfaces&amp;#34;&lt;/span&gt;&lt;span class="p"&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;value&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&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;conditions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;host_name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;match_on&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pve-dell&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pve-forum&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pve-fuji&amp;#34;&lt;/span&gt;&lt;span class="p"&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;operator&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;one_of&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&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;service_description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;match_on&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Interface tap[0-9]+i[0-9]+$&amp;#34;&lt;/span&gt;&lt;span class="p"&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;operator&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;one_of&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;Je n’ignore pas « les interfaces de Proxmox ». Je retire uniquement les services qui correspondent au motif des TAP éphémères. Les interfaces physiques, les bridges, les VLANs et les interfaces des machines invitées restent surveillés.&lt;/p&gt;
&lt;p&gt;C’est une nuance importante : une bonne exception décrit un comportement attendu, pas une catégorie suffisamment large pour faire disparaître le problème.&lt;/p&gt;
&lt;h2 id="un-pic-nest-pas-encore-une-panne"&gt;Un pic n’est pas encore une panne
&lt;/h2&gt;&lt;p&gt;Le runner GitLab présente un autre cas. Pendant un build, sa consommation mémoire peut monter brutalement pendant un cycle de contrôle, puis revenir à la normale.&lt;/p&gt;
&lt;p&gt;Ce pic reste intéressant. S’il dure, je veux le savoir. S’il ne concerne qu’une mesure isolée, je ne veux pas être interrompu.&lt;/p&gt;
&lt;p&gt;Checkmk distingue justement les états &lt;em&gt;soft&lt;/em&gt; et &lt;em&gt;hard&lt;/em&gt;. Un premier échec peut rester transitoire ; il ne devient un problème confirmé qu’après le nombre de tentatives défini.&lt;/p&gt;
&lt;p&gt;Pour le service &lt;code&gt;Memory&lt;/code&gt; de &lt;code&gt;trust-runner-01&lt;/code&gt;, j’ai choisi deux contrôles consécutifs :&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;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;ruleset&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;extra_service_conf:max_check_attempts&amp;#34;&lt;/span&gt;&lt;span class="p"&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;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Homeblack - retry transient runner memory pressure&amp;#34;&lt;/span&gt;&lt;span class="p"&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;value&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&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;conditions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;host_name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;match_on&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;trust-runner-01&amp;#34;&lt;/span&gt;&lt;span class="p"&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;operator&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;one_of&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&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;service_description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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;match_on&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Memory$&amp;#34;&lt;/span&gt;&lt;span class="p"&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;operator&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;one_of&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&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;Un pic d’un cycle reste visible dans l’historique, mais ne déclenche pas de notification immédiate. Si la pression persiste au contrôle suivant, l’état devient &lt;em&gt;hard&lt;/em&gt; et l’alerte suit son chemin normal. Avec la fréquence actuelle des contrôles, cela représente environ une minute de confirmation.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/cover.png"
width="2209"
height="1355"
srcset="https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/cover_hu_55525788465104df.png 480w, https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/cover_hu_7a059729b680908e.png 1024w"
loading="lazy"
alt="Le service Memory du runner dans Checkmk, avec ses pics de consommation visibles"
class="gallery-image"
data-flex-grow="163"
data-flex-basis="391px"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Les pics courts restent visibles dans le graphe sans transformer automatiquement chaque mesure isolée en notification.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;J’ai appliqué le même raisonnement aux contrôles SMTP entrant et IMAP de ma plateforme mail. Un timeout isolé mérite une nouvelle tentative ; deux échecs consécutifs commencent à ressembler à un incident.&lt;/p&gt;
&lt;p&gt;En revanche, je n’ai pas ajouté un délai arbitraire de plusieurs heures au test de livraison de bout en bout. Cette sonde possède déjà sa propre notion du temps : avertissement après 120 secondes et état critique après 300 secondes. Ajouter quatre heures de silence par-dessus revenait à rendre le contrôle presque inutile.&lt;/p&gt;
&lt;h2 id="espacer-une-notification-sans-effacer-le-problème"&gt;Espacer une notification sans effacer le problème
&lt;/h2&gt;&lt;p&gt;Les disques posent une question différente. Un filesystem plein, une erreur SMART ou une latence d’entrée-sortie ne disparaissent pas parce que j’ai déjà reçu l’alerte.&lt;/p&gt;
&lt;p&gt;Mais recevoir continuellement le même message ne rend pas la réparation plus rapide.&lt;/p&gt;
&lt;p&gt;J’ai conservé les services &lt;code&gt;SMART&lt;/code&gt;, &lt;code&gt;Filesystem&lt;/code&gt; et &lt;code&gt;Disk IO&lt;/code&gt; visibles dans Checkmk avec leurs états normaux. C’est uniquement la fréquence de leurs notifications sortantes qui est limitée :&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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DISK_NOTIFICATION_COOLDOWN_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DISK_SERVICE_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;SMART &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Filesystem &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Disk IO &amp;#34;&lt;/span&gt;&lt;span class="p"&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;Le mécanisme garde une réservation par couple hôte/service. Une première notification de problème passe, puis les répétitions sont bloquées pendant 24 heures glissantes. Si le problème reste actif, Checkmk envoie un rappel quotidien.&lt;/p&gt;
&lt;p&gt;Les retours à l’état normal restent présents dans l’historique Checkmk, mais ne génèrent pas de nouvelle notification pour ces trois familles de services. Je préfère ouvrir l’interface et retrouver toute la chronologie plutôt que d’être interrompu à chaque oscillation autour d’un seuil disque.&lt;/p&gt;
&lt;p&gt;Il y a un détail auquel je tenais : si l’envoi de la notification échoue, la réservation est libérée. Le système pourra donc retenter plus tard au lieu de considérer comme livré un message qui n’est jamais arrivé.&lt;/p&gt;
&lt;p&gt;Enfin, cette limitation ne concerne ni les hôtes, ni les autres services. Une machine inaccessible ou un contrôle métier en échec ne profite pas discrètement du même silence.&lt;/p&gt;
&lt;h2 id="un-seuil-doit-représenter-un-risque"&gt;Un seuil doit représenter un risque
&lt;/h2&gt;&lt;p&gt;La supervision DNS de Technitium m’a fourni un bon exemple de seuil techniquement valide, mais opérationnellement trop sensible.&lt;/p&gt;
&lt;p&gt;Je contrôle deux choses depuis le serveur Checkmk :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;une résolution DNS autoritative en UDP, avec vérification de la réponse ;&lt;/li&gt;
&lt;li&gt;l’ouverture du transport DNS sur le port TCP 53.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Au départ, les deux contrôles avertissaient au-dessus de 500 millisecondes. J’ai conservé ce seuil pour l’ouverture TCP, mais porté l’avertissement UDP à une seconde. L’état critique reste fixé à 1,5 seconde et le timeout à trois secondes.&lt;/p&gt;
&lt;p&gt;Pourquoi séparer les deux ? Une connexion TCP sur le réseau interne doit rester très rapide. Une requête DNS complète traverse davantage d’étapes et peut subir une variation ponctuelle sans que le service soit dégradé.&lt;/p&gt;
&lt;p&gt;Le 29 juillet 2026, ma vérification en lecture seule remontait :&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th style="text-align: right"&gt;État observé&lt;/th&gt;
&lt;th style="text-align: right"&gt;Temps de réponse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;résolution DNS Technitium en UDP&lt;/td&gt;
&lt;td style="text-align: right"&gt;OK&lt;/td&gt;
&lt;td style="text-align: right"&gt;65 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transport DNS Technitium en TCP&lt;/td&gt;
&lt;td style="text-align: right"&gt;OK&lt;/td&gt;
&lt;td style="text-align: right"&gt;7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ce sont des mesures prises à un instant donné, pas une promesse de performance. Elles confirment surtout que les contrôles interrogent réellement le service et exposent leur métrique après la modification.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/checkmk-technitium-dns.png"
width="2209"
height="1355"
srcset="https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/checkmk-technitium-dns_hu_43c66d4b85749bcc.png 480w, https://blog.homeblack.fr/p/reduire-bruit-alertes-checkmk/checkmk-technitium-dns_hu_4d9627a493fa113b.png 1024w"
loading="lazy"
alt="La sonde DNS UDP de Technitium dans Checkmk, avec ses seuils d’avertissement et critique"
class="gallery-image"
data-flex-grow="163"
data-flex-basis="391px"
&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;La mesure UDP reste très en dessous du seuil d’avertissement fixé à une seconde ; le seuil critique apparaît à 1,5 seconde.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="ce-que-je-refuse-de-faire-taire"&gt;Ce que je refuse de faire taire
&lt;/h2&gt;&lt;p&gt;Réduire le bruit n’a d’intérêt que si les vrais problèmes restent bruyants.&lt;/p&gt;
&lt;p&gt;Les avertissements critiques NVMe et les erreurs d’intégrité continuent donc d’alerter dès leur première occurrence. Les filesystems restent surveillés avec leurs seuils normaux. Lorsque le disque du runner manque réellement de place, la réponse attendue est un nettoyage ou une augmentation du volume, pas un seuil plus généreux.&lt;/p&gt;
&lt;p&gt;Même logique pour les TAP : j’ignore les interfaces éphémères, mais pas les uplinks. Pour la mémoire du runner, j’ajoute une confirmation, mais je ne supprime pas le contrôle. Pour le DNS, je détends seulement le seuil UDP ; le contrôle TCP conserve sa valeur plus stricte.&lt;/p&gt;
&lt;p&gt;Je me sers désormais de cette règle simple :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Si l’alerte représente un problème mesuré, je la garde. Si elle décrit un comportement attendu, je corrige son modèle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="des-règles-versionnées-plutôt-que-des-clics-oubliés"&gt;Des règles versionnées plutôt que des clics oubliés
&lt;/h2&gt;&lt;p&gt;Tous ces réglages sont décrits dans le dépôt d’infrastructure et appliqués par un helper qui utilise l’API REST de Checkmk. Cela me permet de relire une exception, de comprendre pourquoi elle existe et de la tester comme le reste du code.&lt;/p&gt;
&lt;p&gt;Pour appliquer les règles de calibration :&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-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 ansible/scripts/checkmk-rest-api.py calibrate-rules
&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;Je rejoue ensuite la commande sans activation :&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-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 ansible/scripts/checkmk-rest-api.py calibrate-rules --no-activate
&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 seconde exécution doit annoncer toutes les règles gérées comme inchangées. Pour les modifications de découverte, notamment les interfaces Proxmox, je relance aussi explicitement la découverte des services concernés :&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-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 ansible/scripts/checkmk-rest-api.py discover &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --host pve-dell &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --host pve-forum &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --host pve-fuji
&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;Les tests automatisés vérifient également la portée des règles : motif exact des TAP, deux tentatives pour la mémoire du runner et intervalle de 1 440 minutes pour les problèmes disque persistants.&lt;/p&gt;
&lt;p&gt;Enfin, je contrôle l’état effectif avec Livestatus, directement sur le serveur Checkmk. Lors de la dernière vérification, le service &lt;code&gt;Memory&lt;/code&gt; du runner était bien configuré avec deux tentatives maximales. Les deux boucles mail et les contrôles DNS étaient en état &lt;code&gt;OK&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Cette dernière étape est indispensable. Une configuration correcte dans Git ne prouve pas encore que Checkmk l’a activée ni qu’elle s’applique au bon service.&lt;/p&gt;
&lt;h2 id="ce-que-je-retiens"&gt;Ce que je retiens
&lt;/h2&gt;&lt;p&gt;Je pensais au départ qu’une bonne supervision devait tout remonter le plus vite possible. Je la vois maintenant comme un système de décisions : que faut-il conserver dans l’historique, que faut-il confirmer et qu’est-ce qui mérite réellement une interruption ?&lt;/p&gt;
&lt;p&gt;Les quatre leviers ne sont pas interchangeables :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ignorer précisément une ressource éphémère ;&lt;/li&gt;
&lt;li&gt;confirmer un état transitoire avec une nouvelle tentative ;&lt;/li&gt;
&lt;li&gt;espacer le rappel d’un problème déjà connu ;&lt;/li&gt;
&lt;li&gt;adapter un seuil à la mesure réellement effectuée.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Je garde volontairement le canal de réception en dehors de cet article. Je raconterai prochainement comment j’achemine ces alertes, où je les reçois et comment je décide lesquelles ont réellement le droit de m’interrompre.&lt;/p&gt;
&lt;p&gt;Depuis cette reprise, l’écran vert m’intéresse moins. Ce qui compte, c’est que la prochaine notification inhabituelle récupère immédiatement mon attention — et qu’elle la mérite.&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>