diff --git a/content/posts/review-stack-01-suricata-host-yml-2026-07.md b/content/posts/review-stack-01-suricata-host-yml-2026-07.md new file mode 100644 index 0000000..1115ddf --- /dev/null +++ b/content/posts/review-stack-01-suricata-host-yml-2026-07.md @@ -0,0 +1,124 @@ +--- +title: "Suricata en mode host sur Docker Swarm : pourquoi on triche (et pourquoi c'est OK)" +date: 2026-07-23 +draft: false +tags: ["homelab", "docker", "cybersecurity", "build-in-public", "french-tech", "infosec"] +summary: "Analyse du stack Suricata de Bojemoi Lab : comment intégrer un IDS/IPS en network_mode host dans un environnement Docker Swarm, avec exporter Prometheus et nettoyage automatique des logs." +author: "Bojemoi" +ShowToc: true +ShowReadingTime: true +--- + +## TL;DR + +Suricata ne joue pas bien avec Docker Swarm. On le sait. On a quand même trouvé un compromis fonctionnel, et cet article explique pourquoi on a fait ces choix — y compris les moins glorieux. + +--- + +## Le problème fondamental : Swarm et la capture réseau + +Docker Swarm est excellent pour orchestrer des services applicatifs. Mais dès qu'on veut faire de la capture de paquets **à la couche physique**, les choses se compliquent sérieusement. + +Un service Swarm tourne dans un réseau overlay (`ingress` ou custom). Les paquets qu'il voit sont déjà encapsulés, NATés, transformés. Pour un IDS comme Suricata, c'est rédhibitoire : tu n'analyses plus le trafic réel, tu analyses l'ombre d'un trafic. + +La solution honnête ? **Sortir Suricata du Swarm et lui donner accès direct à l'interface réseau physique.** + +C'est ce que fait `stack/01-suricata-host.yml` — et c'est assumé dès le premier commentaire du fichier : + +```yaml +# Suricata IDS/IPS — standalone docker compose (NOT Swarm) +# Requires network_mode: host for real packet capture on eth0. +``` + +--- + +## Architecture du stack : trois rôles, trois conteneurs + +### 1. `suricata` — le moteur IDS/IPS + +```yaml +network_mode: host +cap_add: + - NET_ADMIN + - SYS_NICE + - NET_RAW +``` + +`network_mode: host` est le choix central. Le conteneur partage la stack réseau du nœud hôte. Suricata peut ainsi écouter sur `eth0` directement, comme s'il était installé en bare-metal. + +Les capabilities ajoutées sont les trois pilliers de la capture réseau sous Linux : +- **NET_RAW** : ouvrir des raw sockets, lire les paquets bruts +- **NET_ADMIN** : manipuler les interfaces (mode promiscuité, règles nftables si IPS) +- **SYS_NICE** : ajuster la priorité des threads de capture pour éviter les drops sous charge + +Les options Suricata méritent attention : + +```yaml +SURICATA_OPTIONS=-i eth0 --set stream.reassembly.depth=0 --set detect.profile=low +``` + +- `stream.reassembly.depth=0` : pas de limite sur la profondeur de réassemblage TCP. En homelab avec peu de RAM, c'est un pari — ça peut consommer beaucoup sur des transferts massifs. À monitorer. +- `detect.profile=low` : profil de détection économique. On sacrifie de la couverture pour des performances acceptables sur du matériel modeste. C'est honnête pour un lab. + +### 2. `suricata-exporter` — le pont vers Prometheus + +Ce conteneur lit les métriques Suricata via son **Unix socket** (`suricata-command.socket`) et les expose au format Prometheus. C'est un pattern propre : Suricata ne connaît pas Prometheus, l'exporter fait le pont sans modifier le moteur. + +```yaml +command: + - '--suricata.socket-path=/var/run/suricata/suricata-command.socket' +``` + +La communication passe par un volume partagé monté en lecture seule côté exporter. Le `depends_on` garantit que Suricata démarre en premier — même si ça ne garantit pas que le socket existe déjà au moment où l'exporter tente de s'y connecter. Un `restart: unless-stopped` compense ce race condition de démarrage. + +L'exporter rejoint le réseau `monitoring` (externe, donc géré par le Swarm), ce qui lui permet d'être scraped par Prometheus même si le reste du stack est hors Swarm. + +### 3. `eve-cleaner` — gestion des logs à l'ancienne + +C'est le composant le plus artisanal — et probablement le plus honnête du stack. + +Suricata génère un fichier `eve.json` en append continu. Il n'y a **pas de rotation native** du fichier actif dans Suricata (contrairement aux fichiers horodatés qu'il crée lui-même). Résultat : sans intervention, `eve.json` grossit indéfiniment. + +La solution ici : un conteneur Alpine qui tourne une boucle shell toutes les heures. + +```sh +# Suppression des fichiers archivés de plus de 48h +AGE_H=$(( (NOW - MTIME) / 3600 )) +if [ "$AGE_H" -ge "$KEEP_HOURS" ]; then rm -f "$f"; fi + +# Troncature de eve.json si > 5GB +if [ "$EVE_KB" -ge "$MAX_KB" ]; then truncate -s 0 "$EVE"; fi +``` + +Points notables : +- `truncate -s 0` plutôt que `> file` ou `rm` : Suricata conserve son file descriptor ouvert, la troncature vide le fichier sans casser le handle. C'est le bon geste. +- La compatibilité `stat` est gérée avec deux syntaxes (`-c %Y` Linux, `-f %m` macOS) — vestige probable de dev en local sur Mac. +- Le logging de l'opération est verbose et structuré, ce qui est bien pour le debug. + +--- + +## Ce qui manque (et on le sait) + +### Déploiement multi-nœuds manuel + +Le commentaire dit tout : `Deploy on each node: docker compose -f stack/01-suricata-host.yml up -d`. Il n'y a pas d'automatisation de déploiement sur plusieurs nœuds. Sur un cluster de 5 machines, c'est 5 commandes SSH manuelles. Un Ansible playbook serait le prochain palier évident. + +### Le race condition au démarrage + +`suricata-exporter` démarre après `suricata` mais le socket Unix peut mettre quelques secondes à apparaître. Les premières tentatives de connexion échouent. Le `restart: unless-stopped` rattrape ça, mais c'est du bricolage. Une `healthcheck` sur Suricata testant l'existence du socket serait plus propre. + +### `detect.profile=low` en production + +Acceptable pour un lab, problématique pour une vraie infrastructure. Ce paramètre réduit la précision de certaines détections comportementales. À revoir si le lab évolue vers de la détection d'incidents réels. + +### L'enrichissement des alertes est ailleurs + +Le commentaire final pointe vers `stack/01-service-hl.yml` pour le `suricata-attack-enricher`. Ce découpage est logique (l'enrichisseur a besoin du réseau overlay Swarm et des secrets), mais ça crée une dépendance inter-fichiers non évidente à l'onboarding. + +--- + +## Ce qu'on retient + +Ce fichier est un bon exemple de pragmatisme en homelab : on fait ce qui marche, on documente les compromis, et on ne prétend pas que c'est parfait. `network_mode: host` dans un environnement Swarm n'est pas élégant, mais c'est la seule façon d'avoir un IDS qui voit vraiment le trafic. + +L'