Edge stack — Caddy + CrowdSec¶
Couche de bordure du Kimsufi : reverse proxy + filtrage IP. Tous les services publics (*.carai.be) passent par ici.
Architecture en bref¶
graph LR
Internet -.HTTPS.-> Caddy
Caddy -- "bouncer pull" --> CrowdSec
CrowdSec -- "parse access.log" --> Caddy
Caddy -- "reverse_proxy" --> Apps
CrowdSec -- "parse journalctl" --> Host
CrowdSec <-- "alerts + blocklists" --> Console[app.crowdsec.net] | Composant | Repo | Image | Port |
|---|---|---|---|
| Caddy | phantoms/caddy | gitea.carai.be/phantoms/caddy-crowdsec:latest | 80, 443, 443/udp |
| CrowdSec | phantoms/crowdsec | crowdsecurity/crowdsec:latest | 8080 (interne core_internal_net) |
Console enrôlée engine 9b59c20de105 (app.crowdsec.net) — reçoit les blocklists CTI + remonte les alertes locales.
Réseaux Docker¶
proxy_net(external) — toutes les apps publiques ; Caddy y reverse-proxy.core_internal_net(external) — lien Caddy ↔ CrowdSec (API 8080), DB Postgres, bouncer feed. Jamais d'app publique ici sans Caddy.
Volumes (preserved, external)¶
| Volume | Contenu | Criticité |
|---|---|---|
caddy_caddy_data | Certs Let's Encrypt | 🔴 perte = re-validation LE (rate-limit) |
caddy_caddy_config | Cache parsing Caddy | 🟢 régénérable |
crowdsec_crowdsec_data | crowdsec.db (decisions, alerts, bouncers), MMDB GeoIP | 🟠 perte = ré-enregistrement bouncers + perte historique alerts |
crowdsec_crowdsec_config | online_api_credentials.yaml (enrôlement Console), parsers/whitelists/profiles, collections | 🔴 perte = ré-enrôlement Console manuel + perte whitelists |
Backup régulier via Backrest (cf. BACKUP.md). Snapshots tar à la demande : docker run --rm -v <volume>:/d alpine tar czf - -C /d . > backup.tar.gz. Pour la DB sqlite crowdsec en cours d'écriture, faire un snapshot atomique d'abord : docker exec crowdsec sqlite3 /var/lib/crowdsec/data/crowdsec.db '.backup /tmp/snap.db'.
Pattern de labels (côté app)¶
Tout service public ajoute ces labels à son conteneur :
labels:
caddy: monservice.carai.be
caddy.reverse_proxy: monservice:PORT # PAS {{upstreams}} → stale au recreate
caddy.import: secure_headers # snippet du Caddyfile
caddy.crowdsec: "" # active le bouncer
caddy.log: "" # access.log JSON
Et doit être sur proxy_net :
Snippets dispo dans caddy/Caddyfile : secure_headers (HSTS/XFO/etc.), secure_headers_iframe_ok (sans X-Frame-Options).
Bouncer key¶
Une seule clé partagée entre les 2 stacks (BOUNCER_KEY_CADDY) : - Côté CrowdSec : env BOUNCER_KEY_caddy=... → crée le bouncer "caddy" au boot - Côté Caddy : env BOUNCER_KEY_CADDY=... lue par {env.BOUNCER_KEY_CADDY} dans le Caddyfile
⚠️ Si désynchro : le bouncer Caddy n'arrive plus à pull les décisions ; toutes les IPs passent (mode dégradé, pas mode panique). Voir Debug.
Stockée dans .env local des stacks (gitignored) et dans les env vars Portainer si jamais on adopte (cf. README caddy).
Rotation :
# 1. Génère une nouvelle clé
docker exec crowdsec cscli bouncers add caddy-tmp -o raw
# 2. Update les 2 .env (ou env Portainer) avec la nouvelle clé
# 3. docker compose up -d --force-recreate sur les 2 stacks
# 4. Supprime l'ancienne entrée
docker exec crowdsec cscli bouncers delete caddy
docker exec crowdsec cscli bouncers rename caddy-tmp caddy # optionnel
Caddyfile global¶
caddy/Caddyfile — versionné, contient le block global (email, order crowdsec first, bloc crowdsec {...}, log default) + les snippets réutilisables. Modifier localement, commit, push, puis sur le serveur :
cd /srv/docker/caddy.v2
git pull # si géré en git ; sinon scp depuis le Mac
docker exec caddy caddy reload --config /config/Caddyfile
Le caddy reload est sans coupure (graceful).
CrowdSec collections¶
COLLECTIONS: "crowdsecurity/caddy crowdsecurity/linux crowdsecurity/sshd crowdsecurity/http-cve crowdsecurity/base-http-scenarios"
Pour ajouter : éditer la var, redeploy. Les collections déjà installées restent dans le volume jusqu'à cscli collections remove.
Ce qui est couvert : - Bruteforce SSH (sshd) + génériques Linux - Bruteforce HTTP, scans (base-http-scenarios) - CVE web connues (http-cve : Log4Shell, etc.) - Logs Caddy spécifiques (caddy → parsing JSON access.log)
crowdsecurity/whitelist-good-actors est tirée en dépendance auto.
Whitelists¶
| Fichier | Géré par | Usage |
|---|---|---|
parsers/s02-enrich/trusted-ips.yaml | ~/cs-whitelist.sh add <ip> | IPs explicites toujours autorisées |
parsers/s02-enrich/whitelists-perso.yaml | édition manuelle | CIDRs persos |
parsers/s02-enrich/whitelists.yaml | shipped CrowdSec | private ranges (RFC1918) |
Reload sans restart : docker kill --signal=HUP crowdsec.
Helper script : scripts/cs-whitelist.sh.
Maintenance — runbook¶
| Action | Commande |
|---|---|
| Voir config Caddy en cours | docker exec caddy wget -qO- http://localhost:2019/config/ \| jq |
| Valider Caddyfile | docker exec caddy caddy validate --config /config/Caddyfile |
| Reload Caddy sans restart | docker exec caddy caddy reload --config /config/Caddyfile |
| Logs Caddy (live) | tail -f /srv/docker/caddy/logs/access.log \| jq |
| Décisions actives | docker exec crowdsec cscli decisions list |
| Ban IP manuel | docker exec crowdsec cscli decisions add --ip <IP> --duration 24h |
| Unban | docker exec crowdsec cscli decisions delete --ip <IP> |
| Alerts récentes | docker exec crowdsec cscli alerts list -s 1h |
| Statut Console | docker exec crowdsec cscli console status |
| Bouncers connectés | docker exec crowdsec cscli bouncers list |
| Reload CrowdSec | docker kill --signal=HUP crowdsec |
| Hub update | docker exec crowdsec cscli hub update && cscli hub upgrade |
Debug — symptômes courants¶
| Symptôme | Cause probable | Fix |
|---|---|---|
| Tous les sites en HTTP 000 (TLS handshake fail) | Caddy n'a chargé aucune config | docker logs caddy \| grep -i error ; souvent un Caddyfile invalide → bloc removed → API key empty → tout pète. Validate + reload. |
| 502 Bad Gateway sur 1 site | App down OU pas sur proxy_net | docker exec caddy ping monapp ; vérifier labels caddy.reverse_proxy ; container UP |
| 403 Forbidden sur tous les sites | Bouncer en panic mode | Vérifier BOUNCER_KEY_CADDY matche cscli bouncers list ; crowdsec UP |
| Pas de cert LE | Rate-limit ou DNS KO | Logs caddy ; dig +short monservice.carai.be ; rate-limit = 50 certs/sem |
| Bouncer pull qui timeout | crowdsec:8080 injoignable | Réseau core_internal_net ; les 2 containers dessus |
| Nouveau site pas pris en compte | caddy-docker-proxy a raté un event | docker restart caddy ; voir CADDY_INGRESS_NETWORKS matche le réseau de l'app |
| Bouncer "stale" (caddy@172.x.x.x) | Auto-créés lors de recreates passés | Cosmétique, pas suppressibles via cscli (auto-created) ; ignorer |
disable_streaming_bouncer / autre option qui fait planter Caddy | Option inexistante de la directive crowdsec | Ne PAS inventer d'options ; consulter doc bouncer |
Migration / refonte¶
L'edge stack a été refondue le 2026-05-18 (de CLI dans /srv/docker/caddy + /srv/docker/core/crowdsec vers repos Gitea + image registry). Les anciens dossiers sont conservés tel quel pour permettre le rollback via scripts/edge-rollback.sh.
Backup pré-migration : ~/backups/edge-stack-20260518-0003/ sur le serveur.
Les stacks tournent actuellement en CLI (docker compose up -d dans /srv/docker/caddy.v2 et /srv/docker/crowdsec.v2), pas via Portainer. Possible d'adopter plus tard : stop CLI, deploy via Portainer-from-Git. Risque connu : Portainer-from-Git peut mal résoudre le bind ./Caddyfile (crée un dossier vide) → fallback prêt = bind absolu /srv/docker/caddy.v2/Caddyfile.
Upgrade¶
| Composant | Comment |
|---|---|
| Caddy + plugins | scripts/caddy-rebuild-push.sh → xcaddy rebuild + push registry → docker compose pull && up -d |
| CrowdSec | Watchtower auto (label com.centurylinklabs.watchtower.enable=true) |
| Bouncer | Suit l'upgrade Caddy (plugin bundlé dans l'image) |
| Collections | docker exec crowdsec cscli hub upgrade |