Le problème

Dès qu’on expose des services sur Internet — ne serait-ce qu’un reverse proxy avec un certificat Let’s Encrypt — les scans et tentatives d’intrusion commencent. En quelques heures, les logs se remplissent de requêtes suspectes :

GET /wp-admin HTTP/1.1
GET /phpmyadmin HTTP/1.1
POST /xmlrpc.php HTTP/1.1
GET /.env HTTP/1.1
GET /actuator/health HTTP/1.1
POST /cgi-bin/luci HTTP/1.1
GET /telescope/requests HTTP/1.1

C’est le bruit de fond d’Internet. Les bots scannent en permanence toutes les plages IP à la recherche de services vulnérables — WordPress non patchés, interfaces d’admin exposées, fichiers de configuration accessibles.

La question n’est pas si on sera scanné, mais quand — et la réponse est “dans les minutes qui suivent l’exposition”.

Pourquoi CrowdSec ?

CrowdSec est un IPS (Intrusion Prevention System) collaboratif et open-source. Son fonctionnement en quatre étapes :

  1. Détection — l’agent analyse les logs et détecte les comportements malveillants (scénarios)
  2. Décision — les IPs malveillantes sont ajoutées à une liste de blocage locale
  3. Partage — les détections sont anonymisées et partagées avec la communauté mondiale
  4. Enrichissement — on bénéficie des détections de tous les autres utilisateurs CrowdSec

Le modèle collaboratif est ce qui le distingue de fail2ban : quand une IP est identifiée comme malveillante quelque part dans le monde, elle est bloquée préventivement sur votre infrastructure avant même qu’elle ne vous attaque.

CrowdSec vs fail2ban

CritèreCrowdSecfail2ban
ModèleCollaboratif (cloud community)Local uniquement
Langage des scénariosYAML + expressions GoRegex Python
BouncerPlugin natif Traefik, Nginx, etc.iptables principalement
DashboardConsole web + API + PrometheusLogs uniquement
ScalabilitéLAPI centralisé, agents distribuésUn daemon par machine
Blocklist communautaireOui (3M+ IPs partagées)Non

Architecture

Mon déploiement CrowdSec est distribué, avec un LAPI central et des agents sur plusieurs machines :

Internet
Traefik ──→ [Plugin Bouncer CrowdSec] ──→ Services
   └──→ Access logs (JSON)
      Agent CrowdSec (docker-infra) ──→ LAPI central
            │                              ↑
            ├─ Logs Traefik               │
            ├─ Logs Authentik (via Loki)  │
            ├─ Logs Grafana (via Loki)    │
            ├─ Syslog / auth.log          │
            └─ Logs MeshCentral (via Loki)│
      Agent CrowdSec (HAOS) ─────────────┘

Les composants

  • LAPI (Local API) : le cerveau central. Stocke les décisions dans PostgreSQL, gère les bouncers, distribue les blocklists. Tourne sur la VM Docker principale.
  • Agent docker-infra : lit les logs de Traefik, Authentik, Grafana, MeshCentral et le syslog. Détecte les scénarios d’attaque et envoie les alertes au LAPI.
  • Agent HAOS : agent CrowdSec sur Home Assistant OS, connecté au LAPI central. Surveille les tentatives de brute-force sur l’interface HA.
  • Bouncer Traefik : plugin natif intégré dans Traefik, interroge le LAPI et bloque les requêtes des IPs bannies.

Pourquoi PostgreSQL plutôt que SQLite ?

Par défaut, CrowdSec utilise SQLite. J’ai migré vers PostgreSQL (le même LXC qui sert déjà Grafana, Authentik, Immich, etc.) pour :

  • Des performances meilleures sous charge (SQLite ne gère pas bien les écritures concurrentes)
  • Des backups intégrés au plan de sauvegarde PostgreSQL existant
  • La possibilité de requêter les données CrowdSec depuis d’autres outils

Installation

Agent + LAPI (Docker Compose)

