1. Security Architecture (10 layers)
| # | Layer | Technology |
|---|---|---|
| 1 | DNS | DNSSEC ECDSA P256-SHA256 |
| 2 | Cloud CDN + WAF | Cloudflare WAF + DDoS + AI Crawl Control |
| 3 | Network | CrowdSec nftables Bouncer |
| 4 | Firewall | Netgear PR60X SPI |
| 5 | Local WAF | ModSecurity + OWASP CRS 4.x |
| 6 | IDS/IPS | CrowdSec Agent + SSH/HTTP scenarios |
| 7 | HTTPS | TLS 1.3 + HSTS preload |
| 8 | DNS-TLS | DoH port 853 |
| 9 | Application | Grav CMS + CSP + Secure cookies |
| 10 | Monitoring | BetterStack + CrowdSec poller + Vector |
2. CrowdSec → Cloudflare Sync (crowdsec-cf-sync.py)
Location: /usr/local/bin/crowdsec-cf-sync.py
Service: crowdsec-cf-sync.service
Logs: /var/log/crowdsec/cf-sync.log
Features
- Syncs active CrowdSec bans → Cloudflare IP Access Rules
- Reports banned IPs → AbuseIPDB
- Repeat-offender escalation: 1st ban handled by CrowdSec | 2nd → 24h | 3rd+ → 7d
- ModSecurity score ≥ 5 → immediate 2h CF ban + AbuseIPDB report
- Automatic /24 ban: 2+ IPs from the same block within 7d → 24h
State files
| File | Role |
|---|---|
/var/log/crowdsec/abuseipdb-reported.json | IPs already reported to AbuseIPDB |
/var/log/crowdsec/recidivists.json | Repeat-offender counter per IP |
/var/log/crowdsec/modsec-banned.json | Active ModSec bans |
/var/log/crowdsec/cidr-banned.json | /24 banned blocks |
Cloudflare tags
| Tag | Source | Duration |
|---|---|---|
crowdsec-local-ban | CrowdSec/cscli | Per escalation |
modsec-ban | ModSecurity score ≥ 5 | 2h |
crowdsec-cidr-ban | 2+ IPs same /24 | 24h |
Bug fixes (April 2026)
cs_originvsorigininget_recent_local_bans()- REST API pagination → replaced by
cscli decisions list --origin Items→items(lowercase) for CrowdSec allowlist
Useful commands
# Service status
systemctl status crowdsec-cf-sync
# Real-time logs
journalctl -fu crowdsec-cf-sync | grep -E "bans|Added|CIDR|ModSec|RECIDIVIST"
# Restart
systemctl restart crowdsec-cf-sync
# Show repeat offenders
cat /var/log/crowdsec/recidivists.json | python3 -c "
import json,sys
d=json.load(sys.stdin)
for ip,v in sorted(d.items(), key=lambda x: x[1]['count'], reverse=True):
print(f'{ip:20} count={v[\"count\"]} last={v[\"last_seen\"]}')
"3. CrowdSec Allowlist
Name: my_allowlist
Sync: cloudflare-allowlist-update.py script (hourly cron)
Sources: BetterStack IPs + Cloudflare IPv4/IPv6
# Inspect contents
cscli allowlists inspect my_allowlist
# Count entries
cscli allowlists list4. Nginx — CSP (Content Security Policy)
File: /etc/nginx/nginx.conf ($csp_header map)
Public CSP (default)
V2 migration: dynamic nonces. See From 46 hashes to zero for details.
httpoxy fix (April 2026)
Added in /etc/nginx/fastcgi.conf:
fastcgi_param HTTP_PROXY "";5. Hugo CMS
Root: /home/jm/hugo-site/ (KVM VM 192.168.122.69)
Theme: LoveIt
Build: hugo --minify via deploy.sh
Deployment: rsync to /var/www/hugo/ (NUC host)
Important files
| File | Role |
|---|---|
hugo.toml | Hugo config (baseURL, languages, theme) |
content/<route>/index.{fr,en}.md | Bilingual pages |
themes/LoveIt/ | Theme (git submodule) |
static/ | Static assets |
deploy.sh | Build + rsync + CF purge |
Useful commands
# Local build for testing
cd ~/hugo-site && hugo --minify
# Full deployment
~/deploy.sh
# Run dev server
hugo server -D --bind 0.0.0.06. Cloudflare
Security rules (order)
| # | Name | Action |
|---|---|---|
| 1 | GOOD BITS | Skip (allowlist) |
| 2 | BLOCK ALL BAD | Block |
| 3 | AI Crawl Control | Block (auto-managed) |
| 4 | Filter NON EU/US | Challenge |
GOOD BITS — conditions
- BetterStack Uptime Bot
- Legitimate bot categories (Search Engine, Monitoring, Security…)
- CloudflareBrowserRenderingCrawler
ip.src in $allowed_ip
AI Crawl Control — blocked bots
GPTBot, ClaudeBot, Bytespider, CCBot, ChatGPT-User, FacebookBot, Meta-ExternalAgent, Perplexity, MistralAI, OAI-SearchBot, AmazonBot…
💡 Note: BingBot passes via GOOD BITS (Search Engine Crawler). 💡 Note: DeepSeekBot blocked by Filter NON EU/US (China-hosted).
IP access rules (dynamic)
Managed automatically by crowdsec-cf-sync.py:
crowdsec-local-ban— local CrowdSec bansmodsec-ban— ModSecurity 2h banscrowdsec-cidr-ban— /24 blocks
7. Monitoring & Alerts
BetterStack: status.arleo.eu
Footer badge: https://status.arleo.eu/en/badge
Cloudflare Analytics token: stored in /etc/secrets/ (not published)
8. SSL Certificates
Managed automatically by Cloudflare (auto-renewal). Qualys SSL Labs: A+
9. Points of attention
- The Cloudflare challenge inline script (
__CF$cv$params) changes on every request → unavoidable CSP error, non-blocking - The CrowdSec allowlist (
my_allowlist) is the single source of truth — synced to Cloudflare csclibans (repeat-offender escalation) are NOT visible in the CrowdSec REST API with?limit=1000— usecscli decisions list --origin cscli- Cloudflare token and AbuseIPDB credentials are stored outside the repo in
/etc/secrets/withchmod 600