Skip to content

Sécurisation de la Stack Traefik & CrowdSec

Guide de Sécurisation de la Stack Traefik & CrowdSec

Ce document décrit l'architecture de sécurisation d'un reverse-proxy Traefik sous Docker : 1. Détection d'intrusions avec CrowdSec : Analyse des logs JSON et bannissement dynamique via le bouncer Traefik. 2. Durcissement HTTP avec des Security Headers : En-têtes recommandés (HSTS, SAMEORIGIN, nosniff). 3. Certificats TLS via Cloudflare DNS-01 : Certificats Let's Encrypt à courbe elliptique (EC256) sans port HTTP public ouvert. 4. Préservation de l'IPv6 native : Configuration Docker avec ip6tables pour éviter le masquage d'adresses source par le proxy userland.


1. Architecture Globale du Filtrage

Le trafic entrant traverse une série de filtres successifs avant d'atteindre les conteneurs applicatifs (Nextcloud, Immich, Open WebUI, etc.) :

graph TD
    Client["Client Web - Navigateur ou Attaquant"] -->|"HTTP 80 ou HTTPS 443"| Traefik["Traefik v3 (Reverse Proxy)"]

    subgraph Traefik_Pipeline["Pipeline de Filtrage Traefik"]
        Redir{"Port 80 ?"}
        Redir -->|"Oui"| Bouncer80["Middleware CrowdSec (Port 80)"]
        Bouncer80 -->|"IP Saine"| HTTPS_Redirect["Redirection 301 HTTPS"]
        Redir -->|"Non (Port 443)"| Bouncer443["Middleware CrowdSec (Port 443)"]
        HTTPS_Redirect --> Bouncer443
        Bouncer443 --> Headers["Middleware Security-Headers"]
    end

    Traefik --> Traefik_Pipeline

    subgraph CrowdSec_Engine["Moteur de Détection CrowdSec"]
        JSONLog["access.log (JSON)"]
        Parser["Parser Traefik et Scénarios Hub"]
        Profiles["profiles.yaml (Ban direct 4h)"]
        LAPI["Local API (LAPI)"]
        JSONLog --> Parser --> Profiles --> LAPI
    end

    Traefik -.->|"Écriture logs"| JSONLog
    Bouncer443 -.->|"Vérification synchrone"| LAPI
    Bouncer80 -.->|"Vérification synchrone"| LAPI

    Headers -->|"IP Saine"| Apps["Conteneurs Applicatifs Internes"]
    Bouncer443 -->|"IP Bannie"| Deny["HTTP 403 Forbidden (Rejet immédiat)"]
    Bouncer80 -->|"IP Bannie"| Deny
Composant Rôle Technique Fichier de Configuration
Traefik Point d'entrée HTTPS, terminaison TLS, routage Docker variable-traefik.yaml
Bouncer Traefik Plugin Go (maxlerebourg/crowdsec-bouncer-traefik-plugin) interrogeant la LAPI variable-crowdsec-middleware.yaml
CrowdSec Détection d'attaques par analyse heuristique des requêtes variable-crowdsec-profiles.yaml
Security Headers Conformité HTTP stricte et protection contre les attaques côté navigateur variable-security-headers.yaml

2. Durcissement Passif : Security Headers

Le middleware dynamique security-headers est injecté au niveau du point d'entrée sécurisé websecure (port 443). Il s'applique de manière transparente à tous les conteneurs exposés.

Fichier dynamique : variable-security-headers.yaml

http:
  middlewares:
    security-headers:
      headers:
        # Empêche l'affichage dans des frames externes (protection contre le Clickjacking)
        # SAMEORIGIN permet toutefois l'intégration interne sécurisée
        customFrameOptionsValue: "SAMEORIGIN"

        # Bloque les tentatives d'interprétation MIME arbitraires par les navigateurs
        contentTypeNosniff: true

        # Active le filtre XSS hérité pour les anciens clients
        browserXssFilter: true

        # Limite la fuite d'URL référente vers des domaines tiers
        referrerPolicy: "strict-origin-when-cross-origin"

        # Force le protocole HTTPS (HSTS) pendant 1 semaine (604800s) avec sous-domaines
        stsSeconds: 604800
        stsIncludeSubdomains: true
        stsPreload: false
        forceSTSHeader: true

3. Configuration Active CrowdSec : Bannissement Direct

A. Politique de Remédiation : variable-crowdsec-profiles.yaml

Les profils de remédiation appliquent un bannissement direct de 4 heures pour les adresses IP et les plages réseau :

# Profil par défaut : bannissement strict 4h pour toutes les adresses IP hostiles
name: default_ip_remediation
filters:
 - Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
 - type: ban
   duration: 4h
