Aller au contenu

Security Stack : Caddy + CrowdSec

La Security Stack est la couche de protection qui se place entre Internet et tous les services de l’infrastructure. Elle combine deux composants :

ComposantRôlePort
CaddyReverse proxy avec TLS automatique et HTTP/380, 443, 443/udp
CrowdSecWAF comportemental et threat intelligence collaborative8080 (LAPI), 7422 (AppSec)
TrivyScanner CVE pour images Docker, appelé via docker execaucun port exposé

ports 80, 443, 443/udp

Services internes · webproxy

N8N

Grafana

Odoo

ntfy

Blog

CrowdSec Engine

Parse Caddy access logs

Detect attacks · scenarios

Apply decisions · ban, captcha

Share threat intel · CAPI

Caddy · security-stack

TLS termination · Let's Encrypt, auto-renew

HTTP/3 QUIC support

Security headers · HSTS, CSP, X-Frame-Options

CrowdSec bouncer module

Internet


CritèreCaddyNginxTraefik
TLS automatiqueNatif (Let’s Encrypt)Certbot externeNatif
HTTP/3 QUICNatifModule expérimentalNon
ConfigurationCaddyfile simplenginx.conf verbeuxYAML/Labels
RechargementSans interruptionReload gracefulSans interruption
CrowdSecModule bouncer officielModule luaPlugin tiers

Critères de décision :

  1. Simplicité — La syntaxe Caddyfile est lisible et concise
  2. TLS zéro-config — Certificats Let’s Encrypt automatiques, pas de cron Certbot
  3. Intégration CrowdSec native — Module bouncer officiel, pas de workaround
FonctionBénéfice
Threat intelligence collaborativeLes IPs malveillantes détectées par d’autres instances CrowdSec sont partagées
Scénarios de détectionRègles prêtes pour HTTP, SSH, CVE connus
Bouncer temps réelLes IPs bannies sont bloquées avant d’atteindre le backend
Whitelists automatiquesBots légitimes (Googlebot, etc.) ne sont pas bloqués

{
# Options globales
email admin@guigpap.com
# Module CrowdSec
crowdsec {
api_url http://crowdsec:8080
api_key {env.CROWDSEC_API_KEY}
ticker_interval 15s
}
}
# Redirection www → apex
www.guigpap.com {
redir https://guigpap.com{uri} permanent
}
# Site principal
guigpap.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 avec webhooks protégés
n8n.guigpap.com {
crowdsec
# Bloquer les webhooks internes depuis l'extérieur
@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
}
# Autres services
grafana.guigpap.com {
crowdsec
reverse_proxy grafana:3000
}
odoo.guigpap.com {
crowdsec
reverse_proxy odoo:8069
}
Fenêtre de terminal
# Lister les collections actives
docker exec crowdsec cscli collections list
CollectionRôle
crowdsecurity/caddyParser des logs Caddy
crowdsecurity/base-http-scenariosDétection attaques HTTP courantes
crowdsecurity/http-cveExploits CVE connus
crowdsecurity/appsec-virtual-patchingPatching virtuel
crowdsecurity/linuxScénarios système Linux
crowdsecurity/sshdDétection brute force SSH
crowdsecurity/whitelist-good-actorsListe blanche bots légitimes
  1. Caddy reçoit une requête
  2. Le module bouncer interroge l’API CrowdSec (cache local)
  3. Si l’IP est dans la liste de décisions → 403 Forbidden
  4. Sinon → requête transmise au backend
