<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Infrastructure - Category - arleo.eu</title><link>https://www.arleo.eu/en/categories/infrastructure/</link><description>Infrastructure - Category - arleo.eu</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 29 May 2026 20:30:00 +0200</lastBuildDate><atom:link href="https://www.arleo.eu/en/categories/infrastructure/" rel="self" type="application/rss+xml"/><item><title>Hugo SEO: Googlebot 404s, noindex aliases and sitemap normalization</title><link>https://www.arleo.eu/en/posts/debug-seo-404-broken-links/</link><pubDate>Fri, 29 May 2026 20:30:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/debug-seo-404-broken-links/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/debug-seo-404-broken-links-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="context">Context</h2>
<p>Google Search Console was reporting four categories of issues on arleo.eu:</p>
<ul>
<li><strong>Googlebot 404s</strong>: <code>/fr/tag/cloudflare</code>, <code>/en/tag/nginx</code>, <code>/fr/tag/javascript</code>… URLs with <code>/fr/</code> prefix or singular <code>/tag/</code> never served by nginx</li>
<li><strong>16 &ldquo;Excluded by noindex tag&rdquo; pages</strong>: all redirect pages generated by <code>aliases:</code> in Hugo frontmatter</li>
<li><strong>Robots tag</strong>: <code>noodp</code> hardcoded in the LoveIt theme</li>
<li><strong>FR/EN sitemap</strong>: 104 vs 105 URLs — a duplicate FR tag and two missing tags</li>
</ul>
<hr>
<h2 id="act-1-hugo-aliases--nginx-301-redirects">Act 1: Hugo aliases → nginx 301 redirects</h2>
<h3 id="why-hugo-generates-noindex-pages">Why Hugo generates noindex pages</h3>
<p>Hugo generates <code>aliases:</code> frontmatter entries as static HTML files:</p>]]></description></item><item><title>Hugo: freezing Mermaid diagrams to static dark/light SVGs</title><link>https://www.arleo.eu/en/posts/debug-mermaid-svg-freeze/</link><pubDate>Fri, 29 May 2026 20:00:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/debug-mermaid-svg-freeze/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/debug-mermaid-svg-freeze-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="problem">Problem</h2>
<p>Mermaid diagrams on arleo.eu were rendering client-side via <code>cdn.jsdelivr.net</code>. Three concrete consequences:</p>
<ol>
<li><strong>CSP constraint</strong> — <code>script-src cdn.jsdelivr.net</code> and <code>worker-src cdn.jsdelivr.net</code> become mandatory (Mermaid v11 uses Web Workers for its parsers).</li>
<li><strong>Render flash</strong> — the diagram appears after JS execution, creating a visible delay.</li>
<li><strong>Dark theme ignored</strong> — Mermaid initialized the SVG in light mode even when the site theme was dark.</li>
</ol>
<p>The solution: generate SVGs <strong>at build time</strong> with <code>mmdc</code>, outside any browser context.</p>]]></description></item><item><title>CSP A+ on Hugo + Cloudflare: from hash-based to origin allowlist, auto-monitoring and hardening</title><link>https://www.arleo.eu/en/posts/csp-a-plus-hugo-cloudflare-origin-allowlist/</link><pubDate>Thu, 28 May 2026 23:45:41 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/csp-a-plus-hugo-cloudflare-origin-allowlist/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/csp-a-plus-hugo-cloudflare-origin-allowlist-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="context">Context</h2>
<p>arleo.eu runs on Hugo (KVM VM) → OpenResty (NUC) → Cloudflare (CDN/WAF). Goal: <strong>A+ score on Mozilla Observatory</strong> with a strict CSP that resists edge-side injections.</p>
<p>The journey went through three strategies over a few weeks — nonces, hashes, then origin allowlist — before landing on a stable, automated solution.</p>
<hr>
<h2 id="act-1-why-hash-based-failed">Act 1: why hash-based failed</h2>
<p>The initial idea seemed solid: Hugo Pipes externalizes all JS with SRI, we list the hashes in the CSP, clean result. In practice, two problems made the approach impossible.</p>]]></description></item><item><title>hugo-mcp Cloudflare plugin: smart cache purge</title><link>https://www.arleo.eu/en/posts/hugo-mcp-plugin-cloudflare/</link><pubDate>Thu, 14 May 2026 09:43:19 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/hugo-mcp-plugin-cloudflare/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/hugo-mcp-plugin-cloudflare-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p>The Cloudflare plugin in <a href="https://github.com/jmrGrav/hugo-mcp/releases/tag/v2.0.0" target="_blank" rel="noopener noreffer ">hugo-mcp v2.0</a> implements 3 cache purge modes (<code>full</code>, <code>partial</code>, <code>smart</code>). The <code>partial</code> mode computes the linked URLs to invalidate (canonical + sitemap + RSS + listing + home) to preserve 95% of the CDN cache on every modification. Concretely: 6 URLs purged instead of wiping everything. This post details the computation, the pitfalls, and why <code>smart</code> became the default.</p>]]></description></item><item><title>NUC Security Audit: ModSecurity Removed, 500 MB Recovered</title><link>https://www.arleo.eu/en/posts/audit-securite-modsecurity-crowdsec/</link><pubDate>Thu, 14 May 2026 05:32:19 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/audit-securite-modsecurity-crowdsec/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/audit-securite-modsecurity-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-tldr">⚡ TL;DR</h2>
<p>A security stack audit on the homelab NUC reveals <strong>redundant double WAF inspection</strong>: ModSecurity + OWASP CRS load 11,872 rules into memory despite <code>SecRuleEngine Off</code>, running in parallel with CrowdSec AppSec which already covers the same surface. After removing the ModSecurity nginx module and five other targeted fixes, nginx drops from <strong>~520 MB to ~27 MB PSS</strong>. Same security, memory footprint divided by 20.</p>
<hr>
<h2 id="-architecture-before-the-audit">🏗️ Architecture Before the Audit</h2>
<p>The security stack had six stacked layers:</p>]]></description></item><item><title>hugo-mcp v2.0: a Python plugin-system in 200 lines</title><link>https://www.arleo.eu/en/posts/hugo-mcp-plugins-architecture/</link><pubDate>Sat, 09 May 2026 23:52:28 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/hugo-mcp-plugins-architecture/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/hugo-mcp-plugins-architecture-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p><a href="https://github.com/jmrGrav/hugo-mcp/releases/tag/v2.0.0" target="_blank" rel="noopener noreffer ">hugo-mcp v2.0.0</a> introduces a <strong>Python plugin-system</strong> that lets anyone add hooks after each <code>create_page</code> / <code>update_page</code> / <code>delete_page</code> operation. 200 lines of code for the core, 3 production plugins shipped (IndexNow, Google Indexing, Cloudflare). This post explains the design, the trade-offs, the security posture, and shows how to write your own plugin in 5 minutes.</p>]]></description></item><item><title>Grav → Hugo migration: 2 years of blog flipped in one day</title><link>https://www.arleo.eu/en/posts/migration-grav-hugo/</link><pubDate>Sat, 09 May 2026 22:33:22 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/migration-grav-hugo/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/migration-grav-hugo-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p>On May 9, 2026, I switched <code>arleo.eu</code> from <strong>Grav</strong> (PHP CMS) to <strong>Hugo</strong> (Go static site generator) in a single session. <strong>Atomic flip</strong> (≈ 0 second downtime), 22 legacy articles migrated under <code>/posts/</code> with <strong>SEO aliases</strong> to preserve Google-indexed URLs, BetterStack <code>/ping</code> monitoring intact throughout the operation.</p>
<p>The code and migration script are open source: <a href="https://github.com/jmrGrav/grav-to-hugo-migration" target="_blank" rel="noopener noreffer ">github.com/jmrGrav/grav-to-hugo-migration</a>.</p>]]></description></item><item><title>MCP security sprint delivered: v1.9.0, 10 chantiers, hardened ecosystem</title><link>https://www.arleo.eu/en/posts/sprint-securite-mcp-livre/</link><pubDate>Sat, 09 May 2026 18:44:12 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/sprint-securite-mcp-livre/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/sprint-securite-mcp-livre-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p>On May 9, 2026, I delivered all 10 chantiers of the MCP security sprint that <a href="/en/posts/roadmap-sprint-securite-mcp/" rel="">I had announced earlier in the day</a> in a single marathon session. <code>hugo-mcp</code> is now at <strong>v1.9.0</strong> (<a href="https://github.com/jmrGrav/hugo-mcp/releases/tag/v1.9.0" target="_blank" rel="noopener noreffer ">GitHub Release</a>), commit <code>1404f83</code> GPG-signed.</p>
<p>Here&rsquo;s the high-level recap + a pedagogical deep-dive on 2 chantiers with real value beyond my specific context: <strong>C2 token rotation</strong> and <strong>C6 internal TLS</strong>.</p>
<h2 id="recap-of-10-chantiers">Recap of 10 chantiers</h2>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>Chantier</th>
          <th>Implementation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>C1</td>
          <td>Rate limiting</td>
          <td><code>slowapi</code>, 60 req/min per IP</td>
      </tr>
      <tr>
          <td>C2</td>
          <td>Token rotation</td>
          <td><code>tokens.json</code> + <code>token_mgr.py</code> CLI</td>
      </tr>
      <tr>
          <td>C3</td>
          <td>JSON audit logs</td>
          <td><code>structlog</code>, machine-readable events</td>
      </tr>
      <tr>
          <td>C4</td>
          <td>Strict Pydantic v2</td>
          <td><code>CreatePageArgs</code> / <code>UpdatePageArgs</code> with constraints</td>
      </tr>
      <tr>
          <td>C5</td>
          <td>bcrypt cost-12</td>
          <td>Tokens hashed in storage</td>
      </tr>
      <tr>
          <td>C6</td>
          <td>NUC ↔ VM TLS</td>
          <td>EC P-256 cert, uvicorn SSL, proxy verifies the cert</td>
      </tr>
      <tr>
          <td>C7</td>
          <td>requirements.lock</td>
          <td>SHA-256 hashes via <code>pip-compile --generate-hashes</code></td>
      </tr>
      <tr>
          <td>C8</td>
          <td>Info disclosure</td>
          <td>Docs off, generic exception handler, <code>proxy_hide_header</code></td>
      </tr>
      <tr>
          <td>C9</td>
          <td>nginx WAF</td>
          <td>POST + <code>application/json</code> enforcement on <code>/mcp</code>, OWASP CRS active</td>
      </tr>
      <tr>
          <td>C10</td>
          <td>Backup DR</td>
          <td><code>backup.sh</code> GPG-encrypted, 30-day retention</td>
      </tr>
  </tbody>