on_success: break
---
# Profil plage : bannissement strict 4h pour les attaques coordonnées par sous-réseau
name: default_range_remediation
filters:
 - Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
 - type: ban
   duration: 4h
on_success: break

B. Configuration du Middleware Bouncer : variable-crowdsec-middleware.yaml

Ce middleware communique directement avec la LAPI (Local API) de CrowdSec via le réseau Docker frontend en mode live :

## Dynamic Traefik middleware for CrowdSec bouncer plugin
http:
  middlewares:
    crowdsec-bouncer:
      plugin:
        bouncer:
          enabled: true
          logLevel: INFO
          crowdsecMode: live
          crowdsecLapiScheme: http
          crowdsecLapiHost: crowdsec:8080
          crowdsecLapiKeyFile: /etc/traefik/conf/crowdsec-api-key.txt

Création de la clé d'API pour le bouncer

La clé d'API est générée au sein du conteneur CrowdSec et déposée dans le volume partagé de configuration de Traefik sans aucun retour à la ligne :

docker exec crowdsec cscli bouncers add traefik-bouncer -o raw | tr -d '\r\n' > /opt/traefik/conf/crowdsec-api-key.txt
chmod 600 /opt/traefik/conf/crowdsec-api-key.txt

C. Acquisition des Logs : acquis.yaml

Pour permettre à CrowdSec de lire les requêtes enregistrées par Traefik, le fichier /opt/crowdsec/crowdsec-config/acquis.yaml pointe vers le point de montage partagé :

