Suricata hardened : image Docker durcie pour NFQUEUE

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 drop reç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/sh en fin d'exécution est bénigne (le reload-command interne 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 :

  1. PID file : vérification de l'existence et de la validité du processus Suricata
  2. Unix socket : interrogation du socket suricata-command.socket pour 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

Articles connexes