services:
  crowdsec:
    image: crowdsecurity/crowdsec:v1.7.4
    hostname: docker-infra-agent
    volumes:
      - ./config:/etc/crowdsec
      - ./data:/var/lib/crowdsec/data
      - /var/log/traefik:/var/log/traefik:ro
      - /var/log:/var/log/host:ro
    environment:
      - COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/linux crowdsecurity/sshd
    networks:
      proxy:
        ipv4_address: 172.18.0.100
      observability:
    security_opt:
      - no-new-privileges
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Les collections installées :

CollectionCe qu’elle détecte
crowdsecurity/traefikScans web, path traversal, bad user-agents, crawl agressif
crowdsecurity/http-cveExploitation de CVE connues (Log4Shell, Spring4Shell, etc.)
crowdsecurity/linuxBrute-force SSH, sudo abuse
crowdsecurity/sshdTentatives de connexion SSH échouées

Sources d’acquisition (acquis.d/)

CrowdSec est capable de lire les logs depuis plusieurs sources simultanément :

1. Fichier direct — les access logs Traefik en JSON :

# acquis.d/traefik.yaml
filenames:
  - /var/log/traefik/access.log
labels:
  type: traefik

2. Via Loki — pour les logs applicatifs des conteneurs Docker :

# acquis.d/authentik-loki.yaml
source: loki
url: http://loki:3100
query: '{container=~"authentik-.*"}'
labels:
  type: authentik
# acquis.d/syslog-loki.yaml
source: loki
url: http://loki:3100
query: '{type="syslog"}'
labels:
  type: syslog

L’acquisition via Loki est élégante : CrowdSec lit les logs déjà centralisés plutôt que d’accéder aux fichiers de chaque conteneur. Ça évite de monter des volumes supplémentaires et ça profite du parsing déjà fait par Promtail.

3. Fallback fichiers directs — syslog et auth.log en cas de panne Loki :

# acquis.d/syslog-file.yaml
filenames:
  - /var/log/host/syslog
  - /var/log/host/auth.log
labels:
  type: syslog

Parsers personnalisés

En plus des parsers inclus dans les collections, j’ai créé des parsers custom pour les applications qui n’ont pas de support officiel :

  • loki-authentik : parse les logs d’Authentik pour détecter les tentatives de login échouées
  • loki-grafana : parse les logs de connexion Grafana
  • loki-meshcentral : parse les logs d’authentification MeshCentral
  • meshcentral-auth + meshcentral-bf : scénario custom de détection de brute-force sur MeshCentral

Le bouncer Traefik : plugin natif

Le bouncer n’est pas un conteneur séparé — c’est un plugin Traefik intégré directement dans le reverse proxy :

# traefik.yml (configuration statique)
experimental:
  plugins:
    crowdsec-bouncer-traefik-plugin:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: v1.4.7

Configuration du middleware :

# dynamic/crowdsec.yml
http:
  middlewares:
    crowdsec:
      plugin:
        crowdsec-bouncer-traefik-plugin:
          crowdsecLapiScheme: http
          crowdsecLapiHost: "172.18.0.100:8080"
          crowdsecLapiKey: "une-cle-bouncer"
          crowdsecMode: live
          updateIntervalSeconds: 60
          defaultDecisionSeconds: 60

Ce middleware est appliqué globalement sur l’entrypoint websecure — toutes les requêtes HTTPS passent par le filtre CrowdSec avant d’atteindre le moindre service :

# traefik.yml
entryPoints:
  websecure:
    address: ":443"
    http:
      middlewares:
        - crowdsec@file

Avantage du mode plugin : zéro latence réseau supplémentaire. Le bouncer est dans le même processus que Traefik. Avec le cache de 60 secondes, la plupart des vérifications se font en mémoire en < 1 ms.

Ban exponentiel

Le système par défaut de CrowdSec banne pour une durée fixe (4h). C’est insuffisant pour les récidivistes. J’ai implémenté un ban exponentiel qui augmente la durée à chaque infraction :

