<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr"><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://50bvd.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://50bvd.com/" rel="alternate" type="text/html" hreflang="fr" /><updated>2026-08-09T11:33:07+02:00</updated><id>https://50bvd.com/feed.xml</id><title type="html">50bvd Guides IT</title><subtitle>Blog technique — Administration systèmes, infrastructure, homelab et open source.</subtitle><author><name>50bvd</name><email></email></author><entry><title type="html">Configurer Google Search Console et le SEO d’un blog Jekyll</title><link href="https://50bvd.com/2026/07/11/seo-jekyll-google-search-console/" rel="alternate" type="text/html" title="Configurer Google Search Console et le SEO d’un blog Jekyll" /><published>2026-07-11T00:00:00+02:00</published><updated>2026-07-11T00:00:00+02:00</updated><id>https://50bvd.com/2026/07/11/seo-jekyll-google-search-console</id><content type="html" xml:base="https://50bvd.com/2026/07/11/seo-jekyll-google-search-console/"><![CDATA[<p>Un site peut être parfaitement fonctionnel sans être référencé : sans <code class="language-plaintext highlighter-rouge">robots.txt</code>, sans sitemap déclaré et sans propriété vérifiée dans Google Search Console, les moteurs de recherche n’ont aucune garantie d’indexation fiable. Ce guide couvre la mise en place minimale mais complète pour un blog Jekyll.</p>

<h2 id="robotstxt">robots.txt</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User-agent: *
Allow: /

Sitemap: https://mondomaine.com/sitemap.xml
</code></pre></div></div>

<p>Un fichier statique à la racine du site suffit — pas besoin de plugin.</p>

<h2 id="sitemapxml-automatique">sitemap.xml automatique</h2>

<p>Le plugin officiel génère et maintient le sitemap sans configuration :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>
<span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-sitemap</span>
</code></pre></div></div>

<p>Jekyll expose alors automatiquement <code class="language-plaintext highlighter-rouge">/sitemap.xml</code>, régénéré à chaque build avec la liste à jour des articles et pages.</p>

<h2 id="meta-tags-seo-jekyll-seo-tag">Meta tags SEO (jekyll-seo-tag)</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">plugins</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">jekyll-seo-tag</span>
</code></pre></div></div>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">{%</span><span class="w"> </span><span class="nt">seo</span><span class="w"> </span><span class="cp">%}</span>
</code></pre></div></div>

<p>Ce tag unique génère titre, description, Open Graph, Twitter Card et la balise <code class="language-plaintext highlighter-rouge">generator</code> à partir des champs déjà présents dans <code class="language-plaintext highlighter-rouge">_config.yml</code> (<code class="language-plaintext highlighter-rouge">title</code>, <code class="language-plaintext highlighter-rouge">description</code>, <code class="language-plaintext highlighter-rouge">url</code>) et le front matter de chaque article (<code class="language-plaintext highlighter-rouge">description</code>, <code class="language-plaintext highlighter-rouge">image</code>).</p>

<h2 id="vérification-de-propriété-google-search-console">Vérification de propriété Google Search Console</h2>

<p>La méthode la plus simple pour un site statique est la balise meta, ajoutée directement dans le <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> du layout principal :</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"google-site-verification"</span> <span class="na">content=</span><span class="s">"VOTRE_CODE_ICI"</span> <span class="nt">/&gt;</span>
</code></pre></div></div>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Où trouver le code</div>
    <div class="callout-content"><p>Dans Search Console : Paramètres → Propriété → Vérification du domaine → méthode ‘Balise HTML’. Le code fourni va directement dans le content de la meta tag, pas ailleurs.</p>
</div>
  </div>
</div>

<h2 id="soumission-du-sitemap">Soumission du sitemap</h2>

<p>Une fois la propriété vérifiée dans Search Console : <strong>Sitemaps → Ajouter un sitemap</strong> → renseigner <code class="language-plaintext highlighter-rouge">sitemap.xml</code> (l’URL complète n’est pas nécessaire, juste le chemin relatif). Le statut passe à “Sitemap envoyé” puis, après quelques heures, “Réussite” avec le nombre de pages découvertes.</p>

<h2 id="vérification">Vérification</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> https://mondomaine.com/robots.txt
curl <span class="nt">-s</span> https://mondomaine.com/sitemap.xml | <span class="nb">head</span> <span class="nt">-20</span>
curl <span class="nt">-s</span> https://mondomaine.com/ | <span class="nb">grep </span>google-site-verification
</code></pre></div></div>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Suppression/recréation de propriété</div>
    <div class="callout-content"><p>Si vous supprimez puis recréez la propriété dans Search Console, un nouveau code de vérification est généré à chaque fois — l’ancienne balise meta ne fonctionnera plus, il faut la remplacer avant de relancer la vérification.</p>
</div>
  </div>
</div>

<h2 id="résultat">Résultat</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">robots.txt</code> en place, pointant vers le sitemap.</li>
  <li>Sitemap généré et tenu à jour automatiquement par Jekyll à chaque build.</li>
  <li>Propriété Search Console vérifiée et sitemap soumis — le site apparaît dans l’index Google sans étape manuelle récurrente.</li>
