125 lines
6.3 KiB
Markdown
125 lines
6.3 KiB
Markdown
---
|
|
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'
|