<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Security - Tag - arleo.eu</title><link>https://www.arleo.eu/en/tags/security/</link><description>Security - Tag - arleo.eu</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 28 May 2026 23:45:41 +0200</lastBuildDate><atom:link href="https://www.arleo.eu/en/tags/security/" rel="self" type="application/rss+xml"/><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>CrowdSec AppSec + OpenResty: Modern WAF Without ModSecurity</title><link>https://www.arleo.eu/en/posts/crowdsec-appsec-openresty/</link><pubDate>Mon, 18 May 2026 00:07:55 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/crowdsec-appsec-openresty/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/crowdsec-appsec-openresty-featured.jpg" referrerpolicy="no-referrer">
            </div><p>After years running ModSecurity + OWASP CRS on nginx, I migrated arleo.eu to a more modern stack: <strong>CrowdSec AppSec on OpenResty</strong>. The result is a tighter inline WAF architecture — better integrated, easier to maintain, and fully coherent with the rest of the security stack.</p>
<h2 id="why-drop-modsecurity">Why Drop ModSecurity?</h2>
<p>ModSecurity v2 is in maintenance mode. Managing OWASP CRS rules on classic nginx generates friction: frequent false positives, logs that are hard to correlate with CrowdSec, and a configuration spread across multiple tools with no unified view.</p>]]></description></item><item><title>SRI on Hugo: automated hashes, auto-update and BetterStack alerting</title><link>https://www.arleo.eu/en/posts/sri-cdn-hugo-automate/</link><pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/sri-cdn-hugo-automate/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/sri-cdn-hugo-automate-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="why-sri">Why SRI?</h2>
<p>When your site loads resources from a third-party CDN — FontAwesome, Mermaid, Animate.css — you&rsquo;re trusting an external party you have no control over. If jsdelivr.net gets compromised, or if a supposedly immutable version is silently mutated, your site can become an attack vector.</p>
<p><strong>Subresource Integrity</strong> (SRI) solves this cleanly: every <code>&lt;link&gt;</code> or <code>&lt;script&gt;</code> tag carries an <code>integrity=&quot;sha256-…&quot;</code> attribute that the browser verifies before executing the resource. If the hash doesn&rsquo;t match, the browser blocks the load.</p>]]></description></item><item><title>CSP Hash on Hugo: migrating from nonce to hash to preserve CDN cache</title><link>https://www.arleo.eu/en/posts/csp-hash-hugo/</link><pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/csp-hash-hugo/</guid><description>&lt;div class="featured-image">
                &lt;img src="/images/csp-hash-hugo-featured.png" referrerpolicy="no-referrer">
            &lt;/div>CSP nonces and CDN caching are incompatible by design. On a Hugo static site, SHA-256 hashes are the native approach: computed at build time, stable across requests, and fully compatible with Cloudflare caching.</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>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>Roadmap: MCP Security Sprint — 4 domains, 10 chantiers (DELIVERED ✅)</title><link>https://www.arleo.eu/en/posts/roadmap-sprint-securite-mcp/</link><pubDate>Sat, 09 May 2026 13:07:33 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/roadmap-sprint-securite-mcp/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/roadmap-sprint-securite-mcp-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="status--delivered--sprint-closed-on-may-9-2026">Status: ✅ DELIVERED — sprint closed on May 9, 2026</h2>
