Suricata est un moteur de détection d’intrusion réseau : il examine les paquets qui entrent et sortent d’une machine, reconstitue les connexions, comprend les protocoles (HTTP, TLS, DNS, SSH…) et compare le tout à des dizaines de milliers de règles décrivant des attaques connues. En mode IPS, il peut aussi bloquer : un paquet qui correspond à une règle de blocage n’arrive jamais à destination.

Ce tutoriel installe Suricata en IPS sur un serveur Rocky Linux exposé à Internet, avec NFQUEUE. C’est le mode le plus utile sur un serveur seul, et aussi le plus délicat : Suricata se retrouve sur le chemin de chaque paquet. Je décris donc aussi, en détail, les pannes que j’ai rencontrées, dont une qui bloquait des connexions locales en silence pendant des semaines.

Schéma du chemin d'un paquet via firewalld, NFQUEUE et Suricata, avec les quatre réglages clés et la leçon du loopback
Le chemin d’un paquet et les réglages qui évitent les mauvaises surprises.

IDS ou IPS

ModeFonctionnementRisque
IDS (af-packet)Suricata reçoit une copie des paquets et alerteaucun pour le trafic : au pire, des alertes manquées
IPS (NFQUEUE)le noyau confie chaque paquet à Suricata, qui l’accepte ou le jetteSuricata est sur le chemin : une erreur bloque le trafic

Commencez en IDS si vous découvrez l’outil. Passez en IPS quand vous savez lire ses alertes et que vous avez éliminé les faux positifs les plus bruyants.

Étape 1 : installer

dnf install epel-release
dnf install suricata
suricata --build-info | grep -E 'NFQueue|AF_PACKET'   # les deux doivent être « yes »

Deux modules noyau sont nécessaires pour NFQUEUE : nfnetlink_queue (la file elle-même) et xt_NFQUEUE (la cible utilisée par les règles de pare-feu). Le second est souvent oublié : la règle échoue alors avec un message peu clair sur un module manquant. Chargez-les et rendez-les permanents :

modprobe nfnetlink_queue
modprobe xt_NFQUEUE
printf 'nfnetlink_queue\nxt_NFQUEUE\n' > /etc/modules-load.d/nfqueue.conf

Étape 2 : suricata.yaml, l’essentiel

# /etc/suricata/suricata.yaml (extraits)
vars:
  address-groups:
    HOME_NET: "[203.0.113.25/32,2001:db8::25/128]"
    EXTERNAL_NET: "!$HOME_NET"

nfq:
  mode: accept
  fail-open: yes

flow-timeouts:
  tcp:
    established: 7200

stream:
  memcap: 256mb
  midstream: true
  reassembly:
    memcap: 512mb
    depth: 1mb

outputs:
  - eve-log:
      enabled: yes
      filename: eve.json
      types: [alert, drop, anomaly, stats]
  • HOME_NET : les adresses que vous protégez. Beaucoup de règles en dépendent (« trafic venant de l’extérieur vers HOME_NET ») ; une valeur fausse donne à la fois des faux positifs et des attaques manquées ;
  • nfq.mode: accept : Suricata rend son verdict (accepter ou jeter) et le paquet poursuit ensuite son chemin normal dans le pare-feu ;
  • fail-open: yes : si la file déborde parce que Suricata ne suit pas, les paquets passent au lieu d’être jetés. Sur un serveur seul, mieux vaut laisser passer quelques paquets non inspectés que couper le service ;
  • flow-timeouts.tcp.established: 7200 : le point qui m’a coûté le plus cher, détaillé plus bas. Par défaut, Suricata oublie une connexion TCP inactive au bout d’une heure ; une connexion qui reste ouverte plus longtemps sans trafic voit ses paquets suivants traités comme des paquets « hors connexion », et parfois jetés ;
  • midstream: true : Suricata accepte de prendre en compte une connexion dont il n’a pas vu le début, par exemple après son propre redémarrage. Sans cela, toutes les connexions établies avant le démarrage sont considérées comme invalides ;
  • memcap : les plafonds de mémoire pour le suivi des flux et la reconstitution des données. Trop bas, Suricata abandonne l’inspection des connexions sous charge ; surveillez les compteurs memcap dans les statistiques ;
  • eve.json : le journal structuré, une ligne JSON par événement. C’est ce fichier qu’on envoie à un SIEM.

Étape 3 : les règles

suricata-update                      # télécharge le jeu ET Open (gratuit)
suricata-update list-sources
suricata-update enable-source et/open
ls -l /var/lib/suricata/rules/suricata.rules

suricata-update assemble toutes les règles dans un seul fichier, en appliquant vos fichiers de modification. Deux d’entre eux sont essentiels en IPS :