</table>
<p>Full details in the <a href="https://github.com/jmrGrav/hugo-mcp/blob/main/CHANGELOG.md" target="_blank" rel="noopener noreffer ">CHANGELOG v1.9.0</a> and commit <a href="https://github.com/jmrGrav/hugo-mcp/commit/1404f83" target="_blank" rel="noopener noreffer ">1404f83</a>.</p>]]></description></item><item><title>Strategy 4: separating content (MCP) from structure (Git)</title><link>https://www.arleo.eu/en/posts/strategie-4-mcp-vs-git/</link><pubDate>Sat, 09 May 2026 13:03:25 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/strategie-4-mcp-vs-git/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/strategie-4-mcp-vs-git-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="the-problem">The problem</h2>
<p>You have a Hugo site. You want to:</p>
<ol>
<li><strong>Edit content via Claude.ai</strong> (publish an article, fix a typo, update a draft) without touching an SSH terminal.</li>
<li><strong>Version the structure</strong> (layouts, themes, hugo.toml, deploy scripts) in Git, like a serious dev.</li>
</ol>
<p>First instinct: &ldquo;everything in Git&rdquo;. Articles too. The MCP commits, pushes, GitHub webhook triggers a rebuild. Clean, GitOps-philosophy.</p>
<p>Except it doesn&rsquo;t work that well. Here&rsquo;s why, and the simple solution I call <strong>Strategy 4</strong>.</p>
<h2 id="why-everything-in-git-breaks-in-practice">Why &ldquo;everything in Git&rdquo; breaks in practice</h2>
<p>Imagine your MCP <code>git commit</code>s on every <code>create_page</code>. Naive strategy, often suggested. Here are the problems:</p>
<h3 id="problem-1--mcp--git-conflict">Problem 1 — MCP ↔ Git conflict</h3>
<p>You push a new layout from your laptop (<code>layouts/index.html</code> modified). At the same time, the MCP is committing a new article version. Race condition: the MCP might <code>git pull --rebase</code> and fail, or worse, overwrite your local commit.</p>
<h3 id="problem-2--mcps-git-identity">Problem 2 — MCP&rsquo;s Git identity</h3>
<p>Whose commits is the MCP making? With which GPG key? If you have a &ldquo;signed commits required&rdquo; policy, the MCP needs to manage a GPG key, which has to be secured, rotated, etc.</p>
<h3 id="problem-3--unwanted-auto-commits">Problem 3 — Unwanted auto-commits</h3>
<p>You&rsquo;re testing, you create a draft article to experiment, you delete it. But the MCP already committed. Now you have a &ldquo;wip test&rdquo; commit in history, to rebase or squash manually.</p>
<h3 id="problem-4--asymmetric-reversibility">Problem 4 — Asymmetric reversibility</h3>
<p>A <code>git revert</code> repo-side has no effect on files the MCP already created. You end up with a desynchronized repo and filesystem state.</p>
<h2 id="strategy-4-separate-the-zones">Strategy 4: separate the zones</h2>
<p>The idea: <strong>MCP and Git never write to the same files</strong>.</p>
<table>
  <thead>
      <tr>
          <th>Zone</th>
          <th>Who edits</th>
          <th>Versioned in Git?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>content/**/*.md</code></td>
          <td><strong>MCP only</strong></td>
          <td>❌ NO (<code>.gitignore</code>)</td>
      </tr>
      <tr>
          <td><code>layouts/</code>, <code>themes/</code>, <code>static/</code>, <code>hugo.toml</code>, <code>deploy.sh</code></td>
          <td><strong>Git push only</strong></td>
          <td>✅ YES</td>
      </tr>
  </tbody>