</ul>]]></content><author><name>50bvd</name></author><category term="Infrastructure" /><category term="SEO" /><category term="jekyll" /><category term="seo" /><category term="google-search-console" /><category term="sitemap" /><category term="robots-txt" /><summary type="html"><![CDATA[Mettre en place les fondamentaux SEO d'un blog Jekyll — robots.txt, sitemap.xml automatique, vérification Google Search Console et soumission du sitemap.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/seo-gsc-dark.svg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/seo-gsc-dark.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Ajouter un compteur de visiteurs GoatCounter à un site Jekyll sans requête réseau côté navigateur</title><link href="https://50bvd.com/2026/07/07/goatcounter-jekyll-build-time/" rel="alternate" type="text/html" title="Ajouter un compteur de visiteurs GoatCounter à un site Jekyll sans requête réseau côté navigateur" /><published>2026-07-07T00:00:00+02:00</published><updated>2026-07-07T00:00:00+02:00</updated><id>https://50bvd.com/2026/07/07/goatcounter-jekyll-build-time</id><content type="html" xml:base="https://50bvd.com/2026/07/07/goatcounter-jekyll-build-time/"><![CDATA[<p>GoatCounter est une alternative légère et respectueuse de la vie privée à Google Analytics. Afficher un simple compteur “visiteurs / mois” sur la page d’accueil semble trivial — en pratique, chaque approche côté navigateur se heurte à un blocage différent. Ce guide documente le problème et la solution qui fonctionne réellement : tout récupérer côté serveur, au moment du build.</p>

<h2 id="ce-qui-ne-fonctionne-pas">Ce qui ne fonctionne pas</h2>

<p><strong>L’API stats officielle</strong> (<code class="language-plaintext highlighter-rouge">/api/v0/stats/total</code>) exige systématiquement un token Bearer, même avec le dashboard configuré en public :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> https://MONCODE.goatcounter.com/api/v0/stats/total
<span class="c"># → {"error":"no Authorization header"}</span>
</code></pre></div></div>

<p><strong>Le endpoint public <code class="language-plaintext highlighter-rouge">/counter/*.json</code></strong>, pensé pour être embarqué sans token, fonctionne en <code class="language-plaintext highlighter-rouge">curl</code> mais casse en <code class="language-plaintext highlighter-rouge">fetch()</code> depuis le navigateur :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Access to fetch at 'https://moncode.goatcounter.com/counter//.json?...'
from origin 'https://mondomaine.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present
</code></pre></div></div>

<p><strong>La variante <code class="language-plaintext highlighter-rouge">&lt;img&gt;</code> avec <code class="language-plaintext highlighter-rouge">/counter/*.svg</code></strong> contourne en théorie le CORS (une image n’y est pas soumise) — mais échoue quand même avec un <code class="language-plaintext highlighter-rouge">499</code>/<code class="language-plaintext highlighter-rouge">net::ERR_FAILED</code>, reproductible aussi bien sur PC que sur mobile, sans lien avec un bloqueur de pub local.</p>

<h2 id="la-solution--fetch-côté-serveur-au-build">La solution : fetch côté serveur, au build</h2>

<p>Plutôt que de faire confiance au navigateur du visiteur, un plugin Jekyll récupère le chiffre une seule fois, pendant <code class="language-plaintext highlighter-rouge">jekyll build</code>/<code class="language-plaintext highlighter-rouge">jekyll serve</code>, et l’injecte directement dans le HTML statique généré — plus aucune requête réseau au chargement de la page.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _plugins/goatcounter_stats.rb</span>
<span class="nb">require</span> <span class="s2">"net/http"</span>
<span class="nb">require</span> <span class="s2">"json"</span>
<span class="nb">require</span> <span class="s2">"date"</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:after_reset</span> <span class="k">do</span> <span class="o">|</span><span class="n">site</span><span class="o">|</span>
  <span class="n">code</span> <span class="o">=</span> <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="s2">"analytics"</span><span class="p">,</span> <span class="s2">"goatcounter_code"</span><span class="p">)</span>
  <span class="k">next</span> <span class="k">if</span> <span class="n">code</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">code</span><span class="p">.</span><span class="nf">empty?</span>

  <span class="k">begin</span>
    <span class="n">end_date</span>   <span class="o">=</span> <span class="no">Date</span><span class="p">.</span><span class="nf">today</span>
    <span class="n">start_date</span> <span class="o">=</span> <span class="n">end_date</span> <span class="o">-</span> <span class="mi">30</span>
    <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">(</span><span class="s2">"https://</span><span class="si">#{</span><span class="n">code</span><span class="si">}</span><span class="s2">.goatcounter.com/counter//.json?start=</span><span class="si">#{</span><span class="n">start_date</span><span class="si">}</span><span class="s2">&amp;end=</span><span class="si">#{</span><span class="n">end_date</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

    <span class="no">Net</span><span class="o">::</span><span class="no">HTTP</span><span class="p">.</span><span class="nf">start</span><span class="p">(</span><span class="n">uri</span><span class="p">.</span><span class="nf">host</span><span class="p">,</span> <span class="n">uri</span><span class="p">.</span><span class="nf">port</span><span class="p">,</span> <span class="ss">use_ssl: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">open_timeout: </span><span class="mi">5</span><span class="p">,</span> <span class="ss">read_timeout: </span><span class="mi">5</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">http</span><span class="o">|</span>
      <span class="n">res</span> <span class="o">=</span> <span class="n">http</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">uri</span><span class="p">.</span><span class="nf">request_uri</span><span class="p">)</span>
      <span class="k">if</span> <span class="n">res</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Net</span><span class="o">::</span><span class="no">HTTPSuccess</span><span class="p">)</span>
        <span class="n">data</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">res</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span>
        <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s2">"goatcounter_visitors"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">"count_unique"</span><span class="p">]</span> <span class="o">||</span> <span class="n">data</span><span class="p">[</span><span class="s2">"count"</span><span class="p">]</span> <span class="o">||</span> <span class="s2">"0"</span><span class="p">).</span><span class="nf">to_i</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=&gt;</span> <span class="n">e</span>
    <span class="no">Jekyll</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">warn</span> <span class="s2">"goatcounter_stats:"</span><span class="p">,</span> <span class="s2">"fetch failed (</span><span class="si">#{</span><span class="n">e</span><span class="p">.</span><span class="nf">message</span><span class="si">}</span><span class="s2">)"</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">ca-certificates sur Alpine</div>
    <div class="callout-content"><p>Une image ruby:3.3-alpine ne contient pas les certificats CA par défaut — Net::HTTP en HTTPS échoue silencieusement (ou plante le process Jekyll au démarrage) tant que le paquet ca-certificates n’est pas installé dans le Dockerfile.</p>
</div>
  </div>
</div>

<h2 id="affichage-côté-template">Affichage côté template</h2>

<p>Le widget se contente de lire une valeur déjà calculée — aucun script, aucun appel réseau côté visiteur :</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;div class="stat-card"&gt;
  <span class="cp">{%</span><span class="w"> </span><span class="nt">if</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">goatcounter_visitors</span><span class="w"> </span><span class="cp">%}</span>
    <span class="cp">{%</span><span class="w"> </span><span class="nt">if</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">goatcounter_visitors</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="cp">%}</span>
      &lt;div class="stat-value"&gt;<span class="cp">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">goatcounter_visitors</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">divided_by</span><span class="p">:</span><span class="w"> </span><span class="mf">1000.0</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">round</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="cp">}}</span>k&lt;/div&gt;
    <span class="cp">{%</span><span class="w"> </span><span class="nt">else</span><span class="w"> </span><span class="cp">%}</span>
      &lt;div class="stat-value"&gt;<span class="cp">{{</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">goatcounter_visitors</span><span class="w"> </span><span class="cp">}}</span>&lt;/div&gt;
    <span class="cp">{%</span><span class="w"> </span><span class="nt">endif</span><span class="w"> </span><span class="cp">%}</span>
  <span class="cp">{%</span><span class="w"> </span><span class="nt">else</span><span class="w"> </span><span class="cp">%}</span>
    &lt;div class="stat-value"&gt;—&lt;/div&gt;
  <span class="cp">{%</span><span class="w"> </span><span class="nt">endif</span><span class="w"> </span><span class="cp">%}</span>
  &lt;div class="stat-label"&gt;Visitors / month&lt;/div&gt;
&lt;/div&gt;
</code></pre></div></div>

<h2 id="configuration">Configuration</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># _config.yml</span>
<span class="na">analytics</span><span class="pi">:</span>
  <span class="na">goatcounter_code</span><span class="pi">:</span> <span class="s2">"</span><span class="s">moncode"</span>
</code></pre></div></div>

<h2 id="vérification">Vérification</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nt">-f</span> docker-compose.public.yml up <span class="nt">-d</span> <span class="nt">--build</span>
docker logs infops-public | <span class="nb">grep</span> <span class="nt">-i</span> goatcounter   <span class="c"># aucune erreur = OK</span>

curl <span class="nt">-s</span> http://localhost:4000/ | <span class="nb">grep</span> <span class="nt">-A3</span> <span class="s2">"Visitors / month"</span>
<span class="c"># → le chiffre doit apparaître directement dans le HTML, pas de "—"</span>
</code></pre></div></div>

<h2 id="résultat">Résultat</h2>

<ul>
  <li>Aucun appel réseau côté navigateur : ni CORS, ni bloqueur, ni 499 possible.</li>
  <li>Le chiffre est déjà présent dans le HTML au moment où la page arrive chez le visiteur.</li>
  <li>Contrepartie assumée : la valeur ne se met à jour qu’au prochain build/rechargement, pas en temps réel — largement suffisant pour un compteur mensuel.</li>
</ul>]]></content><author><name>50bvd</name></author><category term="Infrastructure" /><category term="Analytics" /><category term="jekyll" /><category term="goatcounter" /><category term="ruby" /><category term="analytics" /><category term="ca-certificates" /><summary type="html"><![CDATA[Contourner les blocages CORS/bloqueurs sur les endpoints GoatCounter en récupérant le nombre de visiteurs côté serveur, au moment du build Jekyll, plutôt que depuis le navigateur du visiteur.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/goatcounter-dark.svg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/goatcounter-dark.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">HAProxy — Masquer les headers serveur et servir des pages d’erreur personnalisées multi-backends</title><link href="https://50bvd.com/2026/07/03/haproxy-masquage-headers-pages-erreur/" rel="alternate" type="text/html" title="HAProxy — Masquer les headers serveur et servir des pages d’erreur personnalisées multi-backends" /><published>2026-07-03T00:00:00+02:00</published><updated>2026-07-03T00:00:00+02:00</updated><id>https://50bvd.com/2026/07/03/haproxy-masquage-headers-pages-erreur</id><content type="html" xml:base="https://50bvd.com/2026/07/03/haproxy-masquage-headers-pages-erreur/"><![CDATA[<p>Un reverse proxy HAProxy qui expose plusieurs services internes (panel, stockage, jeux, monitoring…) pose deux problèmes récurrents : certains services révèlent trop d’informations dans leurs headers HTTP, et les pages d’erreur par défaut ne sont jamais cohérentes d’un service à l’autre. Ce guide couvre les deux.</p>

<h2 id="masquer-les-headers-serveur-par-domaine">Masquer les headers serveur par domaine</h2>

<p>Objectif : cacher <code class="language-plaintext highlighter-rouge">Server</code>/<code class="language-plaintext highlighter-rouge">X-Powered-By</code> pour tous les domaines internes, sauf le site vitrine où on assume volontairement la stack technique.</p>

<p>Premier réflexe naturel — et premier piège :</p>

<pre><code class="language-haproxy">acl is_main_domain hdr(host) -i 50bvd.com
http-response del-header Server if !is_main_domain
</code></pre>

<p>En contexte <code class="language-plaintext highlighter-rouge">http-response</code>, <code class="language-plaintext highlighter-rouge">hdr(host)</code> n’est plus valable : ce fetch n’existe que côté requête. HAProxy refuse la règle avec <em>“will never match because it only involves keywords that are incompatible with ‘frontend http-response header rule’“</em>.</p>

<p>La solution consiste à capturer le host dès la requête dans une variable de transaction, puis à la relire côté réponse :</p>

<pre><code class="language-haproxy">http-request set-var(txn.host) req.hdr(host)
acl is_main_domain var(txn.host) -m str -i 50bvd.com

http-response del-header Server     if !is_main_domain
http-response del-header X-Powered-By if !is_main_domain
</code></pre>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">-m str obligatoire</div>
    <div class="callout-content"><p>Une ACL basée sur var() n’a pas de type implicite — sans -m str, HAProxy refuse de démarrer avec ‘matching method must be specified first’.</p>
</div>
  </div>
</div>

<h2 id="pages-derreur-personnalisées-sur-tous-les-backends">Pages d’erreur personnalisées sur tous les backends</h2>

<p>Les directives <code class="language-plaintext highlighter-rouge">errorfile</code> classiques (dans <code class="language-plaintext highlighter-rouge">defaults</code>) ne couvrent que les erreurs générées par HAProxy lui-même (backend injoignable → 502/503/504, requête malformée → 400). Elles n’interceptent jamais un 404 ou un 500 renvoyé par un backend qui répond normalement.</p>

<p>Pour remplacer systématiquement n’importe quel code d’erreur — qu’il vienne d’un backend sain ou d’une panne détectée par HAProxy — sur <code class="language-plaintext highlighter-rouge">frontend https_front</code> :</p>

<pre><code class="language-haproxy">http-response return status 404 content-type text/html \
    file /etc/haproxy/errors/404.http \
    hdr X-Frame-Options "SAMEORIGIN" \
    hdr X-Content-Type-Options "nosniff" \
    hdr Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" \
    hdr Referrer-Policy "strict-origin-when-cross-origin" \
    if { status 404 }
</code></pre>

<p>À répéter pour chaque code voulu (400, 403, 404, 408, 500, 502, 503, 504).</p>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">return remplace toute la réponse</div>
    <div class="callout-content"><p>http-response return écrase entièrement la réponse d’origine, y compris les en-têtes de sécurité ajoutés par des règles précédentes — il faut les réinjecter directement sur la ligne return via hdr.</p>
</div>
  </div>
</div>

<h2 id="limite-connue--les-spa-qui-répondent-toujours-200">Limite connue : les SPA qui répondent toujours 200</h2>

<p>Un panel type Pterodactyl gère ses routes côté client (React/Vue) : une URL inexistante dans l’app renvoie quand même un HTTP 200 côté serveur, avec un “not found” purement visuel. Aucune règle HAProxy ne peut intercepter ce cas — seules les vraies pannes (backend injoignable, timeout) restent couvertes.</p>

<h2 id="vérification">Vérification</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>haproxy <span class="nt">-c</span> <span class="nt">-f</span> /etc/haproxy/haproxy.cfg
rcctl reload haproxy   <span class="c"># ou rcctl restart haproxy si reload insuffisant</span>

<span class="c"># Header masqué sur un service interne</span>
curl <span class="nt">-sI</span> https://portainer.exemple.com/ | <span class="nb">grep</span> <span class="nt">-i</span> server

<span class="c"># Header conservé sur le site vitrine</span>
curl <span class="nt">-sI</span> https://50bvd.com/ | <span class="nb">grep</span> <span class="nt">-i</span> server

<span class="c"># Page d'erreur personnalisée sur une route inexistante</span>
curl <span class="nt">-s</span> https://50bvd.com/route-qui-nexiste-pas/ | <span class="nb">head</span> <span class="nt">-c</span> 200
</code></pre></div></div>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">ksh, pas bash</div>
    <div class="callout-content"><table>
  <tbody>
    <tr>
      <td>Sur OpenBSD, HAProxy tourne via rcctl et le shell par défaut est ksh — les astuces bash (sed -i sans argument, systemctl…) ne s’appliquent pas telles quelles. sed nécessite un argument vide explicite : sed -i ‘’ ‘s</td>
      <td>…</td>
      <td>…</td>
      <td>’.</td>
    </tr>
  </tbody>
</table>
</div>
  </div>
</div>

<h2 id="résultat">Résultat</h2>

<ul>
  <li>Headers <code class="language-plaintext highlighter-rouge">Server</code>/<code class="language-plaintext highlighter-rouge">X-Powered-By</code> masqués sur tous les domaines internes, conservés uniquement sur le domaine choisi.</li>
  <li>Une seule page d’erreur cohérente et personnalisée, appliquée uniformément sur tous les backends et tous les codes d’erreur.</li>
</ul>]]></content><author><name>50bvd</name></author><category term="Infrastructure" /><category term="Réseau" /><category term="haproxy" /><category term="reverse-proxy" /><category term="sécurité" /><category term="headers" /><category term="error-pages" /><summary type="html"><![CDATA[Masquer sélectivement les headers Server/X-Powered-By par domaine et remplacer les pages d'erreur HAProxy pour tous les backends, pas seulement en cas de panne détectée par HAProxy lui-même.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/haproxy-headers-dark.svg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/haproxy-headers-dark.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Déployer un blog Jekyll en production avec Docker et HAProxy</title><link href="https://50bvd.com/2026/06/30/deployer-jekyll-docker-haproxy/" rel="alternate" type="text/html" title="Déployer un blog Jekyll en production avec Docker et HAProxy" /><published>2026-06-30T00:00:00+02:00</published><updated>2026-06-30T00:00:00+02:00</updated><id>https://50bvd.com/2026/06/30/deployer-jekyll-docker-haproxy</id><content type="html" xml:base="https://50bvd.com/2026/06/30/deployer-jekyll-docker-haproxy/"><![CDATA[<p>Un blog Jekyll classique se déploie généralement en générant du HTML statique (<code class="language-plaintext highlighter-rouge">jekyll build</code>) servi ensuite par nginx ou Apache. Cette approche est la plus performante, mais elle impose un cycle build → copie des fichiers → redéploiement à chaque modification. Pour un blog personnel à faible trafic, ce guide propose une alternative volontairement plus simple : faire tourner le serveur <code class="language-plaintext highlighter-rouge">jekyll serve</code> (WEBrick) directement en conteneur, avec les sources montées en volume, reverse-proxifié par HAProxy.</p>

<h2 id="pourquoi-pas-un-build-statique-classique-">Pourquoi pas un build statique classique ?</h2>

<p>Un pipeline build + nginx est plus rapide à servir, mais chaque modification de contenu impose de reconstruire l’image ou de resynchroniser un dossier de sortie. Pour un site de quelques articles, non critique, on peut accepter un serveur un peu moins rapide en échange d’une simplicité totale : modifier un fichier sur le disque suffit, Jekyll régénère le site tout seul.</p>

<h2 id="dockerfile">Dockerfile</h2>

<p>Deux stages : <code class="language-plaintext highlighter-rouge">base</code> installe les dépendances Ruby, <code class="language-plaintext highlighter-rouge">dev</code> lance le serveur. Ce même stage sert aussi bien en développement local qu’en “production” — seule la commande de lancement change entre les deux <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">ruby:3.3-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span>

<span class="k">RUN </span>apk add <span class="nt">--no-cache</span> <span class="se">\
</span>      build-base git nodejs npm tzdata <span class="se">\
</span>      libffi-dev yaml-dev zlib-dev <span class="se">\
</span>      ca-certificates

<span class="k">WORKDIR</span><span class="s"> /site</span>
<span class="k">COPY</span><span class="s"> Gemfile Gemfile.lock ./</span>
<span class="k">RUN </span>bundle <span class="nb">install</span> <span class="nt">--jobs</span> 4 <span class="nt">--retry</span> 3

<span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">dev</span>
<span class="k">WORKDIR</span><span class="s"> /site</span>
<span class="k">EXPOSE</span><span class="s"> 4000 35729</span>
<span class="k">CMD</span><span class="s"> ["bundle", "exec", "jekyll", "serve", \</span>
     "--host", "0.0.0.0", \
     "--port", "4000", \
     "--livereload", \
     "--future", \
     "--drafts"]
</code></pre></div></div>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">ca-certificates</div>
    <div class="callout-content"><p>Sur Alpine, le paquet ca-certificates n’est pas installé par défaut. Sans lui, tout appel HTTPS sortant depuis un plugin Jekyll (Net::HTTP, API externe…) échoue avec une erreur SSL.</p>
</div>
  </div>
</div>

<h2 id="docker-composepublicyml--la-version--production-">docker-compose.public.yml — la version « production »</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">jekyll-public</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span>
      <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
      <span class="na">target</span><span class="pi">:</span> <span class="s">dev</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">infops-public</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">.:/site</span>
      <span class="pi">-</span> <span class="s">gem_cache:/usr/local/bundle</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">&gt;</span>
      <span class="s">bundle exec jekyll serve</span>
      <span class="s">--host 0.0.0.0</span>
      <span class="s">--port 4000</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">4000:4000"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">JEKYLL_ENV=production</span>
      <span class="pi">-</span> <span class="s">TZ=Europe/Paris</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">gem_cache</span><span class="pi">:</span>
</code></pre></div></div>

<p>Deux différences avec le compose de développement : pas de <code class="language-plaintext highlighter-rouge">--drafts</code>/<code class="language-plaintext highlighter-rouge">--future</code> (on ne publie que les articles réellement datés et publiés), et <code class="language-plaintext highlighter-rouge">JEKYLL_ENV=production</code> pour activer les blocs conditionnels (<code class="language-plaintext highlighter-rouge">{% if jekyll.environment == "production" %}</code>) comme le chargement des scripts d’analytics.</p>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">--incremental à éviter</div>
    <div class="callout-content"><p>Le flag –incremental de Jekyll a un bug connu : il ne recharge pas toujours correctement les layouts/includes modifiés. Sur un site qui tourne en continu avec du contenu qui change souvent, mieux vaut laisser Jekyll régénérer intégralement à chaque changement détecté.</p>
</div>
  </div>
</div>

<h2 id="reverse-proxy-haproxy">Reverse proxy HAProxy</h2>

<p>HAProxy termine le TLS et route le trafic vers le conteneur WEBrick en HTTP simple :</p>

<pre><code class="language-haproxy">frontend https_front
    bind *:443 ssl crt /etc/haproxy/combined.pem alpn h2,http/1.1

    acl is_main_domain hdr(host) -i 50bvd.com
    use_backend main_website if is_main_domain

backend main_website
    option http-server-close
    option forwardfor
    option httpchk GET /
    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }
    timeout tunnel 1h
    server webserver 192.168.1.12:4000 check
</code></pre>

<h2 id="lancer-et-vérifier">Lancer et vérifier</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nt">-f</span> docker-compose.public.yml up <span class="nt">-d</span> <span class="nt">--build</span>

<span class="c"># Depuis srv-docker</span>
curl <span class="nt">-sI</span> http://localhost:4000/
<span class="c"># → HTTP/1.1 200 OK, Server: WEBrick/1.9.2</span>

<span class="c"># Depuis l'extérieur, via HAProxy</span>
curl <span class="nt">-sI</span> https://50bvd.com/
</code></pre></div></div>

<p>Toute modification sur le disque (article, layout, style) est reprise automatiquement en quelques secondes — pas de build ni de redéploiement séparé à orchestrer.</p>

<h2 id="résultat">Résultat</h2>

<ul>
  <li>Site en ligne derrière TLS via HAProxy, sans étape de build manuelle.</li>
  <li>Modification d’un fichier = site à jour en quelques secondes.</li>
  <li>Compromis assumé : un peu moins rapide qu’un site statique nginx, largement suffisant pour un blog personnel à faible trafic.</li>
</ul>]]></content><author><name>50bvd</name></author><category term="Infrastructure" /><category term="Docker" /><category term="jekyll" /><category term="docker" /><category term="haproxy" /><category term="webrick" /><category term="deploiement" /><category term="reverse-proxy" /><summary type="html"><![CDATA[Déployer un site Jekyll en production avec son propre serveur WEBrick dans un conteneur Docker, reverse-proxifié par HAProxy — sans étape de build séparée.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/jekyll-deploy-dark.svg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/jekyll-deploy-dark.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Migrer Docker de RHEL 8 vers Debian 13 — Guide complet</title><link href="https://50bvd.com/2026/06/28/migration-docker-rhel-debian13/" rel="alternate" type="text/html" title="Migrer Docker de RHEL 8 vers Debian 13 — Guide complet" /><published>2026-06-28T00:00:00+02:00</published><updated>2026-06-28T00:00:00+02:00</updated><id>https://50bvd.com/2026/06/28/migration-docker-rhel-debian13</id><content type="html" xml:base="https://50bvd.com/2026/06/28/migration-docker-rhel-debian13/"><![CDATA[<p>RHEL 8 a atteint sa fin de support par Docker à partir de la version 29. Si votre infrastructure tourne encore sur RHEL 8 avec Docker, voici comment migrer proprement vers Debian 13 sans coupure de service prolongée.</p>

<h2 id="pourquoi-migrer-">Pourquoi migrer ?</h2>

<ul>
  <li>Docker 28 est la version maximale supportée sur RHEL 8</li>
  <li>Docker 29+ nécessite RHEL 9 ou une distribution alternative</li>
  <li>Debian 13 (Trixie) supporte Docker 29+ nativement</li>
  <li>Moins de contraintes de licence que RHEL</li>
</ul>

<h2 id="inventaire-avant-migration">Inventaire avant migration</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Lister les conteneurs</span>
docker ps <span class="nt">-a</span> <span class="nt">--format</span> <span class="s2">"table {{.Names}}</span><span class="se">\t</span><span class="s2">{{.Image}}</span><span class="se">\t</span><span class="s2">{{.Status}}</span><span class="se">\t</span><span class="s2">{{.Ports}}"</span>

<span class="c"># Lister les volumes</span>
docker volume <span class="nb">ls</span>

<span class="c"># Identifier les bind mounts (données hors volumes Docker)</span>
docker inspect <span class="si">$(</span>docker ps <span class="nt">-aq</span><span class="si">)</span> <span class="se">\</span>
  <span class="nt">--format</span> <span class="s1">'{{.Name}}: {{range .Mounts}}{{.Type}}:{{.Source}}→{{.Destination}} {{end}}'</span>
</code></pre></div></div>

<p><strong>Exemple de stack à migrer :</strong></p>

<table>
  <thead>
    <tr>
      <th>Conteneur</th>
      <th>Données persistantes</th>
      <th>Méthode</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>portainer</td>
      <td>Volume <code class="language-plaintext highlighter-rouge">portainer_data</code></td>
      <td>Export tar</td>
    </tr>
    <tr>
      <td>app-custom</td>
      <td>Build local + .env</td>
      <td>Copier sources + rebuild</td>
    </tr>
    <tr>
      <td>stateless-tools</td>
      <td>Aucune</td>
      <td>Re-pull image</td>
    </tr>
    <tr>
      <td>site-web</td>
      <td>Bind mount <code class="language-plaintext highlighter-rouge">/root/site/</code></td>
      <td>Copier dans l’archive home</td>
    </tr>
  </tbody>
</table>

<h2 id="étape-1--sauvegarder">Étape 1 — Sauvegarder</h2>

<h3 id="archive-du-home-bind-mounts-inclus">Archive du home (bind mounts inclus)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Exclure les caches volumineux et régénérables</span>
<span class="nb">tar </span>czf /tmp/home-backup.tar.gz <span class="se">\</span>
  <span class="nt">--exclude</span><span class="o">=</span>/root/jellyfin/cache <span class="se">\</span>
  <span class="nt">--exclude</span><span class="o">=</span>/root/.cache <span class="se">\</span>
  /root

<span class="nb">ls</span> <span class="nt">-lh</span> /tmp/home-backup.tar.gz
</code></pre></div></div>

<h3 id="exporter-les-volumes-docker">Exporter les volumes Docker</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Pour chaque volume nommé</span>
<span class="k">for </span>vol <span class="k">in</span> <span class="si">$(</span>docker volume <span class="nb">ls</span> <span class="nt">-q</span><span class="si">)</span><span class="p">;</span> <span class="k">do
  </span><span class="nb">echo</span> <span class="s2">"Exporting </span><span class="nv">$vol</span><span class="s2">..."</span>
  docker run <span class="nt">--rm</span> <span class="se">\</span>
    <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">vol</span><span class="k">}</span><span class="s2">:/data"</span> <span class="se">\</span>
    <span class="nt">-v</span> /tmp:/backup <span class="se">\</span>
    alpine <span class="nb">tar </span>czf <span class="s2">"/backup/</span><span class="k">${</span><span class="nv">vol</span><span class="k">}</span><span class="s2">.tar.gz"</span> /data
<span class="k">done

</span><span class="nb">ls</span> <span class="nt">-lh</span> /tmp/<span class="k">*</span>.tar.gz
</code></pre></div></div>

<h2 id="étape-2--préparer-la-vm-debian-13">Étape 2 — Préparer la VM Debian 13</h2>

<p>Créez une nouvelle VM (Proxmox ou autre) avec Debian 13 Netinstall, 50 Go de disque.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Mise à jour système</span>
apt update <span class="o">&amp;&amp;</span> apt upgrade <span class="nt">-y</span>

<span class="c"># Outils essentiels</span>
apt <span class="nb">install</span> <span class="nt">-y</span> curl wget git vim <span class="nb">sudo </span>openssh-server netcat-openbsd

<span class="c"># Docker via le script officiel</span>
curl <span class="nt">-fsSL</span> https://get.docker.com | sh

<span class="c"># Vérifier</span>
docker version
<span class="c"># → Client/Server: Docker Engine 29.x.x</span>
</code></pre></div></div>

<h2 id="étape-3--transférer-les-données">Étape 3 — Transférer les données</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Depuis la VM Debian — récupérer les archives</span>
scp root@ANCIEN-IP:/tmp/home-backup.tar.gz /tmp/
scp root@ANCIEN-IP:/tmp/portainer_data.tar.gz /tmp/

<span class="c"># Extraire le home</span>
<span class="nb">tar </span>xzf /tmp/home-backup.tar.gz <span class="nt">-C</span> /

<span class="c"># Importer le volume Portainer</span>
docker volume create portainer_data
docker run <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">-v</span> portainer_data:/data <span class="se">\</span>
  <span class="nt">-v</span> /tmp:/backup <span class="se">\</span>
  alpine <span class="nb">tar </span>xzf /backup/portainer_data.tar.gz <span class="nt">-C</span> /

<span class="c"># Vérifier</span>
docker run <span class="nt">--rm</span> <span class="nt">-v</span> portainer_data:/data alpine <span class="nb">ls</span> /data
<span class="c"># → backups bin certs portainer.db ...</span>
</code></pre></div></div>

<h2 id="étape-4--recréer-les-conteneurs">Étape 4 — Recréer les conteneurs</h2>

<h3 id="portainer-sans-docker-swarm">Portainer (sans Docker Swarm)</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># /root/portainer-compose.yml</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">portainer</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">portainer/portainer-ee:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">portainer</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8000:8000"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">9443:9443"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">portainer_data:/data</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>

  <span class="na">portainer_agent</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">portainer/agent:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">portainer_agent</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">9001:9001"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
      <span class="pi">-</span> <span class="s">/var/lib/docker/volumes:/var/lib/docker/volumes</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">portainer_data</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker compose <span class="nt">-f</span> /root/portainer-compose.yml up <span class="nt">-d</span>
</code></pre></div></div>

<h3 id="conteneurs-stateless">Conteneurs stateless</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Pas de données à migrer — juste re-pull et recréer</span>
docker run <span class="nt">-d</span> <span class="nt">--name</span> it-tools <span class="nt">--restart</span> always <span class="nt">-p</span> 8080:80 corentinth/it-tools
docker run <span class="nt">-d</span> <span class="nt">--name</span> srv-kms  <span class="nt">--restart</span> always <span class="nt">-p</span> 1688:1688 mikolatero/vlmcsd
</code></pre></div></div>

<h3 id="conteneur-avec-build-local">Conteneur avec build local</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /root/mon-app
docker compose up <span class="nt">-d</span> <span class="nt">--build</span>
</code></pre></div></div>

<h2 id="étape-5--bascule-dip">Étape 5 — Bascule d’IP</h2>

<p>Une fois tous les conteneurs vérifiés sur la nouvelle VM :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Arrêter l'ancienne VM (ne pas la supprimer immédiatement)</span>
<span class="c"># 2. Assigner la même IP à la nouvelle VM (DHCP statique ou config réseau)</span>

<span class="c"># Sur Debian 13 — modifier l'IP</span>
nano /etc/network/interfaces
<span class="c"># Changer l'adresse IP</span>

systemctl restart networking

<span class="c"># 3. Vérifier la connectivité</span>
ip a
ping 8.8.8.8
</code></pre></div></div>

<h2 id="étape-6--mettre-à-jour-haproxy-si-nécessaire">Étape 6 — Mettre à jour HAProxy si nécessaire</h2>

<p>Si les backends pointent vers une IP qui a changé :</p>

<pre><code class="language-haproxy"># haproxy.cfg — mettre à jour l'IP des backends
backend mon_backend
    server app NOUVELLE-IP:PORT check
</code></pre>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>haproxy <span class="nt">-c</span> <span class="nt">-f</span> /etc/haproxy/haproxy.cfg <span class="o">&amp;&amp;</span> systemctl reload haproxy
</code></pre></div></div>

<h2 id="vérification-finale">Vérification finale</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Tous les conteneurs doivent être Up</span>
docker ps <span class="nt">--format</span> <span class="s2">"table {{.Names}}</span><span class="se">\t</span><span class="s2">{{.Status}}</span><span class="se">\t</span><span class="s2">{{.Ports}}"</span>

<span class="c"># Tester chaque service</span>
curl <span class="nt">-sk</span> https://mon-service.domaine.com <span class="nt">-o</span> /dev/null <span class="nt">-w</span> <span class="s2">"%{http_code}"</span>
<span class="c"># → 200</span>
</code></pre></div></div>

<h2 id="rollback">Rollback</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Si problème — rallumer l'ancienne VM</span>
<span class="c"># (gardez-la éteinte 48h avant suppression définitive)</span>
<span class="c"># Remettre l'IP sur l'ancienne VM</span>
<span class="c"># La nouvelle VM reprend son IP d'origine</span>
</code></pre></div></div>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Zero downtime</div>
    <div class="callout-content"><p>En configurant la nouvelle VM sur une IP temporaire et en basculant l’IP uniquement une fois la migration validée, le downtime se réduit au temps du redémarrage réseau (~30 secondes).</p>
</div>
  </div>
</div>

<h2 id="conclusion">Conclusion</h2>

<p>La migration RHEL 8 → Debian 13 s’effectue proprement en moins de 2 heures pour une stack standard. La clé : sauvegarder les volumes Docker correctement et ne supprimer l’ancienne VM qu’après 48h de validation en production.</p>]]></content><author><name>50bvd</name></author><category term="Infrastructure" /><category term="Docker" /><category term="docker" /><category term="debian" /><category term="rhel" /><category term="migration" /><category term="portainer" /><category term="linux" /><category term="conteneurs" /><summary type="html"><![CDATA[Guide pas à pas pour migrer une stack Docker complète de RHEL 8 vers Debian 13 — sauvegarde des volumes, transfert des données, recréation des conteneurs et bascule d'IP.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/docker-cover.jpg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/docker-cover.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Pterodactyl Panel + Wings derrière HAProxy — Port 443 depuis partout</title><link href="https://50bvd.com/2026/06/01/pterodactyl-haproxy-wings-port-443/" rel="alternate" type="text/html" title="Pterodactyl Panel + Wings derrière HAProxy — Port 443 depuis partout" /><published>2026-06-01T00:00:00+02:00</published><updated>2026-06-01T00:00:00+02:00</updated><id>https://50bvd.com/2026/06/01/pterodactyl-haproxy-wings-port-443</id><content type="html" xml:base="https://50bvd.com/2026/06/01/pterodactyl-haproxy-wings-port-443/"><![CDATA[<p>Pterodactyl est un panel open source de gestion de serveurs de jeu. Le problème classique : Wings (le daemon) écoute par défaut sur le port 8080, souvent bloqué dans les réseaux d’entreprise. Solution : passer Wings derrière HAProxy sur le port 443.</p>

<h2 id="problème">Problème</h2>

<p>Le panel Pterodactyl contacte le daemon Wings via le navigateur du client :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Navigateur → node.domaine.com:8080 → Wings
</code></pre></div></div>

<p>Depuis un réseau d’entreprise, le port 8080 est bloqué par le pare-feu. Le node apparaît en rouge dans le panel même si Wings fonctionne parfaitement en local.</p>

<h2 id="architecture-cible">Architecture cible</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Internet (port 443 uniquement)
        │
        ▼
HAProxy — TLS termination
        │
        ├── pterodactyl.domaine.com:443 → Panel (nginx:443)
        └── node.domaine.com:443        → Wings (HTTP:8080)

# Wings écoute en HTTP sur 8080 — HAProxy gère le TLS
</code></pre></div></div>

<h2 id="installation-debian-13">Installation Debian 13</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># PHP 8.3 via sury.org</span>
curl <span class="nt">-sSLo</span> /usr/share/keyrings/deb.sury.org-php.gpg <span class="se">\</span>
  https://packages.sury.org/php/apt.gpg
<span class="nb">echo</span> <span class="s2">"deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] </span><span class="se">\</span><span class="s2">
  https://packages.sury.org/php/ trixie main"</span> <span class="se">\</span>
  <span class="o">&gt;</span> /etc/apt/sources.list.d/php.list

apt update <span class="o">&amp;&amp;</span> apt <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
  php8.3 php8.3-<span class="o">{</span>cli,fpm,mysql,mbstring,bcmath,xml,curl,zip,gd,tokenizer<span class="o">}</span> <span class="se">\</span>
  nginx mariadb-client certbot python3-certbot-nginx
</code></pre></div></div>

<h2 id="installation-du-panel">Installation du Panel</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /var/www/pterodactyl <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /var/www/pterodactyl
curl <span class="nt">-Lo</span> panel.tar.gz <span class="se">\</span>
  https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz
<span class="nb">tar</span> <span class="nt">-xzf</span> panel.tar.gz
<span class="nb">chmod</span> <span class="nt">-R</span> 755 storage/<span class="k">*</span> bootstrap/cache/

composer <span class="nb">install</span> <span class="nt">--no-dev</span> <span class="nt">--optimize-autoloader</span>
<span class="nb">cp</span> .env.example .env
php artisan key:generate <span class="nt">--force</span>
php artisan p:environment:setup
php artisan p:environment:database
php artisan migrate <span class="nt">--seed</span> <span class="nt">--force</span>
php artisan p:user:make
<span class="nb">chown</span> <span class="nt">-R</span> www-data:www-data /var/www/pterodactyl
</code></pre></div></div>

<h2 id="nginx-pour-le-panel">Nginx pour le Panel</h2>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
    <span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span><span class="p">;</span>
    <span class="kn">server_name</span> <span class="s">pterodactyl.domaine.com</span><span class="p">;</span>

    <span class="kn">ssl_certificate</span>     <span class="n">/etc/letsencrypt/live/pterodactyl.domaine.com/fullchain.pem</span><span class="p">;</span>
    <span class="kn">ssl_certificate_key</span> <span class="n">/etc/letsencrypt/live/pterodactyl.domaine.com/privkey.pem</span><span class="p">;</span>

    <span class="kn">root</span> <span class="n">/var/www/pterodactyl/public</span><span class="p">;</span>
    <span class="kn">index</span> <span class="s">index.php</span><span class="p">;</span>

    <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span> <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="n">/index.php?</span><span class="nv">$query_string</span><span class="p">;</span> <span class="p">}</span>
    <span class="kn">location</span> <span class="p">~</span> <span class="sr">\.php$</span> <span class="p">{</span>
        <span class="kn">include</span> <span class="nc">snippets/fastcgi-php</span><span class="s">.conf</span><span class="p">;</span>
        <span class="kn">fastcgi_pass</span> <span class="s">unix:/var/run/php/php8.3-fpm.sock</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="installation-de-wings">Installation de Wings</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /etc/pterodactyl
curl <span class="nt">-L</span> <span class="nt">-o</span> /usr/local/bin/wings <span class="se">\</span>
  <span class="s2">"https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_amd64"</span>
<span class="nb">chmod </span>u+x /usr/local/bin/wings
</code></pre></div></div>

<h3 id="configuration-wings--http-interne">Configuration Wings — HTTP interne</h3>

<p>Dans le panel : <strong>Admin → Nodes → Créer un node</strong> :</p>
<ul>
  <li>FQDN : <code class="language-plaintext highlighter-rouge">node.domaine.com</code></li>
  <li>Daemon Port : <code class="language-plaintext highlighter-rouge">443</code> ← port public exposé par HAProxy</li>
  <li>✅ <strong>Behind Proxy</strong> coché ← critique</li>
</ul>

<p>Copier la config générée dans <code class="language-plaintext highlighter-rouge">/etc/pterodactyl/config.yml</code>.</p>

<p>Vérifier que Wings écoute en <strong>HTTP</strong> sur <strong>8080</strong> (pas 443) :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># /etc/pterodactyl/config.yml</span>
<span class="na">api</span><span class="pi">:</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>      <span class="c1"># Port interne — HAProxy proxifie vers ici</span>
  <span class="na">ssl</span><span class="pi">:</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="kc">false</span> <span class="c1"># HAProxy gère le TLS</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Service systemd</span>
curl <span class="nt">-o</span> /etc/systemd/system/wings.service <span class="se">\</span>
  https://raw.githubusercontent.com/pterodactyl/wings/develop/wings.service
systemctl <span class="nb">enable</span> <span class="nt">--now</span> wings

<span class="c"># Vérifier</span>
ss <span class="nt">-tlnp</span> | <span class="nb">grep </span>8080
<span class="c"># → Wings écoute sur 0.0.0.0:8080</span>
</code></pre></div></div>

<h2 id="configuration-haproxy">Configuration HAProxy</h2>

<pre><code class="language-haproxy">frontend https_front
    bind *:443 ssl crt /etc/haproxy/combined.pem alpn h2,http/1.1

    acl is_panel hdr(host) -i pterodactyl.domaine.com
    acl is_node  hdr(host) -i node.domaine.com

    use_backend panel_backend if is_panel
    use_backend wings_backend if is_node

backend panel_backend
    option forwardfor
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    timeout tunnel 1h
    server panel 192.168.x.x:443 check ssl verify none

backend wings_backend
    mode http
    option forwardfor
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    timeout tunnel 1h
    server wings 192.168.x.x:8080 check
    # Pas de "ssl" ici — Wings tourne en HTTP pur
</code></pre>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">ssl verify none vs check</div>
    <div class="callout-content"><p>Pour le backend Wings : juste ‘check’ sans ‘ssl’ — Wings écoute en HTTP. Pour le panel nginx qui écoute en HTTPS : ‘check ssl verify none’.</p>
</div>
  </div>
</div>

<h2 id="vérification">Vérification</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Tester Wings via HAProxy</span>
curl <span class="nt">-sk</span> https://node.domaine.com/api/system <span class="nt">-o</span> /dev/null <span class="nt">-w</span> <span class="s2">"%{http_code}"</span>
<span class="c"># → 401 (authentification requise — Wings répond correctement)</span>

<span class="c"># Le node doit passer en vert dans le panel</span>
</code></pre></div></div>

<h2 id="firewall-sur-le-serveur-wings">Firewall sur le serveur Wings</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Autoriser Wings uniquement depuis HAProxy</span>
iptables <span class="nt">-A</span> INPUT <span class="nt">-p</span> tcp <span class="nt">--dport</span> 8080 <span class="nt">-s</span> 192.168.x.11 <span class="nt">-j</span> ACCEPT  <span class="c"># IP HAProxy</span>
iptables <span class="nt">-A</span> INPUT <span class="nt">-p</span> tcp <span class="nt">--dport</span> 8080 <span class="nt">-j</span> DROP

<span class="c"># Port Panel nginx</span>
iptables <span class="nt">-A</span> INPUT <span class="nt">-p</span> tcp <span class="nt">--dport</span> 443 <span class="nt">-j</span> ACCEPT

<span class="c"># SFTP Pterodactyl</span>
iptables <span class="nt">-A</span> INPUT <span class="nt">-p</span> tcp <span class="nt">--dport</span> 2022 <span class="nt">-j</span> ACCEPT

<span class="c"># Persister</span>
apt <span class="nb">install</span> <span class="nt">-y</span> iptables-persistent
netfilter-persistent save
</code></pre></div></div>

<h2 id="résultat">Résultat</h2>

<ul>
  <li>Panel accessible depuis n’importe quel réseau sur le port 443 ✅</li>
  <li>Wings accessible via HAProxy sur node.domaine.com:443 ✅</li>
  <li>Port 8080 non exposé sur internet ✅</li>
  <li>Serveurs de jeu accessibles (Docker ouvre les ports automatiquement) ✅</li>
</ul>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Renouvellement certificats</div>
    <div class="callout-content"><p>Configurez un cron pour renouveler automatiquement les certs Let’s Encrypt et regénérer le combined.pem pour HAProxy (cat fullchain.pem privkey.pem &gt; combined.pem).</p>
</div>
  </div>
</div>]]></content><author><name>50bvd</name></author><category term="Homelab" /><category term="Docker" /><category term="pterodactyl" /><category term="haproxy" /><category term="wings" /><category term="reverse-proxy" /><category term="ssl" /><category term="minecraft" /><category term="debian" /><summary type="html"><![CDATA[Déployer Pterodactyl Panel et Wings sur Debian 13 avec HAProxy comme reverse proxy TLS — Wings exposé sur le port 443 pour contourner les restrictions réseau d'entreprise.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/pterodactyl-cover.jpg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/pterodactyl-cover.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Deming ISMS — Déploiement Docker, LDAP et correction de bugs upstream</title><link href="https://50bvd.com/2026/05/10/deming-isms-docker-ldap-bugs/" rel="alternate" type="text/html" title="Deming ISMS — Déploiement Docker, LDAP et correction de bugs upstream" /><published>2026-05-10T00:00:00+02:00</published><updated>2026-05-10T00:00:00+02:00</updated><id>https://50bvd.com/2026/05/10/deming-isms-docker-ldap-bugs</id><content type="html" xml:base="https://50bvd.com/2026/05/10/deming-isms-docker-ldap-bugs/"><![CDATA[<p><a href="https://github.com/sourcentis/deming">Deming</a> est un outil open source de gestion de la conformité ISMS (ISO 27001) développé par sourcentis. Ce guide couvre son déploiement Docker complet avec LDAP, les deux bugs rencontrés et leur correction upstream.</p>

<h2 id="déploiement-docker">Déploiement Docker</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># docker-compose.yml</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">deming</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">sourcentis/deming:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">deming</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8080:80"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">APP_ENV</span><span class="pi">:</span> <span class="s">production</span>
      <span class="na">APP_KEY</span><span class="pi">:</span> <span class="s">${APP_KEY}</span>
      <span class="na">DB_HOST</span><span class="pi">:</span> <span class="s">mariadb</span>
      <span class="na">DB_DATABASE</span><span class="pi">:</span> <span class="s">deming</span>
      <span class="na">DB_USERNAME</span><span class="pi">:</span> <span class="s">deming</span>
      <span class="na">DB_PASSWORD</span><span class="pi">:</span> <span class="s">${DB_PASSWORD}</span>
      <span class="na">LDAP_HOST</span><span class="pi">:</span> <span class="s">${LDAP_HOST}</span>
      <span class="na">LDAP_PORT</span><span class="pi">:</span> <span class="m">389</span>
      <span class="na">LDAP_BASE_DN</span><span class="pi">:</span> <span class="s">${LDAP_BASE_DN}</span>
      <span class="na">LDAP_USERNAME</span><span class="pi">:</span> <span class="s">${LDAP_USERNAME}</span>
      <span class="na">LDAP_PASSWORD</span><span class="pi">:</span> <span class="s">${LDAP_PASSWORD}</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">deming_storage:/var/www/html/storage</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mariadb</span>

  <span class="na">mariadb</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mariadb:10.11</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">deming-db</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">MYSQL_ROOT_PASSWORD</span><span class="pi">:</span> <span class="s">${DB_ROOT_PASSWORD}</span>
      <span class="na">MYSQL_DATABASE</span><span class="pi">:</span> <span class="s">deming</span>
      <span class="na">MYSQL_USER</span><span class="pi">:</span> <span class="s">deming</span>
      <span class="na">MYSQL_PASSWORD</span><span class="pi">:</span> <span class="s">${DB_PASSWORD}</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">deming_db:/var/lib/mysql</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">deming_storage</span><span class="pi">:</span>
  <span class="na">deming_db</span><span class="pi">:</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Générer la clé APP Laravel</span>
docker compose run <span class="nt">--rm</span> deming php artisan key:generate <span class="nt">--show</span>
<span class="c"># Copier la valeur dans .env</span>

docker compose up <span class="nt">-d</span>
docker compose <span class="nb">exec </span>deming php artisan migrate <span class="nt">--force</span>
</code></pre></div></div>

<h2 id="bug-1--filtre-ldap-andor-incorrect">Bug #1 — Filtre LDAP AND/OR incorrect</h2>

<h3 id="symptôme">Symptôme</h3>

<p>L’authentification LDAP échoue avec des attributs multiples :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LDAP Error: Invalid filter syntax
ldap_search(): Search: Bad search filter
</code></pre></div></div>

<h3 id="cause">Cause</h3>

<p>Le filtre LDAP généré était syntaxiquement incorrect lors de l’utilisation de plusieurs attributs d’identification (ex: <code class="language-plaintext highlighter-rouge">sAMAccountName</code> ET <code class="language-plaintext highlighter-rouge">mail</code>) :</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Code original (bugué)</span>
<span class="c1">// Génère : (&amp;(objectClass=user)(attr1=val)(attr2=val))</span>
<span class="c1">// Correct pour 1 attr, INVALIDE pour plusieurs en OR</span>
<span class="nv">$filter</span> <span class="o">=</span> <span class="s2">"(&amp;(objectClass=user)"</span> <span class="mf">.</span> <span class="nb">implode</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span> <span class="nv">$conditions</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">")"</span><span class="p">;</span>
</code></pre></div></div>

<p>Le filtre correct pour plusieurs attributs en OR doit encapsuler dans <code class="language-plaintext highlighter-rouge">(|...)</code> :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Pour 1 attribut :  (&amp;(objectClass=user)(sAMAccountName=foo))       ✅
// Pour plusieurs :   (&amp;(objectClass=user)(|(sAMAccountName=foo)(mail=foo@bar.com)))  ✅
// Généré (bugué) :   (&amp;(objectClass=user)(sAMAccountName=foo)(mail=foo@bar.com))    ❌
</code></pre></div></div>

<h3 id="fix-pr-688">Fix (PR #688)</h3>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Après correction</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">count</span><span class="p">(</span><span class="nv">$conditions</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$filter</span> <span class="o">=</span> <span class="s2">"(&amp;(objectClass=user)(|"</span> <span class="mf">.</span> <span class="nb">implode</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span> <span class="nv">$conditions</span><span class="p">)</span> <span class="mf">.</span> <span class="s2">"))"</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="nv">$filter</span> <span class="o">=</span> <span class="s2">"(&amp;(objectClass=user)"</span> <span class="mf">.</span> <span class="nv">$conditions</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="mf">.</span> <span class="s2">")"</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="bug-2--email-auto-provision-hardcodé-à-null">Bug #2 — Email auto-provision hardcodé à null</h2>

<h3 id="symptôme-1">Symptôme</h3>

<p>À la première connexion d’un utilisateur LDAP non encore en base, l’auto-provisionnement échoue :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SQLSTATE[23000]: Integrity constraint violation:
1062 Duplicate entry 'null' for key 'users_email_unique'
</code></pre></div></div>

<h3 id="cause-1">Cause</h3>

<p>Le code récupérait l’attribut <code class="language-plaintext highlighter-rouge">mail</code> depuis LDAP, mais en cas d’absence (compte de service, compte sans email configuré), il fallait à <code class="language-plaintext highlighter-rouge">null</code> <strong>hardcodé</strong> plutôt qu’utiliser le <code class="language-plaintext highlighter-rouge">sAMAccountName</code> comme fallback :</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Code original (bugué)</span>
<span class="nv">$email</span> <span class="o">=</span> <span class="nv">$ldapUser</span><span class="o">-&gt;</span><span class="nf">getAttribute</span><span class="p">(</span><span class="s1">'mail'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="o">??</span> <span class="kc">null</span><span class="p">;</span>
<span class="c1">// → null si pas de mail → contrainte unique violée au second utilisateur sans mail</span>
</code></pre></div></div>

<h3 id="fix-pr-690">Fix (PR #690)</h3>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Après correction — fallback sur sAMAccountName@domaine</span>
<span class="nv">$defaultDomain</span> <span class="o">=</span> <span class="nf">config</span><span class="p">(</span><span class="s1">'ldap.default_domain'</span><span class="p">,</span> <span class="s1">'domain.local'</span><span class="p">);</span>
<span class="nv">$email</span> <span class="o">=</span> <span class="nv">$ldapUser</span><span class="o">-&gt;</span><span class="nf">getAttribute</span><span class="p">(</span><span class="s1">'mail'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
      <span class="o">??</span> <span class="p">(</span><span class="nv">$ldapUser</span><span class="o">-&gt;</span><span class="nf">getAttribute</span><span class="p">(</span><span class="s1">'sAMAccountName'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="mf">.</span> <span class="s1">'@'</span> <span class="mf">.</span> <span class="nv">$defaultDomain</span><span class="p">)</span>
      <span class="o">??</span> <span class="kc">null</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="tester-la-connexion-ldap">Tester la connexion LDAP</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Depuis le conteneur</span>
docker <span class="nb">exec</span> <span class="nt">-it</span> deming ldapsearch <span class="se">\</span>
  <span class="nt">-H</span> ldap://DC_IP <span class="se">\</span>
  <span class="nt">-D</span> <span class="s2">"CN=svc-compte,OU=Services,DC=domain,DC=local"</span> <span class="se">\</span>
  <span class="nt">-w</span> PASS <span class="se">\</span>
  <span class="nt">-b</span> <span class="s2">"DC=domain,DC=local"</span> <span class="se">\</span>
  <span class="s2">"(sAMAccountName=testuser)"</span> <span class="se">\</span>
  sAMAccountName mail displayName

<span class="c"># Tester le filtre corrigé manuellement</span>
docker <span class="nb">exec</span> <span class="nt">-it</span> deming ldapsearch <span class="se">\</span>
  <span class="nt">-H</span> ldap://DC_IP <span class="se">\</span>
  <span class="nt">-D</span> <span class="s2">"CN=svc-compte,OU=Services,DC=domain,DC=local"</span> <span class="se">\</span>
  <span class="nt">-w</span> PASS <span class="se">\</span>
  <span class="nt">-b</span> <span class="s2">"DC=domain,DC=local"</span> <span class="se">\</span>
  <span class="s2">"(&amp;(objectClass=user)(|(sAMAccountName=testuser)(mail=test@domain.local)))"</span>
</code></pre></div></div>

<h2 id="contribuer-le-fix-upstream">Contribuer le fix upstream</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Fork + clone</span>
git clone https://github.com/VOTRE_FORK/deming.git
<span class="nb">cd </span>deming
git checkout <span class="nt">-b</span> fix/ldap-filter-and-or

<span class="c"># Modifier le code (voir fixes ci-dessus)</span>
<span class="c"># ...</span>

git add <span class="nb">.</span>
git commit <span class="nt">-m</span> <span class="s2">"Fix LDAP login filter incorrect AND/OR grouping with multiple attributes"</span>
git push origin fix/ldap-filter-and-or
<span class="c"># → Ouvrir la PR sur github.com/sourcentis/deming</span>
</code></pre></div></div>

<p>Les deux PRs ont été mergées dans la version <code class="language-plaintext highlighter-rouge">2026.06.16</code> :</p>
<ul>
  <li><strong>PR #688</strong> — Fix LDAP login filter AND/OR grouping</li>
  <li><strong>PR #690</strong> — Fix hardcoded LDAP auto-provision email fallback</li>
</ul>

<div class="callout callout-success" role="note">
  <div class="callout-icon">
    <i class="fas fa-check-circle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Open source</div>
    <div class="callout-content"><p>Contribuer upstream profite à toute la communauté. Si vous trouvez un bug en déployant un projet open source, prenez 30 minutes pour soumettre une PR — c’est souvent simple et très apprécié.</p>
</div>
  </div>
</div>

<h2 id="configuration-haproxy">Configuration HAProxy</h2>

<pre><code class="language-haproxy">backend deming_backend
    option forwardfor
    option http-server-close
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    timeout tunnel 1h
    server deming 192.168.x.x:8080 check
</code></pre>]]></content><author><name>50bvd</name></author><category term="Infrastructure" /><category term="Sécurité" /><category term="deming" /><category term="isms" /><category term="docker" /><category term="ldap" /><category term="active-directory" /><category term="open-source" /><category term="php" /><category term="bug" /><summary type="html"><![CDATA[Déploiement de l'outil ISMS Deming sur Docker avec authentification LDAP Active Directory — deux bugs découverts, corrigés et mergés en upstream (PR #688 et #690).]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/deming-cover.jpg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/deming-cover.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Proxmox 8 sur Mac Pro 3.1 — Installation, ZFS raidz1 et fix Harpertown</title><link href="https://50bvd.com/2026/04/15/proxmox-mac-pro-31-zfs-harpertown/" rel="alternate" type="text/html" title="Proxmox 8 sur Mac Pro 3.1 — Installation, ZFS raidz1 et fix Harpertown" /><published>2026-04-15T00:00:00+02:00</published><updated>2026-04-15T00:00:00+02:00</updated><id>https://50bvd.com/2026/04/15/proxmox-mac-pro-31-zfs-harpertown</id><content type="html" xml:base="https://50bvd.com/2026/04/15/proxmox-mac-pro-31-zfs-harpertown/"><![CDATA[<p>Le Mac Pro 3.1 (2008) est une plateforme homelab sous-estimée : silencieuse, extensible, et disponible à moins de 200€ d’occasion. Ce guide couvre l’installation complète de Proxmox 8.4 avec ZFS raidz1 et les correctifs spécifiques aux CPU Harpertown.</p>

<h2 id="matériel">Matériel</h2>

<table>
  <thead>
    <tr>
      <th>Composant</th>
      <th>Détail</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Modèle</td>
      <td>Mac Pro 3.1 (Early 2008)</td>
    </tr>
    <tr>
      <td>CPU</td>
      <td>2× Intel Xeon E5462 (Harpertown, 4c/4t × 2 = 8 threads)</td>
    </tr>
    <tr>
      <td>RAM</td>
      <td>32 Go DDR2 ECC 800 MHz</td>
    </tr>
    <tr>
      <td>Stockage boot</td>
      <td>SSD 120 Go (SATA via adaptateur)</td>
    </tr>
    <tr>
      <td>Stockage data</td>
      <td>4× HDD 2 To (ZFS raidz1)</td>
    </tr>
  </tbody>
</table>

<h2 id="installation-de-proxmox">Installation de Proxmox</h2>

<p>Flashez l’ISO sur une clé USB :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">dd </span><span class="k">if</span><span class="o">=</span>proxmox-ve_8.4.iso <span class="nv">of</span><span class="o">=</span>/dev/sdX <span class="nv">bs</span><span class="o">=</span>4M <span class="nv">status</span><span class="o">=</span>progress
</code></pre></div></div>

<p>Au démarrage du Mac Pro, maintenez <strong>Option</strong> pour sélectionner la clé USB. Le Mac Pro 3.1 boot en mode BIOS legacy — si la clé ne s’affiche pas, essayez un autre port USB (les ports arrière fonctionnent mieux).</p>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Boot EFI</div>
    <div class="callout-content"><p>Le Mac Pro 3.1 peut être capricieux avec certaines clés USB. Si ça bloque, gravez un DVD ou utilisez rEFInd comme bootloader intermédiaire.</p>
</div>
  </div>
</div>

<h2 id="bug--msr-ia32_debugctl-harpertown">Bug — MSR IA32_DEBUGCTL (Harpertown)</h2>

<p>Après installation, les VMs crashent avec cette erreur dans les logs KVM :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kvm: vcpu0, guest rip=0xffffffff8104b90c, error 1,
     info 0: MSR_IA32_DEBUGCTL error
</code></pre></div></div>

<p>C’est un bug connu avec les Xeon Harpertown (architecture Penryn) — ces CPU ne supportent pas complètement le registre <code class="language-plaintext highlighter-rouge">IA32_DEBUGCTL</code> requis par KVM.</p>

<h3 id="correctif">Correctif</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Option 1 : modifier le paramètre du module</span>
<span class="nb">echo</span> <span class="s2">"options kvm-intel ignore_msrs=Y"</span> <span class="o">&gt;</span> /etc/modprobe.d/kvm-intel.conf

<span class="c"># Recharger sans redémarrer</span>
modprobe <span class="nt">-r</span> kvm_intel
modprobe kvm_intel <span class="nv">ignore_msrs</span><span class="o">=</span>Y

<span class="c"># Vérifier</span>
<span class="nb">cat</span> /sys/module/kvm_intel/parameters/ignore_msrs
<span class="c"># → Y</span>

<span class="c"># Appliquer définitivement</span>
update-initramfs <span class="nt">-u</span> <span class="nt">-k</span> all
</code></pre></div></div>

<p>Après reboot, les VMs démarrent et tournent stablement.</p>

<h2 id="configuration-du-pool-zfs-raidz1">Configuration du pool ZFS raidz1</h2>

<p>Raidz1 avec 4 disques de 2 To = <strong>6 To utilisables</strong> (1 disque de parité).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Identifier les disques par leur ID stable (éviter /dev/sdX qui peut changer)</span>
<span class="nb">ls</span> <span class="nt">-la</span> /dev/disk/by-id/ | <span class="nb">grep</span> <span class="nt">-v</span> part | <span class="nb">grep </span>ata

<span class="c"># Créer le pool</span>
zpool create <span class="nt">-o</span> <span class="nv">ashift</span><span class="o">=</span>12 <span class="se">\</span>
  <span class="nt">-O</span> <span class="nv">compression</span><span class="o">=</span>lz4 <span class="se">\</span>
  <span class="nt">-O</span> <span class="nv">atime</span><span class="o">=</span>off <span class="se">\</span>
  <span class="nt">-O</span> <span class="nv">xattr</span><span class="o">=</span>sa <span class="se">\</span>
  <span class="nt">-O</span> <span class="nv">dnodesize</span><span class="o">=</span>auto <span class="se">\</span>
  datastore raidz1 <span class="se">\</span>
  /dev/disk/by-id/ata-DISK1 <span class="se">\</span>
  /dev/disk/by-id/ata-DISK2 <span class="se">\</span>
  /dev/disk/by-id/ata-DISK3 <span class="se">\</span>
  /dev/disk/by-id/ata-DISK4

<span class="c"># Vérifier</span>
zpool status
zfs list
</code></pre></div></div>

<h3 id="paramètres-recommandés">Paramètres recommandés</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ARC limité à 8 Go (éviter de tout consommer pour les VMs)</span>
<span class="nb">echo</span> <span class="s2">"options zfs zfs_arc_max=8589934592"</span> <span class="o">&gt;</span> /etc/modprobe.d/zfs.conf

<span class="c"># Scrub hebdomadaire automatique (déjà configuré par Proxmox)</span>
systemctl status zfs-scrub.timer
</code></pre></div></div>

<h3 id="ajouter-le-datastore-à-proxmox">Ajouter le datastore à Proxmox</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pvesm add zfspool datastore <span class="se">\</span>
  <span class="nt">--pool</span> datastore <span class="se">\</span>
  <span class="nt">--sparse</span> 1 <span class="se">\</span>
  <span class="nt">--content</span> images,rootdir
</code></pre></div></div>

<p>Ou via l’interface : <strong>Datacenter → Storage → Add → ZFS</strong>.</p>

<h2 id="ssd-en-cache-l2arc-optionnel">SSD en cache L2ARC (optionnel)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Ajouter un SSD comme cache de lecture</span>
zpool add datastore cache /dev/disk/by-id/ata-SSD

<span class="c"># Vérifier</span>
zpool status
<span class="c"># → cache: SSD affiché</span>
</code></pre></div></div>

<h2 id="configuration-réseau-bridge">Configuration réseau bridge</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/network/interfaces</span>
auto lo
iface lo inet loopback

auto enp0s3
iface enp0s3 inet manual

auto vmbr0
iface vmbr0 inet static
    address 192.168.1.10/24
    gateway 192.168.1.1
    bridge-ports enp0s3
    bridge-stp off
    bridge-fd 0
</code></pre></div></div>

<h2 id="performances-observées">Performances observées</h2>

<p>Après plusieurs mois avec 8-10 VMs simultanées :</p>

<table>
  <thead>
    <tr>
      <th>Métrique</th>
      <th>Valeur</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CPU moyen</td>
      <td>15-40%</td>
    </tr>
    <tr>
      <td>RAM utilisée</td>
      <td>22-28 Go / 32</td>
    </tr>
    <tr>
      <td>I/O ZFS (seq read)</td>
      <td>~150 Mo/s</td>
    </tr>
    <tr>
      <td>I/O ZFS (seq write)</td>
      <td>~80 Mo/s</td>
    </tr>
    <tr>
      <td>Température CPU</td>
      <td>45-55°C</td>
    </tr>
  </tbody>
</table>

<p>La ventilation Mac Pro (axiale, très silencieuse) gère parfaitement la thermique même sous charge.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Le Mac Pro 3.1 est une base homelab solide malgré ses 16 ans. Le seul vrai obstacle est le bug <code class="language-plaintext highlighter-rouge">ignore_msrs</code> — une fois corrigé, tout fonctionne parfaitement sous Proxmox 8.4.</p>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">RAM</div>
    <div class="callout-content"><p>Le Mac Pro 3.1 supporte jusqu’à 32 Go DDR2 ECC en 8 slots. Les barrettes appairées sont importantes pour les performances. Achetez en kits testés pour Mac Pro.</p>
</div>
  </div>
</div>]]></content><author><name>50bvd</name></author><category term="Homelab" /><category term="Virtualisation" /><category term="proxmox" /><category term="zfs" /><category term="mac-pro" /><category term="homelab" /><category term="virtualisation" /><category term="harpertown" /><category term="kvm" /><summary type="html"><![CDATA[Retour d'expérience sur l'installation de Proxmox 8.4 sur un Mac Pro 3.1 (dual Xeon Harpertown 2008), pool ZFS raidz1, et correctif du bug MSR IA32_DEBUGCTL.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/proxmox-cover.jpg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/proxmox-cover.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Joindre AlmaLinux 9 à un domaine Active Directory via SSSD</title><link href="https://50bvd.com/2026/02/10/almalinux-active-directory-sssd-ldap/" rel="alternate" type="text/html" title="Joindre AlmaLinux 9 à un domaine Active Directory via SSSD" /><published>2026-02-10T00:00:00+01:00</published><updated>2026-02-10T00:00:00+01:00</updated><id>https://50bvd.com/2026/02/10/almalinux-active-directory-sssd-ldap</id><content type="html" xml:base="https://50bvd.com/2026/02/10/almalinux-active-directory-sssd-ldap/"><![CDATA[<p>Joindre un serveur Linux à un domaine Active Directory permet aux utilisateurs du domaine de s’authentifier avec leurs identifiants AD et de bénéficier des politiques centralisées. Voici comment le faire proprement sur AlmaLinux 9 avec SSSD et Realmd.</p>

<h2 id="prérequis">Prérequis</h2>

<ul>
  <li>AlmaLinux 9 à jour</li>
  <li>Accès réseau au contrôleur de domaine (ports 88, 389, 636, 3268, 3269)</li>
  <li>Compte AD avec droits de jonction de machine</li>
  <li>DNS configuré pour résoudre le domaine AD</li>
</ul>

<h2 id="vérification-du-réseau">Vérification du réseau</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vérifier la résolution DNS du domaine AD</span>
dig +short domaine.local _ldap._tcp.domaine.local SRV
<span class="c"># → doit retourner l'IP et le port LDAP du DC</span>

<span class="c"># Ping vers le DC</span>
ping dc1.domaine.local

<span class="c"># Vérifier les ports nécessaires</span>
<span class="k">for </span>port <span class="k">in </span>88 389 636<span class="p">;</span> <span class="k">do
  </span>nc <span class="nt">-zv</span> dc1.domaine.local <span class="nv">$port</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"Port </span><span class="nv">$port</span><span class="s2"> OK"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"Port </span><span class="nv">$port</span><span class="s2"> FERMÉ"</span>
<span class="k">done</span>
</code></pre></div></div>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">DNS critique</div>
    <div class="callout-content"><p>Le DNS doit impérativement résoudre le domaine AD et les enregistrements SRV. Configurez les DCs comme serveurs DNS dans /etc/resolv.conf avant de commencer.</p>
</div>
  </div>
</div>

<h2 id="configurer-le-dns">Configurer le DNS</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/resolv.conf</span>
search domaine.local
nameserver 192.168.x.10   <span class="c"># DC1</span>
nameserver 192.168.x.11   <span class="c"># DC2</span>

<span class="c"># Vérifier</span>
nslookup domaine.local
nslookup dc1.domaine.local
</code></pre></div></div>

<h2 id="installation-des-paquets">Installation des paquets</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dnf <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
  realmd <span class="se">\</span>
  sssd <span class="se">\</span>
  sssd-ad <span class="se">\</span>
  sssd-tools <span class="se">\</span>
  oddjob <span class="se">\</span>
  oddjob-mkhomedir <span class="se">\</span>
  adcli <span class="se">\</span>
  samba-common-tools <span class="se">\</span>
  krb5-workstation <span class="se">\</span>
  openldap-clients
</code></pre></div></div>

<h2 id="jonction-au-domaine">Jonction au domaine</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Découverte du domaine (sans jonction)</span>
realm discover domaine.local

<span class="c"># Jonction au domaine</span>
realm <span class="nb">join</span> <span class="nt">-U</span> administrateur domaine.local
<span class="c"># Entrez le mot de passe du compte AD quand demandé</span>

<span class="c"># Vérifier la jonction</span>
realm list
</code></pre></div></div>

<p>Sortie attendue :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>domaine.local
  type: kerberos
  realm-name: DOMAINE.LOCAL
  domain-name: domaine.local
  configured: kerberos-member
  server-software: active-directory
  client-software: sssd
  required-package: oddjob
  required-package: oddjob-mkhomedir
  required-package: sssd
  required-package: adcli
  required-package: samba-common-tools
  login-formats: %U@domaine.local
  login-policy: allow-realm-logins
</code></pre></div></div>

<h2 id="configuration-sssd">Configuration SSSD</h2>

<p>Le fichier <code class="language-plaintext highlighter-rouge">/etc/sssd/sssd.conf</code> est généré automatiquement par <code class="language-plaintext highlighter-rouge">realm join</code>, mais quelques ajustements sont recommandés :</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/sssd/sssd.conf
</span><span class="nn">[sssd]</span><span class="w">
</span><span class="py">domains</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">domaine.local</span>
<span class="py">config_file_version</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">2</span>
<span class="py">services</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">nss, pam</span>
<span class="w">
</span><span class="nn">[domain/domaine.local]</span><span class="w">
</span><span class="py">default_shell</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">/bin/bash</span>
<span class="py">krb5_store_password_if_offline</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">True</span>
<span class="py">cache_credentials</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">True</span>
<span class="py">krb5_realm</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">DOMAINE.LOCAL</span>
<span class="py">realmd_tags</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">manages-system joined-with-adcli</span>
<span class="py">id_provider</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">ad</span>
<span class="py">fallback_homedir</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">/home/%u@%d</span>
<span class="py">ad_domain</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">domaine.local</span>
<span class="py">use_fully_qualified_names</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">False    # ← Permet login sans @domaine.local</span>
<span class="py">ldap_id_mapping</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">True</span>
<span class="py">access_provider</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">ad</span>
<span class="w">
</span><span class="c"># Performance
</span><span class="py">ad_gpo_access_control</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">disabled    # Désactiver GPO Linux (optionnel)</span>
<span class="py">ldap_referrals</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">false</span>
<span class="w">
</span><span class="c"># Cache
</span><span class="py">cache_credentials</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">true</span>
<span class="py">offline_credentials_expiration</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">7</span>
<span class="w">
</span><span class="c"># Timeout
</span><span class="py">ldap_network_timeout</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">3</span>
<span class="py">ldap_opt_timeout</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">3</span>
</code></pre></div></div>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">use_fully_qualified_names = False</div>
    <div class="callout-content"><p>Sans cette option, les utilisateurs doivent se connecter avec utilisateur@domaine.local. Avec False, ‘utilisateur’ suffit — beaucoup plus pratique.</p>
</div>
  </div>
</div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Appliquer les permissions (obligatoire)</span>
<span class="nb">chmod </span>600 /etc/sssd/sssd.conf

<span class="c"># Redémarrer SSSD</span>
systemctl restart sssd
systemctl <span class="nb">enable </span>sssd
</code></pre></div></div>

<h2 id="création-automatique-du-home-directory">Création automatique du home directory</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Activer la création automatique du home au premier login</span>
authselect <span class="k">select </span>sssd with-mkhomedir <span class="nt">--force</span>

<span class="c"># Activer le service oddjobd</span>
systemctl <span class="nb">enable</span> <span class="nt">--now</span> oddjobd
</code></pre></div></div>

<h2 id="tester-lauthentification">Tester l’authentification</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vérifier qu'un utilisateur AD est visible</span>
<span class="nb">id </span>utilisateur@domaine.local
<span class="c"># Avec use_fully_qualified_names = False :</span>
<span class="nb">id </span>utilisateur

<span class="c"># Tester l'authentification Kerberos</span>
kinit administrateur@DOMAINE.LOCAL
klist   <span class="c"># Affiche le ticket Kerberos</span>

<span class="c"># Tester la connexion SSH avec un compte AD</span>
ssh utilisateur@localhost
</code></pre></div></div>

<h2 id="autoriser-uniquement-certains-groupes-ad">Autoriser uniquement certains groupes AD</h2>

<p>Par défaut, tous les utilisateurs AD peuvent se connecter. Limitez l’accès :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Autoriser uniquement le groupe "Linux-Admins" AD</span>
realm permit <span class="nt">-g</span> <span class="s1">'Linux-Admins@domaine.local'</span>

<span class="c"># Interdire tout le monde sauf les autorisés</span>
realm deny <span class="nt">--all</span>
realm permit <span class="nt">-g</span> <span class="s1">'Linux-Admins@domaine.local'</span>
realm permit <span class="nt">-g</span> <span class="s1">'Linux-Users@domaine.local'</span>

<span class="c"># Vérifier</span>
realm list
</code></pre></div></div>

<h2 id="sudo-via-groupes-active-directory">Sudo via groupes Active Directory</h2>

<p>Créez un fichier sudoers pour vos groupes AD :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># /etc/sudoers.d/ad-admins</span>
<span class="c"># Donner sudo complet au groupe "Linux-Admins" AD</span>
%Linux-Admins@domaine.local <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL

<span class="c"># Sudo sans mot de passe pour les admins infra</span>
%Linux-Infra-Admins@domaine.local <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> NOPASSWD: ALL
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vérifier la syntaxe</span>
visudo <span class="nt">-c</span> <span class="nt">-f</span> /etc/sudoers.d/ad-admins

<span class="c"># Tester</span>
su - utilisateur-admin@domaine.local
<span class="nb">sudo whoami</span>   <span class="c"># → root</span>
</code></pre></div></div>

<h2 id="problèmes-courants">Problèmes courants</h2>

<h3 id="sssd-ne-démarre-pas">SSSD ne démarre pas</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>journalctl <span class="nt">-u</span> sssd <span class="nt">-n</span> 50 <span class="nt">--no-pager</span>

<span class="c"># Problème fréquent : permissions sssd.conf</span>
<span class="nb">chmod </span>600 /etc/sssd/sssd.conf
<span class="nb">chown </span>root:root /etc/sssd/sssd.conf

<span class="c"># Nettoyer le cache SSSD (si utilisateurs "fantômes")</span>
sss_cache <span class="nt">-E</span>
systemctl restart sssd
</code></pre></div></div>

<h3 id="utilisateur-ad-introuvable">Utilisateur AD introuvable</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vérifier la connexion LDAP directement</span>
ldapsearch <span class="nt">-H</span> ldap://dc1.domaine.local <span class="se">\</span>
  <span class="nt">-D</span> <span class="s2">"CN=svc-linux,OU=Services,DC=domaine,DC=local"</span> <span class="se">\</span>
  <span class="nt">-w</span> PASS <span class="se">\</span>
  <span class="nt">-b</span> <span class="s2">"DC=domaine,DC=local"</span> <span class="se">\</span>
  <span class="s2">"(sAMAccountName=utilisateur)"</span>

<span class="c"># Forcer la synchronisation</span>
sss_cache <span class="nt">-u</span> utilisateur
<span class="nb">id </span>utilisateur
</code></pre></div></div>

<h3 id="kerberos-expiré-ou-invalide">Kerberos expiré ou invalide</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vérifier le ticket</span>
klist

<span class="c"># Renouveler manuellement</span>
kinit utilisateur@DOMAINE.LOCAL

<span class="c"># Synchroniser l'heure (Kerberos est sensible à la dérive)</span>
timedatectl status
chronyc tracking

<span class="c"># Si décalage &gt; 5 min → Kerberos échoue</span>
chronyc makestep
</code></pre></div></div>

<h2 id="déjoindre-le-domaine">Déjoindre le domaine</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>realm leave domaine.local
<span class="c"># Le compte machine est supprimé de l'AD</span>
</code></pre></div></div>

<h2 id="vérification-finale">Vérification finale</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Test complet</span>
getent passwd utilisateur        <span class="c"># L'utilisateur est visible</span>
getent group <span class="s1">'Linux-Admins'</span>      <span class="c"># Le groupe AD est visible</span>
su - utilisateur                  <span class="c"># Connexion → home créé automatiquement</span>
<span class="nb">sudo</span> <span class="nt">-l</span>                           <span class="c"># Droits sudo corrects</span>
</code></pre></div></div>

<div class="callout callout-success" role="note">
  <div class="callout-icon">
    <i class="fas fa-check-circle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Résultat</div>
    <div class="callout-content"><p>Les utilisateurs AD peuvent désormais se connecter en SSH sur le serveur Linux avec leurs identifiants du domaine. Le home directory est créé automatiquement au premier login.</p>
</div>
  </div>
</div>

<h2 id="cas-particulier--authentification-sans-jonction-bind-ldap-simple">Cas particulier — authentification sans jonction (bind LDAP simple)</h2>

<p>Si vous ne pouvez pas joindre la machine au domaine (environnements restrictifs), une alternative est le bind LDAP simple via <code class="language-plaintext highlighter-rouge">nslcd</code> :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dnf <span class="nb">install</span> <span class="nt">-y</span> nss-pam-ldapd nslcd

<span class="c"># /etc/nslcd.conf</span>
uri ldap://dc1.domaine.local
base <span class="nv">dc</span><span class="o">=</span>domaine,dc<span class="o">=</span><span class="nb">local
</span>binddn <span class="nv">CN</span><span class="o">=</span>svc-linux,OU<span class="o">=</span>Services,DC<span class="o">=</span>domaine,DC<span class="o">=</span><span class="nb">local
</span>bindpw VOTRE_MOT_DE_PASSE
ssl start_tls
tls_reqcert never
scope sub
filter passwd <span class="o">(</span>&amp;<span class="o">(</span><span class="nv">objectClass</span><span class="o">=</span>user<span class="o">)(</span><span class="nv">memberOf</span><span class="o">=</span><span class="nv">CN</span><span class="o">=</span>Linux-Users,OU<span class="o">=</span>Groupes,DC<span class="o">=</span>domaine,DC<span class="o">=</span><span class="nb">local</span><span class="o">))</span>
map passwd uid sAMAccountName
map passwd homeDirectory unixHomeDirectory
map passwd gecos displayName
</code></pre></div></div>

<p>Cette méthode est moins intégrée (pas de Kerberos, pas de SSO) mais fonctionne dans des environnements contraints.</p>]]></content><author><name>50bvd</name></author><category term="Infrastructure" /><category term="Linux" /><category term="ldap" /><category term="active-directory" /><category term="almalinux" /><category term="sssd" /><category term="linux" /><category term="authentification" /><summary type="html"><![CDATA[Guide complet pour intégrer un serveur AlmaLinux 9 à un domaine Active Directory — authentification LDAP/Kerberos, sudo AD, montage homedir automatique.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/ldap-almalinux-cover.jpg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/ldap-almalinux-cover.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Pi-hole sur Raspberry Pi — Bloquer les pubs sur tout le réseau</title><link href="https://50bvd.com/2026/01/15/pihole-raspberry-pi-dns/" rel="alternate" type="text/html" title="Pi-hole sur Raspberry Pi — Bloquer les pubs sur tout le réseau" /><published>2026-01-15T00:00:00+01:00</published><updated>2026-01-15T00:00:00+01:00</updated><id>https://50bvd.com/2026/01/15/pihole-raspberry-pi-dns</id><content type="html" xml:base="https://50bvd.com/2026/01/15/pihole-raspberry-pi-dns/"><![CDATA[<p>Pi-hole est un bloqueur de publicités au niveau DNS qui protège l’ensemble de votre réseau — téléphones, TVs connectées, consoles, ordinateurs — sans installer quoi que ce soit sur chaque appareil.</p>

<h2 id="principe-de-fonctionnement">Principe de fonctionnement</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Appareil (téléphone, PC, TV...)
       │ Requête DNS : pub.example.com
       ▼
Pi-hole (serveur DNS local)
       │ Liste noire : pub.example.com → BLOQUÉ → 0.0.0.0
       │ Domaine légitime → forwarde vers 1.1.1.1
       ▼
Internet (uniquement pour les domaines légitimes)
</code></pre></div></div>

<p>Pi-hole répond <code class="language-plaintext highlighter-rouge">0.0.0.0</code> pour les domaines publicitaires — l’appareil ne charge jamais le contenu.</p>

<h2 id="matériel-requis">Matériel requis</h2>

<ul>
  <li>Raspberry Pi (Zero 2W, 3B+, 4 ou 5)</li>
  <li>Carte microSD ≥ 8 Go (classe 10)</li>
  <li>Alimentation micro-USB ou USB-C selon le modèle</li>
  <li>Connexion Ethernet (recommandé) ou Wi-Fi</li>
</ul>

<div class="callout callout-tip" role="note">
  <div class="callout-icon">
    <i class="fas fa-lightbulb" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Quel modèle choisir ?</div>
    <div class="callout-content"><p>Le Raspberry Pi Zero 2W (~18€) suffit amplement pour Pi-hole sur un réseau domestique. Le Pi 4 est surdimensionné pour cet usage mais permet d’héberger d’autres services en même temps.</p>
</div>
  </div>
</div>

<h2 id="installation-de-raspberry-pi-os">Installation de Raspberry Pi OS</h2>

<p><strong>1.</strong> Téléchargez <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a></p>

<p><strong>2.</strong> Dans l’imager, cliquez sur l’icône ⚙️ avant de flasher pour préconfigurer :</p>
<ul>
  <li>Activez SSH</li>
  <li>Configurez votre Wi-Fi (SSID + mot de passe)</li>
  <li>Définissez un hostname (ex: <code class="language-plaintext highlighter-rouge">pihole</code>)</li>
  <li>Définissez un utilisateur/mot de passe</li>
</ul>

<p><strong>3.</strong> Flashez la carte SD avec <strong>Raspberry Pi OS Lite (64-bit)</strong></p>

<p><strong>4.</strong> Insérez la carte et démarrez le Pi. Connectez-vous en SSH :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh pi@pihole.local
<span class="c"># ou ssh pi@IP_DU_PI</span>
</code></pre></div></div>

<h2 id="attribution-dune-ip-statique">Attribution d’une IP statique</h2>

<p>Pi-hole doit toujours avoir la même IP pour servir de DNS :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Option 1 : réservation DHCP dans votre box/routeur (recommandé)</span>
<span class="c"># Trouvez l'adresse MAC du Pi dans l'interface de votre routeur</span>
<span class="c"># et créez une réservation DHCP</span>

<span class="c"># Option 2 : IP statique sur le Pi</span>
<span class="nb">sudo </span>nano /etc/dhcpcd.conf

<span class="c"># Ajouter à la fin :</span>
interface eth0
static <span class="nv">ip_address</span><span class="o">=</span>192.168.1.100/24
static <span class="nv">routers</span><span class="o">=</span>192.168.1.1
static <span class="nv">domain_name_servers</span><span class="o">=</span>127.0.0.1
</code></pre></div></div>

<h2 id="installation-de-pi-hole">Installation de Pi-hole</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-sSL</span> https://install.pi-hole.net | bash
</code></pre></div></div>

<p>L’installateur interactif vous guidera :</p>

<ol>
  <li><strong>Interface réseau</strong> : <code class="language-plaintext highlighter-rouge">eth0</code> (câble) ou <code class="language-plaintext highlighter-rouge">wlan0</code> (Wi-Fi)</li>
  <li><strong>DNS upstream</strong> : choisissez votre forwarder (Cloudflare, Quad9, etc.)</li>
  <li><strong>Confirmation IP statique</strong> : validez votre IP</li>
  <li><strong>Interface web</strong> : activez-la</li>
</ol>

<p>À la fin, notez le mot de passe admin généré.</p>

<div class="callout callout-warning" role="note">
  <div class="callout-icon">
    <i class="fas fa-exclamation-triangle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Mot de passe admin</div>
    <div class="callout-content"><p>Si vous ratez le mot de passe, vous pouvez le changer à tout moment avec : pihole -a -p nouveau-mot-de-passe</p>
</div>
  </div>
</div>

<h2 id="configuration-post-installation">Configuration post-installation</h2>

<h3 id="accéder-à-linterface-web">Accéder à l’interface web</h3>

<p>Ouvrez <code class="language-plaintext highlighter-rouge">http://192.168.1.100/admin</code> dans votre navigateur.</p>

<h3 id="mettre-à-jour-les-listes-de-blocage">Mettre à jour les listes de blocage</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pihole <span class="nt">-g</span>
</code></pre></div></div>

<h3 id="ajouter-des-listes-supplémentaires">Ajouter des listes supplémentaires</h3>

<p>Dans l’interface : <strong>Group Management → Adlists</strong>, ajoutez :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
https://someonewhocares.org/hosts/zero/hosts
https://raw.githubusercontent.com/PolishFiltersTeam/KADhosts/master/KADhosts.txt
https://adaway.org/hosts.txt
</code></pre></div></div>

<p>Après ajout : <strong>Tools → Update Gravity</strong> pour télécharger les listes.</p>

<h2 id="configurer-votre-box-comme-serveur-dns">Configurer votre box comme serveur DNS</h2>

<p>C’est l’étape clé : tous les appareils du réseau doivent utiliser Pi-hole comme DNS.</p>

<p><strong>Dans l’interface de votre box/routeur :</strong></p>
<ul>
  <li>Paramètres DHCP → DNS primaire : <code class="language-plaintext highlighter-rouge">192.168.1.100</code> (IP du Pi)</li>
  <li>DNS secondaire : laisser vide ou mettre <code class="language-plaintext highlighter-rouge">1.1.1.1</code> (fallback si Pi-hole tombe)</li>
</ul>

<p>Sur certaines box, il faut redémarrer les appareils pour qu’ils obtiennent le nouveau DNS.</p>

<h2 id="résultats-typiques">Résultats typiques</h2>

<p>Après quelques heures de fonctionnement :</p>

<table>
  <thead>
    <tr>
      <th>Métrique</th>
      <th>Valeur typique</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Requêtes bloquées</td>
      <td>20-35%</td>
    </tr>
    <tr>
      <td>Domaines dans la liste noire</td>
      <td>150 000 - 300 000</td>
    </tr>
    <tr>
      <td>Latence DNS ajoutée</td>
      <td>&lt; 1ms en LAN</td>
    </tr>
    <tr>
      <td>RAM utilisée (Pi Zero 2W)</td>
      <td>~80 Mo</td>
    </tr>
  </tbody>
</table>

<h2 id="commandes-utiles">Commandes utiles</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Statut Pi-hole</span>
pihole status

<span class="c"># Voir les requêtes en temps réel</span>
pihole <span class="nt">-t</span>

<span class="c"># Mettre à jour Pi-hole</span>
pihole <span class="nt">-up</span>

<span class="c"># Désactiver temporairement (300 secondes)</span>
pihole disable 300
pihole <span class="nb">enable</span>

<span class="c"># Whitelister un domaine</span>
pihole <span class="nt">-w</span> domaine.com

<span class="c"># Blacklister un domaine manuellement</span>
pihole <span class="nt">-b</span> domaine-pub.com

<span class="c"># Statistiques rapides</span>
pihole <span class="nt">-c</span>
</code></pre></div></div>

<h2 id="dépannage-courant">Dépannage courant</h2>

<h3 id="un-site-est-bloqué-par-erreur">Un site est bloqué par erreur</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Chercher dans les logs</span>
pihole <span class="nt">-t</span> | <span class="nb">grep</span> <span class="s2">"nom-du-site.com"</span>

<span class="c"># Whitelister</span>
pihole <span class="nt">-w</span> nom-du-site.com
</code></pre></div></div>

<h3 id="les-appareils-nutilisent-pas-pi-hole">Les appareils n’utilisent pas Pi-hole</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vérifier quel DNS un appareil utilise (depuis cet appareil)</span>
<span class="c"># Windows :</span>
nslookup google.com
<span class="c"># → Server: doit afficher l'IP de Pi-hole</span>

<span class="c"># Linux/Mac :</span>
dig google.com | <span class="nb">grep </span>SERVER
</code></pre></div></div>

<h3 id="pi-hole-ne-répond-pas">Pi-hole ne répond pas</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vérifier le service</span>
<span class="nb">sudo </span>systemctl status pihole-FTL

<span class="c"># Redémarrer si nécessaire</span>
<span class="nb">sudo </span>systemctl restart pihole-FTL
</code></pre></div></div>

<h2 id="sur-mon-homelab">Sur mon homelab</h2>

<p>Sur mon Raspberry Pi, Pi-hole tourne en conteneur Docker avec <code class="language-plaintext highlighter-rouge">vlmcsd</code> (serveur KMS) sur la même machine :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">pihole</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">pihole/pihole:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">pihole</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">53:53/tcp"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">53:53/udp"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">80:80/tcp"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">TZ</span><span class="pi">:</span> <span class="s">Europe/Paris</span>
      <span class="na">WEBPASSWORD</span><span class="pi">:</span> <span class="s">votre-mot-de-passe</span>
      <span class="na">PIHOLE_DNS_</span><span class="pi">:</span> <span class="s">94.140.14.14;1.1.1.1</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./etc-pihole:/etc/pihole</span>
      <span class="pi">-</span> <span class="s">./etc-dnsmasq.d:/etc/dnsmasq.d</span>

  <span class="na">vlmcsd</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mikolatero/vlmcsd:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">vlmcsd</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">1688:1688"</span>
</code></pre></div></div>

<div class="callout callout-success" role="note">
  <div class="callout-icon">
    <i class="fas fa-check-circle" aria-hidden="true"></i>
  </div>
  <div class="callout-body">
    <div class="callout-title">Résultat</div>
    <div class="callout-content"><p>Sur mon réseau, Pi-hole bloque en moyenne 28% des requêtes DNS — principalement de la publicité, des trackers et de la télémétrie Windows et Android.</p>
</div>
  </div>
</div>

<h2 id="conclusion">Conclusion</h2>

<p>Pi-hole est l’un des projets homelab avec le meilleur rapport effort/résultat. Une fois installé, il fonctionne de manière transparente pour tous les appareils du réseau sans aucune configuration côté client.</p>]]></content><author><name>50bvd</name></author><category term="Réseau" /><category term="Homelab" /><category term="pi-hole" /><category term="raspberry-pi" /><category term="dns" /><category term="adblock" /><category term="linux" /><category term="réseau" /><summary type="html"><![CDATA[Guide complet pour installer Pi-hole sur un Raspberry Pi et bloquer publicités et trackers pour tous les appareils du réseau local via DNS.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://50bvd.com/assets/images/posts/pihole-cover.jpg" /><media:content medium="image" url="https://50bvd.com/assets/images/posts/pihole-cover.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>