/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.

nginx/CrowdSec Logs: Normalize Them with Vector

In short

Two problems coexisted in BetterStack: mcp-oauth.access.log logs arrived as unreadable raw JSON, and CrowdSec logs produced visual duplicates. This work normalizes all logs so they display as structured clickable tags, with correct timestamps and without parasitic fields.

🧠 Why

BetterStack displays logs as highlighted clickable tags in the Live Tail when JSON fields are properly structured. Before this work, observation was degraded on two fronts:

  • mcp-oauth.access.log logs arrived as unreadable raw JSON (custom format incompatible with Vector’s nginx parser) — fields nginx.client, nginx.path, nginx.status were not extracted
  • CrowdSec and CF WAF logs arrived as plain text with duplicates (Ban ban | ... | Ban ban)

The goal was to normalize all logs in BetterStack to display like standard nginx logs:

Automated IP Bans: CrowdSec, Cloudflare and AbuseIPDB

In short

Passive monitoring is not enough. This pipeline automates closing the loop in under 5 minutes 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.

🧠 Why

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 (block, challenge, managed_challenge, jschallenge) 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.

Post-Mortem — Incident 522 / WAN Failover (April 8, 2026)

What happened
  • Date: April 7-8, 2026
  • Duration: ~3h (21:28 UTC → 22:31 UTC)
  • Severity: P1
  • Status: Resolved
  • Impact: arleo.eu unreachable for 3 hours — an HTTPS port forwarding rule attached to generic WAN instead of explicit WAN1 on the Netgear PR60X

In short

arleo.eu was unreachable for 3 hours. The root cause was not the server, not nginx, not CrowdSec — it was an HTTPS port forwarding rule attached to generic WAN instead of explicit WAN1 on the Netgear PR60X. The daily DHCP lease renewal of the 4G modem (WAN2) triggered a NAT rebalance that broke routing to port 443.

jQuery → Vanilla ES6 Migration

In short

This site was migrated from jQuery to native JavaScript (Vanilla ES6). An 87 KB external dependency removed, replaced by modern code that does exactly the same thing — no extra download, no third-party library.

For visitors, nothing changes. For security and performance, it’s a measurable improvement.

🧠 Why

jQuery was essential in 2010 when browsers all behaved differently. In 2026, modern browsers natively support everything jQuery did:

  • $(selector)document.querySelector()
  • $.ajax()fetch()
  • $(el).on('click', fn)el.addEventListener('click', fn)
  • $(document).ready()document.addEventListener('DOMContentLoaded', fn)

Keeping jQuery today means loading 87 KB of code you no longer need.

Hugo