# /etc/suricata/drop.conf : ces familles de règles bloquent au lieu d'alerter
re:classtype:trojan-activity
re:classtype:command-and-control
re:classtype:domain-c2
re:classtype:exploit-kit
re:classtype:coin-mining

# /etc/suricata/disable.conf : protocoles industriels absents ici
re:dnp3
re:modbus

Par défaut, toutes les règles alertent seulement. drop.conf transforme en blocages celles dont la catégorie ne laisse pas de doute : communication avec un serveur de commande, cheval de Troie, minage de cryptomonnaie. Évitez de tout passer en blocage : une règle générique de « trafic suspect » déclenchée par une application légitime coupe cette application sans prévenir. disable.conf retire les règles inutiles, ce qui économise mémoire et processeur.

Planifiez une mise à jour quotidienne par un timer systemd qui lance suricata-update, puis demande à Suricata de recharger ses règles sans redémarrer (suricatasc -c reload-rules).

Étape 4 : envoyer le trafic à Suricata

Le service lit la file 0 :

# /etc/systemd/system/suricata.service.d/ips.conf
[Service]
ExecStart=
ExecStart=/usr/sbin/suricata -c /etc/suricata/suricata.yaml -q 0 --pidfile /var/run/suricata.pid

Puis les règles de pare-feu, avec firewalld en mode « direct » :

# 1. la boucle locale ne passe jamais par Suricata (priorité -1 : avant tout)
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT  -1 -i lo -j ACCEPT
firewall-cmd --permanent --direct --add-rule ipv4 filter OUTPUT -1 -o lo -j ACCEPT
# 2. tout le reste est confié à la file 0 ; --queue-bypass : si Suricata est arrêté, le trafic passe
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT   0 -j NFQUEUE --queue-num 0 --queue-bypass
firewall-cmd --permanent --direct --add-rule ipv4 filter OUTPUT  0 -j NFQUEUE --queue-num 0 --queue-bypass
firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -j NFQUEUE --queue-num 0 --queue-bypass
# mêmes règles en ipv6
firewall-cmd --reload
  • –queue-bypass : sans lui, si Suricata est arrêté ou plante, plus aucun paquet ne passe, SSH compris. Avec lui, le noyau laisse passer le trafic quand personne n’écoute la file. C’est la ligne qui évite d’avoir à appeler la console de secours de l’hébergeur ;
  • la boucle locale exclue : la leçon la plus coûteuse de ce tutoriel, détaillée ci-dessous ;
  • l’ordre : les règles directes de priorité -1 passent avant celles de priorité 0.

La leçon de la boucle locale

Pendant six semaines, un service de mon serveur (un gestionnaire de tâches qui parle à Redis sur 127.0.0.1) s’est figé à intervalles irréguliers, toujours exactement 31 minutes et 40 secondes, avant de repartir. Les journaux de l’application ne montraient rien, ceux de Redis non plus. La cause : Suricata inspectait aussi le trafic de la boucle locale. Une longue connexion inactive dépassait le délai de suivi des flux ; au réveil, ses paquets n’appartenaient plus à aucune connexion connue de Suricata, qui les jetait ; il fallait attendre que TCP abandonne et se reconnecte.

Le diagnostic est venu d’eve.json : chaque paquet jeté y apparaît avec sa raison.

jq -c 'select(.event_type=="drop") | {timestamp, src_ip, dest_ip, dest_port, reason: .drop.reason}' /var/log/suricata/eve.json | tail

Trois corrections, toutes nécessaires : exclure la boucle locale (le trafic entre processus d’une même machine n’a rien à faire dans un IPS), allonger le délai TCP à deux heures, et activer midstream. Retenez le symptôme : une connexion longue qui se fige un temps fixe et reproductible, sans erreur nulle part, c’est presque toujours un équipement intermédiaire (pare-feu, IPS, traduction d’adresses) qui a oublié la connexion.

Le piège des cartes réseau

Les cartes et pilotes modernes regroupent plusieurs paquets en un seul « super-paquet » avant de les remettre au noyau (GRO, LRO). Suricata voit alors des paquets plus gros que la taille réelle du réseau, et sa reconstitution des connexions s’en trouve faussée. Désactivez ces optimisations sur l’interface surveillée, de façon permanente, par NetworkManager plutôt que par un simple ethtool -K qui serait annulé à la reconnexion suivante :

nmcli connection modify eth0 ethtool.feature-gro off ethtool.feature-lro off \
    ethtool.feature-gso off ethtool.feature-tso off
nmcli connection up eth0
ethtool -k eth0 | grep -E 'generic-receive|large-receive'

Étape 5 : démarrer et vérifier

