Mettre en place du filtrage applicatif avec nftables et Suricata sur Debian

Traductions

Sommaire

Dans cet article, je vous propose de découvrir ou redécouvrir le filtrage applicatif avec notamment un support pour mettre en place du filtrage applicatif avec un pare-feu netfilter en employant l’utilitaire nftables ainsi que l’outil Suricata sur le système d’exploitation Debian GNU/Linux. L’objectif est de pouvoir détecter et bloquer des flux dont les comportements ne correspondraient pas à ceux attendus pour les protocoles réseau qui sont autorisés sur un pare-feu.

Introduction

Le filtrage de paquets est la première forme de pare-feu introduite à la fin des années 1980, permettant d’autoriser ou bloquer des paquets de données en transit sur un réseau selon certains critères. Au début des années 1990, une amélioration permet de filtrer les paquets selon les sessions établies, on parle alors de filtrage de sessions effectué par un pare-feu à état ou stateful firewall. Dans les années 2000, une nouvelle forme de pare-feu arrive massivement sur le marché en tant que pare-feu de nouvelle génération ou next-generation firewall, dans le sens où de nouvelles fonctionnalités de sécurité sont apportées en plus. Elles reposent sur l’inspection des paquets sur les couches hautes du modèle OSI appelée inspection en profondeur des packet ou Deep Packet Inspection (DPI), où la charge utile ou payload d’un paquet est analysée à la recherche de critères spécifiques supplémentaires pour filtrer des flux, d’anomalies, de tentatives d’exploitation de vulnérabilités et d’encore beaucoup d’autres choses. Nous allons nous intéresser au filtrage applicatif, déjà développé et proposé au début des années 1990, qui permet d’identifier le type d’application, au sens protocole réseau élargi (i.e. FTP, HTTP, Facebook, YouTube, SSH, Telnet, BitTorent, NFS…) afin de filtrer plus précisemment qu’au niveau Transport du modèle OSI où ce sont principalement les valeurs des protocoles et ports sont évaluées. Je vous propose de voir comment le mettre en place sur une solution open source reposant sur netfilter et Suricata sur Debian.

Le filtrage applicatif

Le filtrage applicatif est une fonctionnalité de sécurité très intéressante afin de réduire la surface d’attaque au niveau des réseaux. En effet, les flux réseau autorisés permettent de véhiculer facilement des attaques (e.g. exploitation de vulnérabilités, réplication de vers, téléchargement de virus…). Cela est encore plus vrai lorsque les types de flux qui circulent peuvent ne pas correspondre aux protocoles autorisés à circuler. Par exemple, un flux TCP sur le port 80 a été autorisé mais ce sont des flux SSH qui circulent via ce port TCP 80 (théoriquement, rien n’empêche un administrateur de faire tourner un serveur SSH sur le port 80 !). Plus globalement, cette fonctionnalité permet de contrôler quels flux d’application sont autorisés à circuler sur le réseau. Le terme application est ici régulièrement employé d’un point de vue du pare-feu réseau, et non du composant logiciel installé sur un serveur. On distingue généralement deux grandes familles d’applications : celles apparentées aux services réseaux communs (et associées à des valeurs de ports inférieures à 1024 : FTP, DNS, DHCP, HTTP…) et celles apparentées à des logiciels (et associées à des valeurs de ports supérieurs ou égales à 1024).

Sans filtrage applicatif, n'importe quelle application peut transiter sur les services ouverts

Avec filtrage applicatif, seules les applications autorisées peuvent transiter sur les services ouverts

Déploiement avec nftables et Suricata

Je vous propose maintenant de voir pas-à-pas comment mettre en place du filtrage applicatif avec une combinaison de solutions open source : netfilter via nftables, ainsi que Suricata.

nftables

