/images/avatar.png

This site runs on an Intel NUC hosted at home, behind a standard fiber connection. Its main purpose is to serve as an experimentation ground for testing server configurations, automation scripts, and open source security tools.

Not a professional website — a homelab: we break things, fix them, and learn.

🌟 Don’t miss

Three articles that capture the spirit of this homelab:

📚 Full documentation is in Documentation and automation scripts in Scripts.

🛠️ Tech stack

ToolRole
OpenRestyReverse proxy · nginx + LuaJIT · TLS 1.3
🔒 CrowdSecCommunity IDS/IPS
🧱 CrowdSec AppSecInline WAF · OWASP CRS 4.x
☁️ CloudflareCDN · WAF · DNS · DDoS
🌐 HugoStatic site generator
🤖 MCP Hugo ServerMCP server to drive this site from an AI
📊 BetterStackMonitoring · Alerts · Logs

🐛 Found a vulnerability?

If you discover a bug, misconfiguration, or security vulnerability on this server, please report it. This homelab is public and I learn from my mistakes.

📨 Responsible disclosure: www.arleo.eu/security.txt

Reports are publicly credited in the Hall of Fame, or kept anonymous on request.

Any contribution to improving security is welcome.

Migration in progress
The site is gradually migrating from Grav CMS to Hugo. Old URLs are preserved, but the visual rendering is evolving. If you spot a bug, report it.

Hugo on KVM: Installing an Ubuntu VM for a Static Site

In short

Progressive migration from Grav CMS to Hugo — a static site generator. The goal is to isolate Hugo in a dedicated KVM VM on the NUC8i3BEH, with nginx on the host as a reverse proxy. The generated static site is served by nginx inside the VM — no PHP, no database, no application attack surface.

  • 🖥️ Host: NUC8i3BEH Ubuntu 24.04 — nginx proxy + KVM
  • 🗄️ VM disk: Samsung X5 external NVMe (exFAT → ext4 loop image)
  • 🌐 Access: hugo-test.arleo.eu
  • 🎨 Theme: LoveIt

🧠 Why

Grav CMS is excellent but relies on PHP — a non-negligible attack surface. Hugo generates pure static HTML: no PHP, no database, no application vulnerability. Performance is also radically better — HTML is served directly by nginx without dynamic processing.

Grav MCP Server: Connect Claude.ai to Grav

In short

Connect Claude.ai to your Grav website in 30 minutes: a PHP plugin exposes 9 tools (read, create, update, delete pages, manage plugins) via JSON-RPC 2.0, a FastAPI proxy handles OAuth 2.1 authentication, and nginx bridges the internet to your server. Result: Claude can read and modify your site content directly from a conversation.

Code available on GitHub:

After the site migrated to Hugo, the evolution of this architecture is documented in Hugo MCP Server: connect Claude.ai to Hugo.

Grav Google Indexing Plugin: Automatic Submission

In short

Following the IndexNow plugin (Bing/Yandex), this second plugin completes the SEO pipeline by submitting modified pages directly to the Google Indexing API — with no Composer dependency, using a RS256 JWT signed in pure PHP from a Google Cloud service account.

Source code available on GitHub:

🧠 Why

IndexNow covers Bing and Yandex, but Google does not support IndexNow. To notify Google instantly, you need to use its dedicated API: Google Indexing API v3.

Grav IndexNow: Automatic Search Indexing

In short

Grav has no IndexNow plugin in its official catalog. This homemade plugin fills the gap by automatically submitting modified URLs to api.indexnow.org on every page save — whether through the Grav admin or the MCP plugin — no manual intervention, no cron job, no external dependency.

Source code available on GitHub:

For Google, which does not support IndexNow, the companion is the Grav Google Indexing plugin.

CrowdSec with Vector: Filter Noise, Capture Bans

In short

The initial Vector pipeline was flooding BetterStack with ~500 events/24h, of which 434 were CAPI pulls with no local monitoring value. This work reconfigures the Vector filter to keep only high-value bans (cscli) and fixes a major blind spot: actual nginx-lua bouncer bans were not appearing anywhere in BetterStack.

🧠 Why

This homelab’s security stack relies on three components working together:

  • nginx with the CrowdSec lua bouncer (lua-resty-crowdsec) for real-time request blocking
  • CrowdSec for threat detection and ban decision management
  • Vector centralizing logs to BetterStack for monitoring

After setting up the initial pipeline, two problems quickly became apparent. First, the signal was drowned in noise: out of 500 events/24h, 434 came from the hourly community CAPI pull and 66 from third-party lists — neither represents a threat detected on this infrastructure. Second, actual lua bouncer bans (real-time blocks in nginx) were not appearing anywhere in BetterStack, creating a blind spot on real security activity.

From 46 Hashes to Zero: Dynamic CSP Nonces

In short

The original CSP listed 46 SHA-256 hashes to cover inline scripts and styles — unmanageable, fragile, and approaching Cloudflare’s 4,096 character limit. This migration to dynamic nonces reduces the CSP to ~600 characters, eliminates all manual hash maintenance, and adds structured violation reporting in BetterStack.

The Grav plugin powering this is available on GitHub: 🔌 Plugin : jmrGrav/grav-plugin-csp-nonce

🧠 Why

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:

Hugo