<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Cloudflare - Tag - arleo.eu</title><link>https://www.arleo.eu/en/tags/cloudflare/</link><description>Cloudflare - Tag - 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/tags/cloudflare/" 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>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>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>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>Post-mortem: Cloudflare Bot Management blocked MCP webhooks</title><link>https://www.arleo.eu/en/posts/postmortem-cf-bot-blocking-mcp/</link><pubDate>Sat, 09 May 2026 13:06:02 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/postmortem-cf-bot-blocking-mcp/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/postmortem-cf-bot-blocking-mcp-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="the-symptom">The symptom</h2>
<p>I just finished a webhook endpoint in <code>hugo-mcp-proxy</code> that will receive notifications from GitHub on every push to the arleo.eu repo. Clean implementation: HMAC-SHA256, rate limiting, IPAddressAllow GitHub ranges in systemd.</p>
<p>Functional test from an external client:</p>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-bash">
        <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><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ curl -X POST https://mcp-hugo.arleo.eu/webhook/test <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -d <span class="s1">&#39;{&#34;test&#34;: true}&#39;</span></span></span></code></pre></div></div>
<p>Response: <strong>403 Forbidden</strong>.</p>
<p>Strange. The service is running, my source IP is whitelisted, the HMAC is correct. Why 403?</p>
<h2 id="server-side-investigation">Server-side investigation</h2>
<p>NUC nginx logs:</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>$ sudo tail -100 /var/log/nginx/mcp-hugo.access.log | grep webhook</code></pre></div>
<p>Empty. No request reaches nginx.</p>
<p><code>mcp-oauth-proxy</code> logs:</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>$ sudo journalctl -u mcp-oauth-proxy -n 100 | grep webhook</code></pre></div>
<p>Empty too. The request doesn&rsquo;t even reach the service.</p>
<p>Either it&rsquo;s blocked by the firewall before nginx (CrowdSec or ufw), or upstream by Cloudflare.</p>
<h2 id="the-truth-at-cloudflare">The truth at Cloudflare</h2>]]></description></item><item><title>Post-mortem: 3 MCP timeouts — IPAddressDeny + Cloudflare + NFS</title><link>https://www.arleo.eu/en/posts/postmortem-mcp-timeouts-cloudflare/</link><pubDate>Sat, 09 May 2026 13:04:20 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/postmortem-mcp-timeouts-cloudflare/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/postmortem-mcp-timeouts-cloudflare-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="context">Context</h2>
<p>I deployed a Hugo MCP Server (FastAPI, 7 tools) that lets me edit arleo.eu from Claude.ai. Architecture: <code>claude.ai → mcp-oauth-proxy NUC → hugo-mcp-proxy NUC → MCP server VM</code>.</p>]]></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><item><title>Infrastructure Scripts</title><link>https://www.arleo.eu/en/scripts/</link><pubDate>Sun, 12 Apr 2026 21:25:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/scripts/</guid><description><![CDATA[<h2 id="1-crowdsec-cf-syncpy">1. crowdsec-cf-sync.py</h2>
<p><strong>Location:</strong> <code>/usr/local/bin/crowdsec-cf-sync.py</code>
<strong>Systemd service:</strong> <code>crowdsec-cf-sync.service</code>
<strong>Logs:</strong> <code>/var/log/crowdsec/cf-sync.log</code></p>
<h3 id="installation">Installation</h3>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-bash">
        <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><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cp crowdsec-cf-sync.py /usr/local/bin/
</span></span><span class="line"><span class="cl">chmod +x /usr/local/bin/crowdsec-cf-sync.py
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> crowdsec-cf-sync
</span></span><span class="line"><span class="cl">systemctl start crowdsec-cf-sync</span></span></code></pre></div></div>
<h3 id="features">Features</h3>
<ul>
<li>Syncs active CrowdSec bans → Cloudflare IP Access Rules (tag <code>crowdsec-local-ban</code>)</li>
<li>Reports banned IPs → AbuseIPDB (48h window, deduplicated)</li>
<li><strong>Repeat-offender escalation</strong>: 1st CrowdSec ban handles | 2nd → 24h | 3rd+ → 7d (7d window)</li>
<li><strong>ModSecurity</strong> score ≥ 5 → immediate 2h CF ban (tag <code>modsec-ban</code>) + AbuseIPDB report</li>
<li><strong>Automatic /24 ban</strong>: 2+ IPs from same block in 7d → CF + CrowdSec ban 24h (tag <code>crowdsec-cidr-ban</code>)</li>
</ul>
<h3 id="bug-fixes-april-2026">Bug fixes (April 2026)</h3>
<ul>
<li><code>cs_origin</code> vs <code>origin</code> in <code>get_recent_local_bans()</code> — the JSON field is <code>cs_origin</code></li>
<li>REST API pagination <code>/v1/decisions?limit=1000</code> misses <code>cscli</code> bans → replaced with <code>cscli decisions list --origin</code></li>
<li><code>Items</code> → <code>items</code> (lowercase) in CrowdSec allowlist JSON parsing</li>
</ul>
<blockquote>
<p>⚠️ <strong>Important</strong>: real tokens (<code>CF_API_TOKEN</code>, <code>CF_ZONE_ID</code>, <code>CS_API_KEY</code>, <code>ABUSEIPDB_KEY</code>) must be stored in <code>/etc/secrets/</code> with <code>chmod 600</code> and loaded via environment variables, not hardcoded in the script. The <code>VOTRE_*</code> values below are placeholders.</p>]]></description></item><item><title>Automating IP Bans with Cloudflare WAF, CrowdSec and AbuseIPDB</title><link>https://www.arleo.eu/en/posts/crowdsec-cloudflare-waf-autoban/</link><pubDate>Fri, 10 Apr 2026 00:23:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/crowdsec-cloudflare-waf-autoban/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/crowdsec-cloudflare-waf-autoban-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-in-short">⚡ In short</h2>
<p>Passive monitoring is not enough. This pipeline automates <strong>closing the loop in under 5 minutes</strong> between an attack detected by Cloudflare WAF and the effective ban of the IP in CrowdSec, its synchronization to Cloudflare, and its report to AbuseIPDB. A Python script polls the Cloudflare GraphQL API every 5 minutes, applies a 3-hit threshold, and triggers the ban with recidivist escalation.</p>
<h2 id="-why">🧠 Why</h2>
<p>Seeing an attack in BetterStack logs after the fact does not stop the malicious IP from continuing to hammer the server. Without automation, the detection → ban loop takes hours or never closes. Cloudflare WAF actions (<code>block</code>, <code>challenge</code>, <code>managed_challenge</code>, <code>jschallenge</code>) are clear attack signals, but they remain confined to the Cloudflare dashboard — without a bridge to CrowdSec, no IP is banned locally, none is reported to the AbuseIPDB community.</p>]]></description></item></channel></rss>