Netfilter est un projet et un logiciel de filtrage de paquet pour distributions Linux. Il est accompagné d’iptables, un célèbre utilitaire pour configurer les règles de filtrage dont il existe un successeur, nftables, plus flexible et performant.
iptables et nftables fonctionnent avec des tables qui permettent de réaliser différent types d’opération. Dans iptables, il y a des tables prédéfinies (par exemple, la table filter permet d’effectuer du filtrage de paquet, tandis que la table nat permet de réaliser de la traduction d’adresse) là où dans nftables aucune n’existe par défaut et il faut donc les créer.
Au sein des tables, des chaînes regroupent l’ensemble des règles et notamment les règles de filtrage dans le cas de la table filter. Dans iptables, il y a des chaînes prédéfinies (par exemple, la chaîne input permet de fitrer les paquets à destination par le pare-feu, la chaîne output permet de fitrer les paquets émis par le pare-feu et la chaîne forward permet de fitrer les paquets transmis par le pare-feu) là où dans nftables aucune n’existe par défaut et il faut donc les créer.

La première étape est d’installer nftables :

$ sudo -- sh -c 'apt-get update && apt-get install -y nftables'
Vous pouvez alors charger la configuration par défaut afin de créer une table de type filter et des chaînes de types input, output et forward.
# nft -f /etc/nftables.conf
Je conseille cependant fortement de changer la politique par défaut (qui est d’accepter tous les paquets sur les 3 chaînes) à minima sur la chaîne forward en bloquant tous les paquets par défaut :
# nft 'add chain inet filter forward { type filter hook forward priority 0; policy drop; }'
Avec nftables (et iptables), des règles de filtrage de paquets caractérisent un trafic donné pour l’autoriser ou le bloquer. On peut y ajouter la fonction de filtrage de sessions pour n’autoriser que les paquets associés à de nouvelles sessions ou à des sessions établies. De plus, un autre type d’action peut être entrepris : transmettre le paquet caractérisé à un autre logiciel pour traitement supplémentaire. Plus précisemment, on parle d’envoyer le paquet dans une queue pour traitement en espace utilisateur ou user space par une application tierce. L’espace utilisateur contient l’ensemble du code (et donc des logiciels) tournant en dehors du noyau ou kernel du système d’exploitation. Je vous invite à consulter cette page pour apprendre à faire des règles avec nftables.
Sur nftables, pour indiquer l’utilisation des queues pour un trafic donné, il suffit d’utiliser le mot-clé queue en tant qu’action dans les règles de filtrage. Par exemple, si on veut envoyer dans une queue le trafic du protocole TCP sur le port destination 80 (avec filtrage de sessions : associé à des nouvelles sessions ou à des sessions existantes) et le trafic du protocole TCP sur le port source 80 (avec filtrage de sessions : associé à des sessions existantes car il s’agit du trafic retour) transmis par le pare-feu, cela donne :
# nft add rule inet filter forward tcp dport 80 ct state new, established counter queue
# nft add rule inet filter forward tcp sport 80 ct state established counter queue
Si on liste les règles de la table filter, voici alors ce qu’on obtient :
# nft -a list table inet filter
table inet filter { # handle 4
	chain input { # handle 1
		type filter hook input priority filter; policy accept;
	}

	chain forward { # handle 2
		type filter hook forward priority filter; policy drop;
		tcp dport 80 ct state established,new counter packets 0 bytes 0 queue num 0-3 # handle 5
		tcp sport 80 ct state established counter packets 0 bytes 0 queue num 0-3 # handle 6
	}

	chain output { # handle 3
		type filter hook output priority filter; policy accept;
	}
}
On peut aussi spécifier dans quelle(s) queue(s) répartir le trafic, par exemple :
# nft add rule inet filter forward tcp dport 80 ct state new, established counter queue num 0-3
# nft add rule inet filter forward tcp sport 80 ct state established counter queue num 0-3
En outre, on peut aussi demander d’autoriser les paquets si jamais aucune application tierce ne surveille les queues (car bloqué par défaut dans ce cas) :
# nft add rule inet filter forward tcp dport 80 ct state new, established counter queue num 0-3 bypass
# nft add rule inet filter forward tcp sport 80 ct state new, established counter queue num 0-3 bypass
Il nous manque désormais un composant tiers qui va inspecter les paquets transmis dans les queues.

