IPS : Strategie de detection et gestion des rules Suricata

par

dans

Un IPS ne vaut pas par son moteur mais par sa politique de règles : ce qu'on bloque vraiment, ce qu'on se contente d'observer, et ce qu'on éteint parce que c'est du trafic normal. Cet article documente cette politique et le mécanisme qui la maintient à jour. Le moteur lui-même, son mode inline et son image durcie ont le leur.

Tout arrive en mode alerte, et ce n'est pas ce qu'on veut

Les règles Emerging Threats arrivent en mode alert : elles produisent un événement et laissent passer le trafic. Un IPS installé sans y toucher est donc un IDS — il regarde et ne fait rien.

Un fichier de conversion promeut les familles jugées sûres de alert vers drop :

# Menaces confirmees
re:"classtype:trojan-activity" "alert" "drop"
re:"classtype:command-and-control" "alert" "drop"
re:"classtype:attempted-admin" "alert" "drop"

# Sources malveillantes connues
re:"ET DROP" "alert" "drop"
re:"ET CINS" "alert" "drop"
re:"ET MALWARE" "alert" "drop"
re:"ET CNC" "alert" "drop"

# Attaques actives
re:"ET EXPLOIT" "alert" "drop"
re:"ET SCAN.*Zmap" "alert" "drop"
re:"ET TROJAN" "alert" "drop"

# Attaques web
re:"PHP|SQLi|RCE|WebShell" "alert" "drop"
re:"GPL DNS.*recon" "alert" "drop"

Le critère de promotion est la fiabilité de la signature, pas la gravité de la menace. Une règle qui décrit un comportement sans ambiguïté — une famille de maliciel connue, un canal de commande — peut bloquer. Une règle heuristique reste en alerte, parce qu'un faux positif en drop coupe du trafic légitime sans prévenir.

Éteindre ce qui est du trafic normal chez soi

Certaines règles se déclenchent sur du trafic parfaitement normal dans ce réseau. Les éteindre par identifiant est plus honnête que de relever le seuil global :

# Methode CONNECT du proxy (trafic transparent normal)
2013926
2013927
2013928

# Robot d'indexation (trafic legitime)
2032981

# Verification de connectivite Android
2036220
2046370
2046428

# Reponses 403 du pare-feu applicatif (pas une menace reseau)
2010516
2101201

# Outil de prise en main a distance (usage legitime)
2030668
2060624
2060625
2060626
2060627
2060628
2060629
2060630
2060631
2060632

Le cas des 403 mérite un mot : ce sont les refus du pare-feu applicatif, c'est-à-dire la trace d'une protection qui a fonctionné. Les compter comme une menace réseau reviendrait à s'alerter de son propre succès.

Ce que la politique donne réellement

flowchart LR
    A["Regles ET Open"] --> B{"Politique appliquee"}
    B --> C["Environ la moitie<br/>en blocage actif"]
    B --> D["Le reste<br/>en detection passive"]
    B --> E["Une vingtaine<br/>desactivees"]
    style C fill:#e74c3c,color:#fff
    style D fill:#f39c12,color:#fff
    style E fill:#95a5a6,color:#fff

Environ la moitié du jeu de règles bloque activement, l'autre observe, et une vingtaine de signatures sont éteintes. L'équilibre est volontaire : les menaces non ambiguës sont coupées, les signatures moins fiables alimentent la supervision pour analyse.

Mettre à jour les règles quand l'image n'a pas de shell

L'outil de mise à jour est embarqué dans l'image principale, ce qui évite d'en maintenir une seconde. La mise à jour tourne dans un container éphémère lancé depuis cette même image :

# Arreter le moteur -- le failsafe maintient le trafic
systemctl stop vyos-container-suricata.service

# Container ephemere, root pour contourner les file capabilities
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 $VERSION"

systemctl start vyos-container-suricata.service

Le jeu de règles téléchargé n'était pas celui du moteur

Voilà le piège qui a coûté le plus longtemps, parce qu'il n'a jamais rien cassé.

L'outil de mise à jour détecte normalement la version du moteur en exécutant le binaire. Or ce binaire est renommé le temps de l'opération, pour contourner les capabilities posées dessus. La détection échoue donc — silencieusement — et l'outil retombe sur son propre défaut interne, 6.0.0, une version que ce moteur n'a jamais eue.

Résultat : pendant des semaines, il téléchargeait consciencieusement le jeu de règles construit pour une génération obsolète du moteur. Rien ne plantait, rien n'alertait, la mise à jour se terminait « avec succès ». Seules les règles étaient les mauvaises.

La correction consiste à passer la version explicitement, dérivée du tag de l'image plutôt que réécrite à la main — sans quoi on crée une seconde source de vérité qui divergera un jour.

La leçon dépasse l'outil : une valeur par défaut cachée est plus dangereuse qu'une erreur, parce qu'elle produit un résultat plausible. Une exécution qui échoue se voit ; une exécution qui réussit avec la mauvaise entrée ne se voit pas.

Un code de sortie non nul qui ne veut rien dire

Dans la même veine, et bénin cette fois : l'étape finale de l'outil tente d'invoquer un shell pour sa commande de rechargement interne. Il n'y en a pas dans une image FROM scratch. L'opération se termine donc systématiquement en erreur, alors que les règles sont déjà écrites sur le disque à ce stade.

Se fier au code de sortie ferait conclure à un échec à chaque exécution pourtant réussie. C'est le symétrique exact du piège précédent : ici le signal crie alors que tout va bien, là il se taisait alors que tout allait mal. Dans les deux cas, le code de sortie ne dit pas ce qu'on croit — il faut vérifier l'effet, pas le statut.

Le pipeline complet

flowchart TD
    A["Sources ET Open"] --> B["Telechargement"]
    B --> C["Conversion<br/>alerte vers blocage"]
    C --> D["Desactivation<br/>des faux positifs"]
    D --> E["Compilation"]
    E --> F["Volume partage"]
    F --> G["Moteur IPS<br/>chargement au demarrage"]
    G --> H{"Verdict"}
    H --> I["Trafic autorise"]
    H --> J["Menace bloquee"]
    J --> K["Journal vers<br/>la supervision"]
    style J fill:#e74c3c,color:#fff
    style I fill:#27ae60,color:#fff

Liens

Articles connexes