WebhookAppelé parBloqué depuis Internet
/webhook/notify/*DIUNOui
/webhook/prometheus/*AlertmanagerOui
/webhook/claude/*CLI OllamaOui
/webhook/claude-simple-chat, /webhook/claude-approve, /webhook/claude-rejectN8NOui
/webhook/odoo/*Odoo internalOui
/webhook/docker-controllerTelegram OrchestratorOui
/webhook/notification-hubN8N (Execute Workflow)Oui

Les services internes appellent ces webhooks via http://n8n:5678 (réseau Docker), pas via le domaine public.

Les routes webhooks de n8n.guigpap.com sont rate-limitées à 30 requêtes/minute par IP via le module caddy-ratelimit. Cela complète l’auth (Bearer / Header / HMAC) en empêchant le brute-force sur les endpoints exposés. CrowdSec AppSec écoute en parallèle sur le port 7422 et rejoue les règles crowdsecurity/appsec-virtual-patching + appsec-generic-rules avant que les requêtes atteignent les backends.

Le blog (blog.guigpap.com) sert volontairement deux artefacts pour les crawlers IA (ChatGPT, Gemini, Perplexity, etc.) :

  1. /llms.txt et /llms-full.txt — index machine-readable des articles du blog, déclarés via header Link :

    header Link "</llms.txt>; rel=\"llms-txt\", </llms-full.txt>; rel=\"llms-full-txt\""
    header X-Llms-Txt "/llms.txt"
  2. Mirrors Markdown — chaque article a un jumeau .md qui rend la version source. Caddy force Content-Type: text/plain au lieu de text/markdown parce que les parseurs des crawlers grand public laissent passer plain text mais ignorent souvent text/markdown :

    @markdown_mirror path_regexp md_ext \.md$
    header @markdown_mirror {
    Content-Type "text/plain; charset=utf-8"
    defer
    }

La CSP du blog autorise 'wasm-unsafe-eval' (pour Pagefind, le moteur de recherche statique) et les data: URIs côté font-src (pour les sous-ensembles fontsource embarqués) :

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:"

L’engine CrowdSec utilise l’image latest-debian (et non Alpine) car le VPS Debian 13 expose ses logs SSH uniquement via journald — il n’y a pas de /var/log/auth.log. L’image Debian inclut journalctl ; l’image Alpine ne l’a pas. Le container monte /var/log/journal, /run/log/journal et /etc/machine-id du host.

LayerRôleApplication
CrowdSec + Caddy bouncerHTTP/HTTPSBlock via Caddy (403)
CrowdSec SSH detectionBrute-force SSHDétection seule (pas de bouncer host)
fail2banEnforcement SSHBan via nftables (inet f2b-table)
UFWFirewall baselinePorts 22, 80, 443 uniquement
Fenêtre de terminal
# Gestion CrowdSec
docker exec crowdsec cscli decisions list # IPs bannies
docker exec crowdsec cscli decisions delete --ip 1.2.3.4 # Débannir
docker exec crowdsec cscli alerts list # Alertes récentes
docker exec crowdsec cscli metrics # Statistiques
# Gestion Caddy
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
docker logs caddy --tail 100
# Trivy : scan d'une image
docker exec trivy trivy image --format json \
--severity CRITICAL,HIGH --ignore-unfixed postgres:17

LimiteImpactMitigation
Pas de HA CaddySingle point of failureMonitoring + restart auto
CrowdSec non testé en chargeComportement inconnu sous attaque DDoSRate limiting amont possible
Faux positifs possiblesIPs légitimes potentiellement bloquéesWhitelist manuelle à prévoir

Si des faux positifs apparaissent :

  • Créer des whitelists par IP ou par User-Agent
  • Ajuster les seuils des scénarios (cscli scenarios inspect)
  • Désactiver des scénarios trop agressifs

Si la charge augmente :

  • Activer le mode streaming Caddy pour les gros fichiers
  • Ajouter un cache (Varnish) devant certains services
  • Considérer un CDN (Cloudflare) pour le contenu statique

Si une attaque DDoS survient :

  • CrowdSec détecte mais ne suffit pas seul
  • Activer le mode Under Attack de Cloudflare
  • Contacter l’hébergeur pour mitigation amont
MétriqueSourceSeuil d’attention
IPs bannies/jourcscli decisions list> 100 = vérifier les scénarios
Alertes HTTPcscli alerts listSpike soudain = attaque potentielle
403 responsesLogs CaddyTaux anormal = faux positifs possibles

  • Glossaire — WAF, Bouncer, TLS, Reverse Proxy