Suricata

Suricata est un système de détection d’intrusion ou Intrusion Detection System (IDS) qui permet de définir des règles reposant sur des signatures (relatives au contenu de la payload des paquets) déclenchées sur un trafic caractérisé et avec lesquelles des actions (e.g. alerter, bloquer) sont prises. Suricata étant capable de lire dans des queues, on peut donc, à partir de nftables, transmettre des paquets pour traitement plus poussé à Suricata qui complète alors le filtrage de sessions par du filtrage applicatif. Le schéma ci-dessous illustre le fonctionnement de ce méchanisme.

nftables et Suricata peuvent unir leurs forces

Installons d’abord Suricata :

$ sudo -- sh -c 'apt-get update && apt-get install -y suricata'

Les règles de Suricata sont placées dans des fichiers localisés dans un répertoire (par défaut /etc/suricata/rules/)

# ls /etc/suricata/rules/
app-layer-events.rules  dhcp-events.rules  dns-events.rules  http-events.rules   kerberos-events.rules  nfs-events.rules  smb-events.rules   stream-events.rules
decoder-events.rules    dnp3-events.rules  files.rules       ipsec-events.rules  modbus-events.rules    ntp-events.rules  smtp-events.rules  tls-events.rules
Vous pouvez mette à jour les règles prédéfinies avec la commande suivante :
# suricata-update
Il y a plusieurs manières de réaliser du filtrage applicatif avec Suricata, l’outil étant très modulaire. La première consiste à utiliser un jeu de règles par défaut pour détecter des anomalies de comportements protocolaires et bloquer des flux qui ne correspondrait pas à ce qui est attendu au niveau applicatif.
Le fichier qui nous intéresse ici est app-layer-events.rules. En effet, il contient plusieurs règles prédéfinies afin de détecter des problèmes applicatifs.
# App layer event  rules
#
# SID's fall in the 2260000+ range. See http://doc.emergingthreats.net/bin/view/Main/SidAllocation
#
# These sigs fire at most once per connection.
#
# A flowint applayer.anomaly.count is incremented for each match. By default it will be 0.
#
alert ip any any -> any any (msg:"SURICATA Applayer Mismatch protocol both directions"; flow:established; app-layer-event:applayer_mismatch_protocol_both_directions; flowint:applayer.anomaly.count,+,1; classtype:protocol-command-decode; sid:2260000; rev:1;)
alert ip any any -> any any (msg:"SURICATA Applayer Wrong direction first Data"; flow:established; app-layer-event:applayer_wrong_direction_first_data; flowint:applayer.anomaly.count,+,1; classtype:protocol-command-decode; sid:2260001; rev:1;)
alert ip any any -> any any (msg:"SURICATA Applayer Detect protocol only one direction"; flow:established; app-layer-event:applayer_detect_protocol_only_one_direction; flowint:applayer.anomaly.count,+,1; classtype:protocol-command-decode; sid:2260002; rev:1;)
alert ip any any -> any any (msg:"SURICATA Applayer Protocol detection skipped"; flow:established; app-layer-event:applayer_proto_detection_skipped; flowint:applayer.anomaly.count,+,1; classtype:protocol-command-decode; sid:2260003; rev:1;)
# alert if STARTTLS was not followed by actual SSL/TLS
alert tcp any any -> any any (msg:"SURICATA Applayer No TLS after STARTTLS"; flow:established; app-layer-event:applayer_no_tls_after_starttls; flowint:applayer.anomaly.count,+,1; classtype:protocol-command-decode; sid:2260004; rev:2;)
# unexpected protocol in protocol upgrade
alert tcp any any -> any any (msg:"SURICATA Applayer Unexpected protocol"; flow:established; app-layer-event:applayer_unexpected_protocol; flowint:applayer.anomaly.count,+,1; classtype:protocol-command-decode; sid:2260005; rev:1;)