</table>
<p>Repo-side <code>.gitignore</code>:</p>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><pre tabindex="0"><code>content/
public/
resources/</code></pre></div>
<p>Implications:</p>
<ul>
<li>The MCP can write to <code>content/</code> whenever. No Git conflict possible.</li>
<li>You can <code>git reset --hard</code> repo-side with confidence — <code>content/</code> stays intact.</li>
<li>No need for the MCP to manage a Git identity.</li>
<li>No &ldquo;commit pollution&rdquo;.</li>
</ul>
<h2 id="trade-off-no-content-versioning">Trade-off: no content versioning</h2>
<p>You lose Git versioning of content. That&rsquo;s a real loss:</p>
<ul>
<li>No <code>git blame</code> on an article to see who wrote what.</li>
<li>No <code>git log content/csp-nonce/index.fr.md</code> for history.</li>
<li>No PR review for articles.</li>
</ul>
<p><strong>Mitigation</strong>: VM snapshots + daily encrypted <code>content/</code> backup to QNAP. That covers <strong>disaster recovery</strong>, but not fine-grained versioning (who-changed-what-when).</p>
<p>For my personal homelab (single author, technical articles, no editorial validation workflow), it&rsquo;s an acceptable trade-off. For a 10-contributor team blog, I&rsquo;d reconsider.</p>
<h2 id="final-architecture">Final architecture</h2>]]></description></item><item><title>systemd hardening: taking a Python service from 9.6 to 1.7</title><link>https://www.arleo.eu/en/posts/hardening-systemd-mcp/</link><pubDate>Sat, 09 May 2026 13:02:29 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/hardening-systemd-mcp/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/hardening-systemd-mcp-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p><code>systemd-analyze security</code> is an underused tool. It scans your unit files and computes an exposure score from <strong>0.0 (UNSAFE)</strong> to <strong>10.0 (PERFECT)</strong>. Custom Python services often score around <strong>9.6</strong> by default — that&rsquo;s bad.</p>
<p>I took my <code>hugo-mcp</code> service (FastAPI exposing 7 MCP tools) from <strong>9.6 → 1.7</strong> without breaking a single feature. Here are the directives that actually matter, and the ones that are traps.</p>
<h2 id="initial-score">Initial score</h2>]]></description></item></channel></rss>