Introduction
Suricata est déployé en mode IPS inline sur le routeur VyOS via le mécanisme Linux NFQUEUE. Contrairement au mode IDS passif (copie miroir du trafic), le mode IPS permet de bloquer activement les paquets malveillants avant qu'ils n'atteignent leur destination.
Cet article détaille l'architecture choisie, la construction de l'image durcie, la gestion des règles et les métriques de production.
Pourquoi NFQUEUE ?
Alternatives évaluées et rejetées
| Mode | Avantage | Inconvénient | Verdict |
|---|---|---|---|
| AF_PACKET bridge | Pas besoin de host network | Perte WAN si container down, complexe | Rejeté |
| TC redirect | Léger | Perd la capacité IPS (mode IDS seulement) | Rejeté |
| macvlan IDS | Simple | IDS only, pas de blocage | Rejeté |
| NFQUEUE | IPS natif + failsafe | Requiert host network | Retenu |
Le failsafe --queue-bypass
L'argument décisif pour NFQUEUE : l'option --queue-bypass des règles nftables. Si aucun processus ne consomme la queue (container arrêté, crash, mise à jour), le kernel laisse passer le trafic automatiquement au lieu de le bloquer.
C'est un failsafe natif inégalable — aucune des alternatives ne propose un mécanisme aussi robuste pour maintenir la connectivité pendant les maintenances.
Architecture
flowchart LR
Packet["Paquet entrant<br/>WAN eth1"] --> NFT["nftables<br/>NFQUEUE"]
NFT --> Suricata["Suricata IPS<br/>50K rules"]
Suricata -->|"NF_ACCEPT"| Forward["Forward vers destination"]
Suricata -->|"NF_DROP"| Drop["Paquet supprime"]
NFT -.->|"queue-bypass<br/>si container down"| Forward
Mode nfq.mode: accept
Suricata utilise le mode accept (et non repeat). Dans ce mode :
- Les paquets qui matchent une règle
dropreçoivent un verdict NF_DROP → le kernel les supprime - Tous les autres paquets reçoivent NF_ACCEPT → le kernel les route normalement
Le mode repeat (ré-injection avec mark) est inutile ici et cause une charge CPU 3x supérieure avec 50K+ règles. À éviter sauf sur une infrastructure dédiée (8+ vCPUs).
Image durcie FROM scratch
Spécifications
| Propriété | Valeur |
|---|---|
| Image | suricata-hardened:8.0.5 |
| Base | FROM scratch (aucun OS) |
| Taille | ~45 MB compressée |
| PID 1 | tini-static |
| Init | Binaire Go custom |
| User | UID 8000 (non-root) |
| Capabilities | cap_net_admin,cap_sys_nice+ep (setcap) |
| Host network | Obligatoire (NFQUEUE) |
L'image suit le pattern générique 4-stage des images durcies (builder Rust/Cargo, Go builder pour l'init, prep + setcap, scratch final sans shell) — détail complet du pattern dans l'article dédié, commun à toutes mes images FROM scratch.
Pourquoi allow-host-networks est obligatoire
NFQUEUE opère au niveau du netfilter du kernel hôte. Le processus Suricata doit être dans le même network namespace que les interfaces réseau pour pouvoir consommer les paquets de la queue. Un container avec son propre namespace réseau ne verrait pas les queues nftables de l'hôte.
Gestion des règles
suricata-update embarqué dans l'image principale
Plus besoin d'une image séparée : depuis la 8.0.5, suricata-update (Python 3.12 + son runtime) est directement embarqué dans l'image suricata-hardened. La mise à jour tourne dans un container éphémère lancé depuis cette même image, en --user root :
# Arrêter Suricata (--queue-bypass maintient le trafic)
systemctl stop vyos-container-suricata.service
# Container ephemere depuis l'image principale, root pour contourner
# les file capabilities du binaire suricata en usage normal
podman run --rm --user root --network host --entrypoint python3 "$IMAGE" -c "
import os, sys
os.rename('/usr/bin/suricata', '/usr/bin/suricata.bak')
os.execvp('suricata-update', ['suricata-update'] + sys.argv[1].split())
" "update -f --no-test --suricata-version 8.0.5"
# Redémarrer Suricata avec les nouvelles règles
systemctl start vyos-container-suricata.service
Note : l'erreur
FileNotFoundError: /bin/shen fin d'exécution est bénigne (lereload-commandinterne de suricata-update tente d'invoquer un shell absent de l'image FROM scratch). Les règles sont écrites AVANT cette erreur.
Piège : --suricata-version obligatoire
Renommer /usr/bin/suricata casse la détection automatique de version de suricata-update — cet outil détecte normalement la version du moteur en exécutant le binaire lui-même. Sans lui, il retombe silencieusement sur son défaut caché 6.0.0 et télécharge le ruleset ET Open construit pour cette vieille version, sans erreur ni avertissement. Passer --suricata-version explicitement (dérivé du tag de l'image) est donc obligatoire, pas optionnel.
La politique de règles complète (modify.conf alert→drop, disable.conf faux positifs, métriques ~50K règles) est détaillée dans IPS : stratégie de détection et gestion des rules Suricata plutôt que redupliquée ici.
Performance
Verdict direct vs ré-injection
En mode accept, chaque paquet reçoit un verdict unique (ACCEPT ou DROP). Pas de ré-injection, pas de mark, pas de second passage dans nftables. C'est le mode le plus performant pour un routeur avec un nombre élevé de règles.
Impact CPU
Avec 50K règles actives et le trafic d'un homelab (~100 Mbps peak), Suricata consomme :
- Idle : <5% CPU (1 vCPU)
- Charge normale : 15-30% CPU
- Burst : 60-80% CPU (téléchargements volumineux)
Le routeur VyOS dispose de 4 vCPUs dédiés à cet usage.
Healthcheck
L'init Go intégré fournit deux méthodes de healthcheck :
- PID file : vérification de l'existence et de la validité du processus Suricata
- Unix socket : interrogation du socket
suricata-command.socketpour confirmation que le moteur d'inspection est actif
Le socket Unix permet également le reload-rules à chaud (via suricatasc) sans redémarrage complet — utile pour les mises à jour urgentes de signatures.
Sécurité de l'image
Surface d'attaque minimale
- Aucun shell : pas de
/bin/sh, pas de bash - Aucun package manager : pas d'apk, apt, ou pip
- Aucun outil de debug : pas de curl, wget, nc, ou cat
- Non-root : UID 8000, capabilities minimales via setcap
- Read-only : le filesystem est immuable (seuls les volumes sont en écriture)
Capabilities justifiées
| Capability | Justification |
|---|---|
cap_net_admin |
Obligatoire pour recevoir les verdicts NFQUEUE |
cap_sys_nice |
Priorité thread pour le traitement temps-réel des paquets |
cap_net_raw n'est PAS nécessaire en mode NFQUEUE (contrairement au mode AF_PACKET).
Conclusion
NFQUEUE offre le meilleur compromis pour un IPS inline sur un routeur VyOS :
- Blocage actif des menaces (verdict NF_DROP)
- Failsafe natif (--queue-bypass)
- Pas de point de défaillance unique (le réseau fonctionne sans Suricata)
- 50K règles avec un impact CPU acceptable
- Image minimale (FROM scratch, ~45 MB) réduisant la surface d'attaque
La combinaison VyOS + Suricata NFQUEUE + image durcie FROM scratch fournit un niveau de sécurité réseau comparable aux appliances commerciales, avec la flexibilité et la transparence de l'open source.
Liens
- Code source : suricata-hardened
- Image Docker : Docker Hub
- Déploiement Ansible : vyos-deploy