#next sid is 2260006
C’est la première règle qui nous intéresse particulièrement, car elle permet de détecter si les flux dans un sens (par exemple d’un client SSH vers un serveur Web) ne sont pas de la même application (protocole) que ceux dans l’autre sens (par exemple d’un serveur Web vers un client SSH). En revanche, elle est ici en mode détection et non pas en mode bloquant. Le mot-clé alert est donc à remplacer par reject (si vous souhaitez écarter le paquet et renvoyer un message TCP RST ou ICMP Port Unreachable à l’émetteur du paquet) ou drop (si vous souhaitez juste écarter le paquet). Je vous invite à consulter cette page pour voir toutes les actions disponibles.
# sed -s 's/alert/reject/g' /etc/suricata/rules/app-layer-events.rules

En plus de la détection et blocage générique des flux anormaux d’un point de vue application, je vous invite à rajouter des règles de détection propre à chaque application. L’objectif est de détecter si, pour un port donné, il s’agit bien de l’application attendue. Par exemple, si on veut bloquer des flux sur le port TCP 80 qui ne seraient pas du HTTP, on peut créer une règle de la sorte :

reject tcp any any -> any 80 (msg:"SURICATA Port 80 but not HTTP"; flow:to_server; app-layer-protocol:!http; sid:2271002; rev:1;)
Je vous invite à consulter cette page pour voir comment construire des règles Suricata. Voici néanmoins la liste des mots-clés pouvant être actuellement employés pour les applications:

  • http
  • ftp
  • tls (this includes ssl)
  • smb
  • dns
  • dcerpc
  • ssh
  • smtp
  • imap
  • modbus (disabled by default)
  • dnp3 (disabled by default)
  • enip (disabled by default)
  • nfs
  • ikev2
  • krb5
  • ntp
  • dhcp
  • rfb
  • rdp
  • snmp
  • tftp
  • sip
  • http2

Il est donc tout à fait possible de créer une règle pour chacune de ces applications en choisissant les ports qu’on souhaite associer comme étant autorisés.

Une fois le fichier de règles prêt, il faut déclarer son utilisation dans la section rules-files du fichier de configuration de Suricata (/etc/suricata/suricata.yaml):

rules-files:
 - app-layer-events.rules

Enfin, vous pouvez démarrer Suricata en spécifiant éventuellement le numéro des queues à surveiller :

# suricata -c /etc/suricata/suricata.yaml -q 0:3 &

Les journaux ou logs des alertes de Suricata apparaissent dans un fichier situé dans un répertoire dédié (par défaut /var/log/suricata/, qui peut être modifié dans le fichier de configuration de Suricata).
Voici un exemple de log lorsque j’ai testé un accès SSH d’un client vers un serveur SSH sur le port TCP 80 :

# tail -f /var/log/suricata/fast.log 
02/04/2023-08:59:19.150163  [Drop] [**] [1:2271002:1] SURICATA Port 80 but not HTTP [**] [Classification: (null)] [Priority: 3] {TCP} 192.168.1.1:47900 -> 193.167.10.13:80
Voici un exemple de log lorsque j’ai testé un accès Web d’un client vers un serveur SSH sur le port TCP 80 :
# tail -f /var/log/suricata/fast.log 
02/04/2023-08:59:38.823127  [Drop] [**] [1:2260000:1] SURICATA Applayer Mismatch protocol both directions [**] [Classification: Generic Protocol Command Decode] [Priority: 3] {TCP} 193.167.10.13:80 -> 192.168.1.1:47902

La solution avec nftables et Suricata que j’ai présenté reste à visée pédagogique mais n’a pas de vocation à être utilisé dans un large environnement de production. En effet, la configuration reste assez lourde (deux outils à configurer avec des règles contenant beaucoup de paramètres à spécifier) alors que l’extensibilité et les performances ne peuvent être garanties et le nombre d’applications proposé est restreint. Cela dit, même avec des produits actuels du marché, de grands défis se posent pour effectuer du filtrage applicatif à grande échelle.