StrikeDurée de ban
1er4 heures
2ème8 heures
3ème16 heures
4ème32 heures
5ème64 heures
6ème128 heures
7ème+168 heures (7 jours, max)

La configuration utilise les expressions Go de CrowdSec dans profiles.yaml :

name: exponential_ban
filters:
  - Alert.Remediation == true
decisions:
  - type: ban
    duration: |
      {{if eq (GetDecisionsCount .) 0}}4h
      {{else if eq (GetDecisionsCount .) 1}}8h
      {{else if eq (GetDecisionsCount .) 2}}16h
      {{else if eq (GetDecisionsCount .) 3}}32h
      {{else if eq (GetDecisionsCount .) 4}}64h
      {{else if eq (GetDecisionsCount .) 5}}128h
      {{else}}168h
      {{end}}

GetDecisionsCount retourne le nombre de décisions précédentes pour cette IP. Un bot qui persiste après un premier ban de 4h se prend 8h, puis 16h, etc. Au bout de quelques itérations, la plupart abandonnent.

Notifications Discord interactives

Les alertes CrowdSec sont envoyées vers un bot Discord custom via un webhook HTTP. Pas un simple message texte — un embed riche avec des informations contextuelles et des boutons interactifs :

Pipeline de notification

CrowdSec alert
  → profiles.yaml (calcul durée ban)
    → notification HTTP (http_default)
      → Discord Bot (:8080/webhook/crowdsec)
        → Embed Discord avec boutons

Contenu de la notification

Chaque notification contient :

  • IP source avec lien vers AbuseIPDB pour vérification
  • AS (Autonomous System) et pays d’origine
  • Scénario déclenché (ex: crowdsecurity/http-crawl-non_statics)
  • Nombre d’événements qui ont déclenché l’alerte
  • Durée du ban (avec le strike count)
  • Machine qui a détecté l’attaque

Boutons interactifs

Le bot Discord offre des boutons cliquables :

  • Info : affiche les détails complets de l’IP (géolocalisation, historique de bans)
  • Whitelist : retire le ban et whitelist l’IP (utile en cas de faux positif, sécurisé par rôle Discord)

Ce système permet de gérer les incidents depuis le téléphone — pas besoin de SSH sur le serveur pour débannir une IP légitime.

Les scénarios en action

Quelques scénarios de la vie réelle observés sur mon homelab :

Crawl agressif

Un bot crawle 200+ pages en quelques secondes. Le scénario http-crawl-non_statics détecte le pattern et ban l’IP.

# Dans Grafana, on voit le pic de requêtes
{job="traefik-access"} | json | line_format "{{.client}} {{.status}} {{.path}}"

Scan de vulnérabilités

Recherche de fichiers sensibles : /.env, /.git/config, /wp-config.php, /.htaccess, /server-status. Le scénario http-sensitive-files bloque dès la 3ème tentative.

Brute-force SSO

Tentatives de login répétées sur Authentik. Le parser custom loki-authentik détecte les échecs consécutifs et ban l’IP source.

Exploitation de CVE