suricata -T -c /etc/suricata/suricata.yaml        # test de configuration et des règles
systemctl enable --now suricata
tail -f /var/log/suricata/suricata.log            # « engine started »
curl -s http://testmynids.org/uid/index.html      # doit déclencher l'alerte 2100498
jq -c 'select(.event_type=="alert") | .alert.signature' /var/log/suricata/eve.json | tail -3

Vérifiez aussi que le trafic normal passe toujours : connexion SSH neuve, site web, envoi de courrier, résolution DNS. Puis surveillez les statistiques pendant quelques jours :

jq -c 'select(.event_type=="stats") | .stats | {drop: .ips.blocked, memcap: .tcp.ssn_memcap_drop, kernel: .capture}' /var/log/suricata/eve.json | tail -1

Les mêmes compteurs sur un serveur en production

Suricata 8.0.8 en mode IPS sur mon serveur web, lu par le socket de contrôle le 2 novembre 2026 après 75 minutes de fonctionnement. suricatasc donne les compteurs en direct, sans attendre la prochaine ligne de statistiques dans eve.json :

# suricatasc -c iface-list
{"message": {"count": 1, "ifaces": ["NFQ#0"]}, "return": "OK"}

# suricatasc -c ruleset-stats
[{"id": 0, "rules_loaded": 53325, "rules_failed": 0, "rules_skipped": 0}]

# suricatasc -c dump-counters | jq '.message | {uptime, ips, alerts: .detect.alert}'
uptime        4544 s
ips.accepted  2 076 277
ips.blocked   771
  drop_reason.stream_error    769
  drop_reason.applayer_error  2
  drop_reason.rules           0
alerts        23
  • NFQ#0 : Suricata est branché sur la file netfilter 0, il voit donc les paquets au passage et peut les jeter. En mode IDS, cette liste montrerait une interface réseau.
  • 53 325 règles chargées, 0 en échec : un rules_failed non nul signifie le plus souvent qu’une règle utilise un mot-clé que votre version ne connaît pas, après une mise à jour des règles ou du moteur.
  • 771 paquets bloqués, 769 pour stream_error, 0 par les règles : c’est le tableau normal d’un serveur web. Les rejets par règle sont rares ; la plupart des paquets jetés n’ont aucun sens pour le flux TCP auquel ils prétendent appartenir (retransmissions après la fermeture, numéros de séquence faux, sommes de contrôle invalides). Si ce nombre grimpe d’un coup, regardez les événements drop dans eve.json : la leçon de la boucle locale, plus haut, a commencé par exactement ce genre de pic.
  • 23 alertes en 75 minutes, aucune bloquante : des anomalies de protocole (trop de SYN pour une même poignée de main, protocoles applicatifs incohérents, horodatages invalides) journalisées pour le SIEM. Pourtant, 23 665 des règles chargées sont réglées pour bloquer, par classe (chevaux de Troie, serveurs de commande, kits d’exploitation, minage…) grâce à drop.conf : pendant cette heure, aucune ne s’est déclenchée. Une règle de blocage qui ne se déclenche jamais, c’est l’état attendu, pas le signe d’une panne.

Faire taire un faux positif

Une règle qui se déclenche sur un trafic légitime se neutralise, du plus précis au plus large : seuil ou suppression pour une adresse dans threshold.config, puis désactivation de la règle par son numéro dans disable.conf. Ne modifiez jamais le fichier de règles directement : la prochaine mise à jour l’écraserait.

# /etc/suricata/threshold.config
suppress gen_id 1, sig_id 2210045, track by_src, ip 203.0.113.40

Envoyer les alertes au SIEM

eve.json est lisible par Wazuh, Elastic, Graylog et la plupart des SIEM. Avec un agent Wazuh, quelques lignes suffisent (format json, chemin du fichier) et les alertes de Suricata apparaissent à côté des journaux système, ce qui permet de rapprocher une alerte réseau d’une connexion SSH ou d’un changement de fichier. Pensez à la rotation : eve.json grossit vite, et logrotate doit envoyer à Suricata le signal de rouvrir le fichier.

Mettre à jour Suricata

Après chaque mise à jour du paquet, relancez suricata -T avant le redémarrage : une option renommée ou une règle incompatible empêche le démarrage, et grâce à --queue-bypass le trafic passera alors sans inspection, sans que rien ne casse visiblement. C’est le revers de la sécurité de fonctionnement : surveillez l’état du service, pas seulement le trafic.


Ce billet fait partie d’une série de tutoriels d’administration système (tag « pas à pas »).
Série « Pas à pas », tutoriel 10/77. · ← Tutoriel précédent · Tutoriel suivant → (à paraître le 11/11) · English version

Articles liés