Les défis du filtrage applicatif

Il existe plusieurs défis à la mise en place du filtrage applicatif dans un environnement de production. En plus de poser les problématiques, j’essaye ici de vous aiguiller vers des solutions qui dépendront toujours du contexte.

  • Que faire lorsque que sur le pare-feu, il n’existe pas d’application pour contrôler certains protocoles réseau ?
    Il est généralement recommandé de créer des signatures d’application personnalisées à partir de de captures de paquets de trafic réel, ou simplement en caractérisant au mieux le trafic à partir de critères assez génériques : sens de la connection, timers TCP, valeurs de certains en-têtes procotolaires…

  • Comment gérer les mises à jour des signatures des applications à mesure que le trafic associé évolue ? Par exemple, si vous utilisez une signature pour une application apparentée à un composant logiciel (e.g. un serveur de bases de données) dont les échanges réseau changent à la suite d’une mise à jour de ces composants : comment s’assurer que la signature est mise à jour à temps et correctement sur le pare-feu ?
    Il est généralement recommandé de sécuriser certains flux en laissant des règles de filtrage sous-jacentes reposant sur des services sans filtrage applicatif, le temps d’avoir confiance en la capacité du pare-feu à être à jour. Aussi, lors des mises à jour de la base de signatures d’applications du pare-feu, pour les flux critiques, cloner temporairement les règles de filtrage applicatif avec des services sans filtrage applicatif est une bonne pratique pour éviter les impacts. Si la règle clone est utilisée, c’est qu’il y a un soucis avec la mise à jour des applications utilisé dans la règle originelle.

  • Comment effectuer du filtrage applicatif sur des flux chiffrés ?
    La seule solution est d’intégrer le déchiffrement des flux au sein du pare-feu afin que celui-ci soit en capacité d’inspecter la payload des paquets initialement chiffrés. Cela pouvant être assez lourd à mettre en œuvre, il faut aussi accepter de ne pas effectuer de filtrage applicatif dans ces cas, au-delà de filtrer sur le protocole de chiffrement (TLS par exemple).

  • Comment déterminer les ports associés à une application dans une règle de filtrage ?
    Un flux applicatif pouvant en théorie utiliser n’importe quel port réseau, il est préférable d’associer un ou plusieurs ports en plus d’une application sur une règle de filtrage. Ceci permet de combiner les deux fonctionnalités : filtrage de paquets/sessions et filtrage applicatif.

  • Les pare-feux applicatifs sont parfois contraints de laisser passer les premiers échanges pour arriver à détecter quelle application est en train de circuler. Bien que les algorithmes et signatures s’améliorent au fil du temps, cela reste une forme de vulnérabilité où un attaquant pourrait mener une attaque via ces premiers paquets. Comment pallier ce genre de failles ?
    D’abord, cela reste à mettre en perspective avec la valeur ajoutée du filtrage applicatif par rapport à du filtrage de paquets ou de sessions. Ensuite, une solution de sécurité n’est jamais seule pour sécuriser une communication. Au niveau du réseau, on peut donc envisager de compléter avec de l’analyse de trafic par des sondes tels que des IDS ou des outils de Network Detection and Response (NDR), ou bien encore des contrôles d’accès au réseau accrus avec des proxys, du Network Access Control (NAC), de la macrosegmentation et microsegmentation des réseaux, etc.

Conclusion

Nous avons vu la valeur ajoutée du filrage applicatif, puis son fonctionnement avec des solutions open source netfilter (via nftables) et Suricata au travers d’un exemple à but pédagogique. Le filtrage applicatif est aujourd’hui très répandu sur le marché des pare-feux mais reste assez exigeant à mettre en place pour limiter les impacts opérationnels et reste donc à intégrer avec précaution dans une stratégie claire.

Sources

Traductions