filenames:
  - /var/log/traefik/*
labels:
  type: traefik

D. Liste blanche d'accès API : /opt/crowdsec/crowdsec-config/config.yaml

Pour que le bouncer Traefik puisse requêter la LAPI sans risquer de se faire rejeter, les sous-réseaux internes Docker doivent être déclarés de confiance :

api:
  server:
    trusted_ips:
      - 127.0.0.1
      - 172.16.0.0/12  # Sous-réseaux Docker internes
      - 10.0.0.0/8
      - ::1
      - fd00::/8

4. Configuration Statique de Traefik (variable-traefik.yaml)

Le fichier statique de Traefik rassemble l'ensemble des garde-fous de sécurité réseau, de journalisation et de génération de certificats :

global:
  checkNewVersion: false
  sendAnonymousUsage: false

experimental:
  plugins:
    bouncer:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: v1.5.0

entryPoints:
  web:
    address: :80
    http:
      # Le bouncer filtre les requêtes dès le port 80 AVANT la redirection HTTPS
      middlewares:
        - crowdsec-bouncer@file
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true

  websecure:
    address: :443
    forwardedHeaders:
      trustedIPs:
        - "127.0.0.1/32"
        - "::1/128"
        - "fd00::/8"
        - "172.16.0.0/12"
        - "192.168.0.0/16"
        - "10.0.0.0/8"
    transport:
      respondingTimeouts:
        readTimeout: 120
        idleTimeout: 360
    http:
      middlewares:
        - crowdsec-bouncer@file
        - security-headers@file

certificatesResolvers:
  cloudflare:
    acme:
      email: "admin@example.com"
      storage: /etc/traefik/certs/cloudflare-acme.json
      caServer: 'https://acme-v02.api.letsencrypt.org/directory'
      # Chiffrement moderne à courbe elliptique
      keyType: EC256
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"

serversTransport:
  insecureSkipVerify: true

providers:
  docker:
    exposedByDefault: false
    endpoint: 'unix:///var/run/docker.sock'
    watch: true
  file:
    directory: /etc/traefik/conf/
    watch: true

accessLog:
  filePath: "/var/log/traefik/access.log"
  # Format JSON obligatoire pour l'extraction fiable par CrowdSec
  format: json
  fields:
    headers:
      defaultMode: keep

5. Déploiement Docker Compose (docker-compose.yaml)

Hardening du Conteneur Traefik

  1. Exécution non-root : Traefik s'exécute sous un utilisateur dédié non privilégié (user: ${PUID}:${PGID}).
  2. Attribution de capabilités ciblées : Seule la capabilité NET_BIND_SERVICE est accordée pour autoriser l'écoute sur les ports privilégiés 80 et 443.
  3. Accès au socket Docker : Le conteneur rejoint le groupe système Docker (group_add: ["110"]) pour lire les événements Docker sans exécuter le binaire en root.
  4. Attente active réseau au boot : Une boucle shell teste la joignabilité de l'infrastructure de plugins Traefik (plugins.traefik.io) avant de lancer le binaire. Cela évite que Traefik ne plante lors des redémarrages serveur si la carte réseau ou la résolution DNS WAN n'est pas encore prête.
services:
  crowdsec:
    image: crowdsecurity/crowdsec:latest
    container_name: crowdsec
    environment:
      GID: "${GID-1000}"
      COLLECTIONS: "crowdsecurity/linux crowdsecurity/traefik"
    volumes:
      - /opt/crowdsec/crowdsec-db:/var/lib/crowdsec/data/
      - /opt/crowdsec/crowdsec-config:/etc/crowdsec/
      - /opt/traefik/log:/var/log/traefik/:ro
      - $INFRASTRUCTURE_FOLDER_LOCATION/variable-crowdsec-profiles.yaml:/etc/crowdsec/profiles.yaml:ro
    networks:
      - frontend
    restart: always

  traefik:
    image: traefik:latest
    container_name: traefik
    restart: always
    networks:
      - frontend
    ports:
      - "80:80"
      - "443:443"
      - "8081:8080"
    user: ${PUID}:${PGID}
    cap_add:
      - NET_BIND_SERVICE
    group_add:
      - "110" # ID de groupe docker pour accéder à /var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/traefik/certs:/etc/traefik/certs/
      - /opt/traefik/conf:/etc/traefik/conf/
      - /opt/traefik/log:/var/log/traefik/
      - /opt/traefik/plugins-storage:/plugins-storage
      - $INFRASTRUCTURE_FOLDER_LOCATION/variable-traefik.yaml:/etc/traefik/traefik.yaml:ro
      - $INFRASTRUCTURE_FOLDER_LOCATION/variable-crowdsec-middleware.yaml:/etc/traefik/conf/middleware-crowdsec.yaml:ro
      - $INFRASTRUCTURE_FOLDER_LOCATION/variable-security-headers.yaml:/etc/traefik/conf/security-headers.yaml:ro
    environment:
      - CF_DNS_API_TOKEN=$CF_DNS_API_TOKEN
    command: >
      sh -c "while ! ping -c 1 plugins.traefik.io > /dev/null 2>&1; do sleep 1; done; exec traefik"

6. Routage IPv6 Natif et Prévention du Bannissement de Passerelle

Le Problème du Userland Proxy Docker

Par défaut, lorsque Docker publie un port (ports: ["443:443"]), il utilise un démon en espace utilisateur (docker-proxy). Ce proxy masque l'adresse IPv6 d'origine du client en la remplaçant par l'adresse de passerelle interne de Docker (ex: fd01::1).

Risque de blocage total de l'IPv6

Si un client externe commet une attaque en IPv6 et que CrowdSec ordonne son bannissement, le bouncer Traefik verrait l'adresse IP source fd01::1. Tous les clients IPv6 légitimes du monde entier se retrouveraient immédiatement bannis !

Solution Sysadmin : ip6tables dans le noyau

Pour préserver la véritable adresse IPv6 source jusqu'à Traefik et CrowdSec, il faut déléguer le routage directement au sous-système netfilter du noyau via ip6tables.

Cette étape peut être automatisée via Ansible ou en configurant directement /etc/docker/daemon.json sur l'hôte :

{
    "ipv6": true,
    "ip6tables": true,
    "fixed-cidr-v6": "fd00::/80",
    "experimental": true
}

Après redémarrage du démon Docker (systemctl restart docker), les paquets IPv6 traversent directement le pont virtuel sans modification de l'en-tête IP source.


7. Exploitation et Supervision au Quotidien

Commandes CLI Usuelles (cscli)

Toutes les commandes d'administration de CrowdSec sont invoquées dans son conteneur Docker :

# Vérifier l'état de synchronisation du bouncer Traefik
docker exec crowdsec cscli bouncers list

# Consulter la liste des décisions actives (bannissements en cours)
docker exec crowdsec cscli decisions list

# Bannir manuellement une adresse IP hostile pour 24 heures
docker exec crowdsec cscli decisions add --ip 198.51.100.42 -d 24h --type ban -r "Attaque manuelle suspecte"

# Révoquer un bannissement (débloquer une IP)
docker exec crowdsec cscli decisions delete --ip 198.51.100.42

# Afficher les statistiques d'analyse de logs et de détection
docker exec crowdsec cscli metrics

Automatisation des Mises à Jour du Hub

Les scénarios de menaces et les collections CrowdSec évoluent quotidiennement. La mise à jour est automatisée chaque matin à 06h00 par notre conteneur cronjob via le script update_security.sh :

docker exec crowdsec cscli hub update && docker exec crowdsec cscli hub upgrade
docker restart crowdsec
Ce script contrôle également la bonne santé des conteneurs de sécurité et transmet un heartbeat push à Uptime Kuma pour garantir le maintien en conditions opérationnelles de la stack.