Aller au contenu

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 :

networks:
  proxy_net:
    external: true

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