<p><strong>Update May 9, 2026 (end of session)</strong>: all 10 chantiers delivered in a single marathon session, <strong>and all 3 coordinated releases published the same day</strong> (<code>hugo-mcp</code> v1.9.0, <code>mcp-oauth-proxy</code> v2.1.0, <code>mcp-installer</code> v1.3.0). Every component of the MCP ecosystem hardened in one day. Full technical recap: <a href="/en/posts/sprint-securite-mcp-livre/" rel="">MCP security sprint delivered: v1.9.0, 10 chantiers, hardened ecosystem</a>.</p>
<p>This page stays published as an archive — to show the trajectory of an announced sprint, then kept. All original content below is preserved.</p>
<hr>
<h2 id="original-status-pre-delivery">Original status (pre-delivery)</h2>
<p>This page publicly documented an ongoing hardening sprint on arleo.eu&rsquo;s MCP infrastructure. For security reasons, specific details of each chantier were not exposed until fixes were delivered (&ldquo;sec-first&rdquo; philosophy: don&rsquo;t publish an attack roadmap).</p>
<h2 id="why-this-transparency">Why this transparency</h2>
<p>I debated whether to publish this page. Arguments for transparency:</p>
<ul>
<li><strong>Public commitment</strong> = healthy pressure on yourself to deliver</li>
<li><strong>Documentation</strong> of a homelab whose goal is to learn and share</li>
<li><strong>Honesty</strong> with readers consuming other technical articles</li>
</ul>
<p>Arguments against:</p>
<ul>
<li><strong>Attack roadmap</strong>: if I precisely list what&rsquo;s not yet protected, I give clues to a patient attacker</li>
<li><strong>Artificial pressure</strong>: announcing a sprint then not delivering = worse than announcing nothing</li>
</ul>
<p>Adopted compromise: publish the <strong>direction</strong> and <strong>hardening domains</strong> without specifying what&rsquo;s weak today. Specific technical detail published on delivery.</p>
<h2 id="sprint-scope">Sprint scope</h2>
<p>The sprint covered 10 chantiers grouped into 4 domains:</p>
<h3 id="1-application-hardening-fastapi--pydantic">1. Application hardening (FastAPI + Pydantic)</h3>
<p>Reinforcement of MCP entry layers: strict input validation, unified error handling, no information leak in responses (stack traces, internal paths, lib versions).</p>
<h3 id="2-authentication-and-tokens">2. Authentication and tokens</h3>
<p>Refactor of MCP access token management: lifetime, rotation, revocation, hashing in storage. Allow cutting off a compromised client&rsquo;s access without redeploying the service.</p>
<h3 id="3-observability-and-audit">3. Observability and audit</h3>
<p>Integration of JSON structured logs ingested in BetterStack via Vector. Each MCP call must produce a traceable event: who, what, when, duration, status. Enables anomaly detection in near real-time.</p>
<h3 id="4-infrastructure-and-resilience">4. Infrastructure and resilience</h3>
<p>TLS for internal traffic between NUC and VM, dedicated ModSec rules on the <code>/mcp</code> path, disaster recovery runbook (token theft, server compromise, data loss).</p>
<h2 id="what-was-already-in-place-before-the-sprint">What was already in place before the sprint</h2>
<p>To not create false impressions, here are the layers <strong>already in place</strong> on infrastructure before the sprint (so out of scope):</p>
<ul>
<li>nginx + mandatory TLS 1.3 (Mozilla Modern config)</li>
<li>Cloudflare WAF + Bot Management + IP whitelist</li>
<li>ModSecurity + OWASP CRS 4.x on all vhosts</li>
<li>CrowdSec in WAF mode + Cloudflare bouncer</li>
<li>systemd hardening at level 1.7 (cf. <a href="/en/posts/hardening-systemd-mcp/" rel="">systemd hardening</a>)</li>
<li>HMAC validation on webhooks</li>
<li>Frontmatter Pydantic validation (1 chantier of 4 in the validation category)</li>
</ul>
<p>The sprint aimed to <strong>complete</strong> this base, not replace it.</p>
<h2 id="method">Method</h2>
<p>For each chantier:</p>
<ol>
<li>Implemented locally + unit tests</li>
<li>Validated on <code>mcp-test-vm</code> (pre-prod VM)</li>
<li>Deployed to prod only after passing tests</li>
<li>Structured JSON audit log for deployment traceability</li>
<li>Post-mortem or technical write-up published <strong>after</strong> delivery</li>
</ol>
<p>No direct prod deployment without pre-prod step.</p>
<h2 id="published-releases">Published releases</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><item><title>From 46 Hashes to Zero: Migrating CSP to Dynamic Nonces</title><link>https://www.arleo.eu/en/posts/csp-nonce/</link><pubDate>Wed, 15 Apr 2026 13:22:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/csp-nonce/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/csp-nonce-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-in-short">⚡ In short</h2>
<p>The original CSP listed 46 SHA-256 hashes to cover inline scripts and styles — unmanageable, fragile, and approaching Cloudflare&rsquo;s 4,096 character limit. This migration to <strong>dynamic nonces</strong> reduces the CSP to ~600 characters, eliminates all manual hash maintenance, and adds structured violation reporting in BetterStack.</p>
<p>The Grav plugin powering this is available on GitHub:
🔌 Plugin : <a href="https://github.com/jmrGrav/grav-plugin-csp-nonce" target="_blank" rel="noopener noreffer ">jmrGrav/grav-plugin-csp-nonce</a></p>
<h2 id="-why">🧠 Why</h2>
<p>When implementing a strict Content Security Policy on a Grav CMS site served through Cloudflare, the naive approach is to list SHA-256 hashes for every inline script. It works, but quickly becomes unmanageable. Several compounding problems:</p>]]></description></item></channel></rss>