La collection http-cve détecte les tentatives d’exploitation de CVE connues (Log4Shell via ${jndi:ldap://...}, Spring4Shell, etc.) et ban immédiatement.

Monitoring CrowdSec

CrowdSec expose des métriques Prometheus sur le port 6060 :

# prometheus.yml
- job_name: crowdsec
  static_configs:
    - targets: ['crowdsec-internal.:6060']

Métriques surveillées :

MétriqueDescriptionAlerte
cs_alertsNombre d’alertes généréesSpike > 50/h
cs_active_decisionsNombre d’IPs actuellement bannies> 1000
crowdsec_heartbeat_statusÉtat du pipeline de notification== 0 (cassé)

Les alertes Prometheus déclenchent des notifications Discord :

- alert: CrowdSecAlertSpike
  expr: increase(cs_alerts[1h]) > 50
  annotations:
    summary: "Pic d'alertes CrowdSec — possible attaque coordonnée"

- alert: CrowdSecMassiveBan
  expr: cs_active_decisions > 1000
  annotations:
    summary: "Plus de 1000 IPs bannies — situation anormale"

Commandes utiles

# Voir les décisions en cours (IPs bannies)
docker exec crowdsec cscli decisions list

# Voir les alertes récentes
docker exec crowdsec cscli alerts list --limit 20

# Bannir manuellement une IP
docker exec crowdsec cscli decisions add --ip 1.2.3.4 --reason "manual ban" --duration 24h

# Débannir une IP
docker exec crowdsec cscli decisions delete --ip 1.2.3.4

# Voir les bouncers connectés
docker exec crowdsec cscli bouncers list

# Voir les agents connectés (architecture multi-agents)
docker exec crowdsec cscli machines list

# Mettre à jour les scénarios
docker exec crowdsec cscli hub update && docker exec crowdsec cscli hub upgrade

# Voir les métriques de parsing
docker exec crowdsec cscli metrics

# Tester un parser sur un log
echo 'log line here' | docker exec -i crowdsec cscli explain --type traefik

Architecture multi-agents

Le LAPI central tourne sur docker-infra et écoute sur le LAN (192.168.1.52:8080). Les agents distants s’y connectent pour envoyer leurs alertes et recevoir les décisions.

Ajouter un agent distant

# Sur le LAPI central — enregistrer la machine
docker exec crowdsec cscli machines add mon-agent-distant --password 'un-mot-de-passe'

# Sur l'agent distant — se connecter au LAPI central
# Dans /etc/crowdsec/local_api_credentials.yaml :
url: http://192.168.1.52:8080
login: mon-agent-distant
password: un-mot-de-passe

L’agent Home Assistant OS (haos-agent) est configuré de cette façon. Il surveille les tentatives de brute-force sur l’interface web de HA et envoie ses alertes au LAPI central. Le bouncer Traefik (sur docker-infra) applique ensuite les bans, même si l’attaque vient d’un autre service.

Whitelisting

Un point critique : ne pas se bannir soi-même. La whitelist inclut :

  • Le réseau LAN (192.168.1.0/24)
  • Le réseau VPN Headscale (100.64.0.0/10)
  • Les IPs des VPS cloud connectés au mesh
# /etc/crowdsec/parsers/s02-enrich/whitelist.yaml
whitelist:
  reason: "Private ranges and VPN"
  ip:
    - "192.168.1.0/24"
    - "100.64.0.0/10"
    - "172.16.0.0/12"

Résultats après plusieurs mois

  • ~300-500 IPs bloquées par jour en moyenne
  • 95% des scans bloqués avant d’atteindre les services applicatifs
  • Zéro faux positif sur les scénarios par défaut (les customs ont nécessité quelques ajustements)
  • Temps de réponse du bouncer : < 1 ms (plugin natif + cache)
  • Récidivistes : la plupart abandonnent après le 3ème ban (ban exponentiel efficace)
  • Top pays d’attaque : Chine, Russie, USA, Brésil (dans cet ordre)

Le dashboard Grafana avec les métriques CrowdSec est un plaisir coupable — voir en temps réel les tentatives d’intrusion se faire rejeter a quelque chose de satisfaisant.

Conseils

  1. Commencer avec les scénarios par défaut — ils sont bien calibrés, ne pas les modifier au début
  2. Whitelister son réseau — LAN + VPN avant tout, pour éviter de se bannir soi-même
  3. Activer le ban exponentiel — la durée fixe est insuffisante pour les bots persistants
  4. Monitorer les métriques — un pic d’alertes peut indiquer une attaque coordonnée
  5. Tester les parsers custom — utiliser cscli explain avant de déployer en production
  6. Notifier sur Discord — la réactivité est essentielle, surtout pour les faux positifs
  7. Utiliser le mode plugin Traefik — plus performant qu’un bouncer en conteneur séparé

CrowdSec est devenu une couche de sécurité indispensable de mon homelab. Le ratio effort/protection est excellent — une heure d’installation, quelques heures de tuning, et une protection permanente qui s’améliore avec le temps grâce au modèle collaboratif.