Security Stack: Caddy + CrowdSec
1. What? — Definition and context
Section titled “1. What? — Definition and context”The Security Stack is the protective layer that sits between the Internet and every service in the infrastructure. It combines two components:
| Component | Role | Port |
|---|---|---|
| Caddy | Reverse proxy with automatic TLS and HTTP/3 | 80, 443, 443/udp |
| CrowdSec | Behavioural WAF and collaborative threat intelligence | 8080 (LAPI), 7422 (AppSec) |
| Trivy | CVE scanner for Docker images, called via docker exec | no exposed port |
Architecture diagram
Section titled “Architecture diagram”2. Why? — Stakes and motivations
Section titled “2. Why? — Stakes and motivations”Why Caddy rather than Nginx or Traefik?
Section titled “Why Caddy rather than Nginx or Traefik?”| Criterion | Caddy | Nginx | Traefik |
|---|---|---|---|
| Automatic TLS | Native (Let’s Encrypt) | External Certbot | Native |
| HTTP/3 QUIC | Native | Experimental module | No |
| Configuration | Simple Caddyfile | Verbose nginx.conf | YAML/labels |
| Reload | Zero-downtime | Graceful reload | Zero-downtime |
| CrowdSec | Official bouncer module | Lua module | Third-party plugin |
Decision drivers:
- Simplicity — Caddyfile syntax is short and readable
- Zero-config TLS — Let’s Encrypt certificates handled automatically, no Certbot cron
- Native CrowdSec integration — Official bouncer module, no workaround
What does CrowdSec bring?
Section titled “What does CrowdSec bring?”| Function | Benefit |
|---|---|
| Collaborative threat intelligence | Malicious IPs detected by other CrowdSec instances are shared |
| Detection scenarios | Ready-made rules for HTTP, SSH, well-known CVEs |
| Real-time bouncer | Banned IPs are blocked before reaching the backend |
| Automatic whitelists | Legitimate bots (Googlebot, etc.) are not blocked |
3. How? — Technical implementation
Section titled “3. How? — Technical implementation”Caddyfile configuration
Section titled “Caddyfile configuration”{ # Global options email admin@guigpap.com
# CrowdSec module crowdsec { api_url http://crowdsec:8080 api_key {env.CROWDSEC_API_KEY} ticker_interval 15s }}
# Redirect www → apexwww.guigpap.com { redir https://guigpap.com{uri} permanent}
# Main siteguigpap.com { crowdsec
header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" }
reverse_proxy blog:4321}
# N8N with protected webhooksn8n.guigpap.com { crowdsec
# Block internal webhooks from the public Internet @blocked_webhooks { path /webhook/notify/* path /webhook/prometheus/* path /webhook/claude/* path /webhook/claude-simple-chat path /webhook/docker-controller path /webhook/odoo/* } respond @blocked_webhooks 403
reverse_proxy n8n:5678}
# Other servicesgrafana.guigpap.com { crowdsec reverse_proxy grafana:3000}
odoo.guigpap.com { crowdsec reverse_proxy odoo:8069}Installed CrowdSec collections
Section titled “Installed CrowdSec collections”# List active collectionsdocker exec crowdsec cscli collections list| Collection | Role |
|---|---|
crowdsecurity/caddy | Caddy log parser |
crowdsecurity/base-http-scenarios | Common HTTP attack detection |
crowdsecurity/http-cve | Known CVE exploits |
crowdsecurity/appsec-virtual-patching | Virtual patching |
crowdsecurity/linux | Linux system scenarios |
crowdsecurity/sshd | SSH brute-force detection |
crowdsecurity/whitelist-good-actors | Whitelist for legitimate bots |
How the bouncer works
Section titled “How the bouncer works”- Caddy receives a request
- The bouncer module queries the CrowdSec API (local cache)
- If the IP appears in the decision list → 403 Forbidden
- Otherwise → request forwarded to the backend
Internal webhook protection
Section titled “Internal webhook protection”| Webhook | Called by | Blocked from Internet |
|---|---|---|
/webhook/notify/* | DIUN | Yes |
/webhook/prometheus/* | Alertmanager | Yes |
/webhook/claude/* | CLI Ollama | Yes |
/webhook/claude-simple-chat, /webhook/claude-approve, /webhook/claude-reject | N8N | Yes |
/webhook/odoo/* | Odoo internal | Yes |
/webhook/docker-controller | Telegram Orchestrator | Yes |
/webhook/notification-hub | N8N (Execute Workflow) | Yes |
Internal services call these webhooks via http://n8n:5678 (Docker network), not the public domain.
Rate limiting and AppSec WAF
Section titled “Rate limiting and AppSec WAF”The webhook routes on n8n.guigpap.com are rate-limited to 30 requests/minute per IP through the caddy-ratelimit module. This complements the auth (Bearer / Header / HMAC) by preventing brute-force on the exposed endpoints. CrowdSec AppSec listens in parallel on port 7422 and replays the crowdsecurity/appsec-virtual-patching + appsec-generic-rules rules before requests reach the backends.
AI crawlers and llms.txt
Section titled “AI crawlers and llms.txt”The blog (blog.guigpap.com) deliberately serves two artefacts for AI crawlers (ChatGPT, Gemini, Perplexity, etc.):
-
/llms.txtand/llms-full.txt— machine-readable index of the blog articles, advertised via theLinkheader:header Link "</llms.txt>; rel=\"llms-txt\", </llms-full.txt>; rel=\"llms-full-txt\""header X-Llms-Txt "/llms.txt" -
Markdown mirrors — every article has a
.mdtwin that returns the source version. Caddy forcesContent-Type: text/plaininstead oftext/markdownbecause mainstream crawler parsers accept plain text but often ignoretext/markdown:@markdown_mirror path_regexp md_ext \.md$header @markdown_mirror {Content-Type "text/plain; charset=utf-8"defer}
The blog’s CSP allows 'wasm-unsafe-eval' (for Pagefind, the static search engine) and data: URIs in font-src (for the embedded fontsource subsets):
header Content-Security-Policy "default-src 'self'; \ script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; \ style-src 'self' 'unsafe-inline'; \ img-src 'self' data:; \ font-src 'self' data:"CrowdSec: Debian image + SSH detection
Section titled “CrowdSec: Debian image + SSH detection”The CrowdSec engine uses the latest-debian image (not Alpine) because the Debian 13 VPS exposes its SSH logs only via journald — there is no /var/log/auth.log. The Debian image ships journalctl; the Alpine image does not. The container mounts /var/log/journal, /run/log/journal and /etc/machine-id from the host.
| Layer | Role | Enforcement |
|---|---|---|
| CrowdSec + Caddy bouncer | HTTP/HTTPS | Block via Caddy (403) |
| CrowdSec SSH detection | Brute-force SSH | Detection only (no host bouncer) |
| fail2ban | SSH enforcement | Ban via nftables (inet f2b-table) |
| UFW | Baseline firewall | Ports 22, 80, 443 only |
Operational commands
Section titled “Operational commands”# CrowdSec managementdocker exec crowdsec cscli decisions list # Banned IPsdocker exec crowdsec cscli decisions delete --ip 1.2.3.4 # Unbandocker exec crowdsec cscli alerts list # Recent alertsdocker exec crowdsec cscli metrics # Statistics
# Caddy managementdocker exec caddy caddy validate --config /etc/caddy/Caddyfiledocker exec caddy caddy reload --config /etc/caddy/Caddyfiledocker logs caddy --tail 100
# Trivy: scan an imagedocker exec trivy trivy image --format json \ --severity CRITICAL,HIGH --ignore-unfixed postgres:174. What if? — Outlook and limits
Section titled “4. What if? — Outlook and limits”Current operational feedback
Section titled “Current operational feedback”Current limits
Section titled “Current limits”| Limit | Impact | Mitigation |
|---|---|---|
| No Caddy HA | Single point of failure | Monitoring + auto restart |
| CrowdSec untested under load | Behaviour during DDoS unknown | Upstream rate limiting possible |
| Possible false positives | Legitimate IPs may be blocked | Plan for manual whitelisting |
Evolution scenarios
Section titled “Evolution scenarios”If false positives appear:
- Create whitelists per IP or User-Agent
- Tune scenario thresholds (
cscli scenarios inspect) - Disable overly aggressive scenarios
If load grows:
- Enable Caddy streaming mode for large files
- Add a cache (Varnish) in front of some services
- Consider a CDN (Cloudflare) for static content
If a DDoS attack happens:
- CrowdSec detects but is not enough on its own
- Activate Cloudflare’s Under Attack mode
- Contact the host for upstream mitigation
Metrics to watch
Section titled “Metrics to watch”| Metric | Source | Attention threshold |
|---|---|---|
| Banned IPs/day | cscli decisions list | > 100 = review scenarios |
| HTTP alerts | cscli alerts list | Sudden spike = potential attack |
| 403 responses | Caddy logs | Abnormal rate = possible false positives |
Related pages
Section titled “Related pages”Infrastructure
Section titled “Infrastructure”- VPS Architecture — Overview
- Monitoring Stack — Caddy metrics
Reference
Section titled “Reference”- Glossary — WAF, bouncer, TLS, reverse proxy