Pare-feu et filtrage#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.gridspec as gridspec
import numpy as np
import pandas as pd
import seaborn as sns
from collections import Counter
import re

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Netfilter — architecture et hooks#

Netfilter est le framework de filtrage intégré au noyau Linux. Il définit cinq points d’accroche (hooks) dans le chemin de traitement des paquets IP. Tous les outils de filtrage — iptables, nftables, ipvs — s’appuient sur ces mêmes hooks.

Les cinq hooks Netfilter#

Hook

Moment d’exécution

NF_INET_PRE_ROUTING

À la réception, avant la décision de routage

NF_INET_LOCAL_IN

Paquet destiné à la machine locale

NF_INET_FORWARD

Paquet à router vers une autre interface

NF_INET_LOCAL_OUT

Paquet généré localement, avant émission

NF_INET_POST_ROUTING

Après la décision de routage, avant émission

Tables Netfilter#

Netfilter organise les règles en tables selon leur fonction :

Table

Rôle

Hooks utilisés

filter

Accepter / rejeter des paquets

INPUT, FORWARD, OUTPUT

nat

Translation d’adresses

PREROUTING, OUTPUT, POSTROUTING

mangle

Modifier les en-têtes IP (TTL, TOS, marques)

Tous

raw

Court-circuiter le suivi de connexion

PREROUTING, OUTPUT

security

Labels SELinux sur les paquets

INPUT, FORWARD, OUTPUT

La table filter est celle qu’on utilise presque exclusivement pour la politique de sécurité. La table nat gère le masquerading et le port forwarding.

iptables — syntaxe et cibles#

Structure d’une règle iptables#

iptables -t TABLE -A CHAINE [critères] -j CIBLE

Élément

Valeurs courantes

-t TABLE

filter (défaut), nat, mangle

Commandes

-A append, -I insert, -D delete, -R replace, -L list, -F flush, -P policy

-p PROTO

tcp, udp, icmp, all

-s SRC

Adresse/réseau source

-d DST

Adresse/réseau destination

--dport PORT

Port destination (avec -p tcp/udp)

--sport PORT

Port source

-i IFACE

Interface entrante

-o IFACE

Interface sortante

Cibles (targets)#

Cible

Effet

ACCEPT

Laisser passer le paquet

DROP

Silencieusement rejeter

REJECT

Rejeter avec réponse ICMP

LOG

Journaliser sans décision finale

MASQUERADE

SNAT dynamique (IP source = IP interface)

DNAT

Modifier l’IP destination

SNAT

Modifier l’IP source

RETURN

Remonter à la chaîne appelante

Modules d’extension#

# -m state : suivi d'état simplifié (alias de conntrack)
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# -m conntrack : suivi de connexion complet
iptables -A INPUT -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# -m limit : limitation de débit
iptables -A INPUT -p icmp -m limit --limit 10/s --limit-burst 20 -j ACCEPT

# -m multiport : plusieurs ports en une règle
iptables -A INPUT -p tcp -m multiport --dports 80,443,8080 -j ACCEPT

# -m recent : blocage d'IP répétitives (anti-scan)
iptables -A INPUT -p tcp --dport 22 -m recent --update --seconds 60 --hitcount 5 -j DROP

# -m iprange : plage d'adresses
iptables -A INPUT -m iprange --src-range 192.168.1.1-192.168.1.50 -j ACCEPT

# -m string : inspection de contenu (couche 7)
iptables -A FORWARD -m string --string "malware.exe" --algo bm -j DROP

Règles iptables essentielles#

Politique de base — serveur sécurisé#

# === Sauvegarder les règles actuelles ===
iptables-save > /etc/iptables/rules.v4.bak

# === Politique par défaut : tout bloquer en entrée et transit ===
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# === Trafic loopback ===
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# === Connexions établies / liées ===
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# === SSH (avec limitation anti-brute-force) ===
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
    -m limit --limit 3/min --limit-burst 5 -j ACCEPT

# === HTTP et HTTPS ===
iptables -A INPUT -p tcp -m multiport --dports 80,443 \
    -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# === ICMP (ping) limité ===
iptables -A INPUT -p icmp --icmp-type echo-request \
    -m limit --limit 5/s --limit-burst 10 -j ACCEPT

# === Journaliser les paquets rejetés ===
iptables -A INPUT -m limit --limit 5/min -j LOG \
    --log-prefix "IPTables-DROP: " --log-level 4
iptables -A INPUT -j DROP

# === Sauvegarder les règles ===
iptables-save > /etc/iptables/rules.v4

Restauration au démarrage#

# Debian/Ubuntu : paquet iptables-persistent
apt install iptables-persistent
netfilter-persistent save

# systemd (manuel)
cat > /etc/systemd/system/iptables-restore.service << 'EOF'
[Unit]
Description=Restore iptables rules
Before=network.target

[Service]
Type=oneshot
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF
systemctl enable iptables-restore

nftables — syntaxe moderne#

nftables remplace iptables depuis le noyau 3.13 et est l’outil par défaut sur Debian 10+, Ubuntu 20.04+, RHEL 8+. Il unifie IPv4, IPv6, ARP et bridge en un seul framework.

Concepts nftables#

  • Table : espace de noms avec une famille (inet, ip, ip6, arp, bridge)

  • Chaîne : attachée à un hook netfilter, avec une priorité

  • Règle : expression de correspondance + action

Syntaxe de base#

# Lister la configuration complète
nft list ruleset

# Créer une table inet (IPv4 + IPv6 simultanément)
nft add table inet filter

# Créer les chaînes
nft add chain inet filter input   '{ type filter hook input priority 0; policy drop; }'
nft add chain inet filter forward '{ type filter hook forward priority 0; policy drop; }'
nft add chain inet filter output  '{ type filter hook output priority 0; policy accept; }'

# Loopback
nft add rule inet filter input iifname lo accept

# Connexions établies
nft add rule inet filter input ct state established,related accept

# SSH
nft add rule inet filter input tcp dport 22 ct state new limit rate 3/minute accept

# HTTP / HTTPS
nft add rule inet filter input tcp dport { 80, 443 } accept

# ICMP
nft add rule inet filter input icmp type echo-request limit rate 5/second accept
nft add rule inet filter input icmpv6 type echo-request limit rate 5/second accept

# Log + drop final
nft add rule inet filter input limit rate 5/minute log prefix "nft-drop: " level warn
nft add rule inet filter input drop

Fichier de configuration nftables#

# /etc/nftables.conf
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;

        iifname lo accept
        ct state invalid drop
        ct state { established, related } accept

        ip protocol icmp icmp type echo-request limit rate 5/second accept
        ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 5/second accept

        tcp dport 22 ct state new limit rate 3/minute accept
        tcp dport { 80, 443 } accept

        log prefix "nft-INPUT-DROP: " level warn limit rate 1/minute
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}
# Appliquer et activer
nft -f /etc/nftables.conf
systemctl enable --now nftables

ufw — frontend simplifié#

ufw (Uncomplicated Firewall) est un frontend iptables/nftables conçu pour simplifier la configuration courante. Il est installé par défaut sur Ubuntu.

# Activer / désactiver
ufw enable
ufw disable
ufw status verbose

# Politique par défaut
ufw default deny incoming
ufw default allow outgoing

# Autoriser des services
ufw allow ssh            # par nom (consulte /etc/services)
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow from 10.0.0.0/8 to any port 5432    # PostgreSQL depuis le LAN

# Refuser explicitement
ufw deny 23/tcp          # Telnet

# Limiter (anti-brute-force SSH)
ufw limit ssh

# Supprimer une règle
ufw delete allow 80/tcp

# Profils d'applications (dans /etc/ufw/applications.d/)
ufw app list
ufw allow "Nginx Full"
ufw app info "OpenSSH"

# Journaux
ufw logging on
journalctl -k | grep UFW

ufw et IPv6

ufw gère IPv4 et IPv6 simultanément. Vérifiez que IPV6=yes est défini dans /etc/default/ufw. Les règles s’appliquent automatiquement aux deux familles d’adresses sauf indication contraire.

firewalld — zones et services#

firewalld est le gestionnaire de pare-feu dynamique utilisé sur RHEL, Fedora et CentOS. Il utilise des zones (ensembles de règles prédéfinies selon le niveau de confiance du réseau) et gère les modifications à chaud sans interrompre les connexions existantes.

# État
firewall-cmd --state
firewall-cmd --list-all

# Zones disponibles
firewall-cmd --get-zones
# public trusted home work dmz external block drop

# Zone par défaut
firewall-cmd --get-default-zone

# Assigner une interface à une zone
firewall-cmd --zone=dmz --change-interface=ens3 --permanent

# Ajouter des services (permanent + reload)
firewall-cmd --zone=public --add-service=https --permanent
firewall-cmd --zone=public --add-service=ssh --permanent
firewall-cmd --reload

# Ajouter un port
firewall-cmd --zone=public --add-port=8080/tcp --permanent

# Retirer un service
firewall-cmd --zone=public --remove-service=telnet --permanent

# Règles riches (rich rules)
firewall-cmd --zone=public --add-rich-rule='
  rule family="ipv4" source address="192.168.1.0/24"
  service name="ssh" accept' --permanent

# Blocage d'une IP
firewall-cmd --zone=public --add-rich-rule='
  rule family="ipv4" source address="203.0.113.42" drop' --permanent

# Lister les règles riches
firewall-cmd --zone=public --list-rich-rules

NAT et masquerading#

ip_forward — activation du routage#

Pour qu’un serveur Linux route des paquets entre interfaces, le forwarding IP doit être activé :

# Activation temporaire
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv6.conf.all.forwarding=1

# Activation permanente
cat >> /etc/sysctl.d/99-forwarding.conf << EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sysctl --system

MASQUERADE — partage de connexion#

# Masquerading : tous les paquets sortant par ens3 auront l'IP de ens3 comme source
iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE

# SNAT statique (IP fixe plus performant que MASQUERADE)
iptables -t nat -A POSTROUTING -o ens3 -j SNAT --to-source 203.0.113.5

# Politique FORWARD permissive pour le LAN
iptables -A FORWARD -i ens4 -o ens3 -j ACCEPT
iptables -A FORWARD -i ens3 -o ens4 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

DNAT — redirection de port (port forwarding)#

# Rediriger le port 80 entrant vers un serveur interne 10.0.0.5:8080
iptables -t nat -A PREROUTING -i ens3 -p tcp --dport 80 \
    -j DNAT --to-destination 10.0.0.5:8080

# Autoriser le FORWARD correspondant
iptables -A FORWARD -p tcp -d 10.0.0.5 --dport 8080 \
    -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT

# Avec nftables
nft add rule ip nat prerouting \
    iifname ens3 tcp dport 80 dnat to 10.0.0.5:8080

fail2ban — protection contre les attaques par force brute#

fail2ban surveille les fichiers de log, détecte les tentatives répétées d’authentification et bloque dynamiquement les IP offensantes via iptables ou nftables.

Architecture#

  • Jail : service surveillé (sshd, nginx, postfix…)

  • Filtre : expression régulière qui détecte les échecs dans les logs

  • Action : ce qui est exécuté lors d’un bannissement (iptables, email…)

Configuration#

# /etc/fail2ban/jail.local  (ne pas modifier jail.conf)
[DEFAULT]
bantime  = 3600        ; 1 heure de bannissement
findtime = 600         ; fenêtre de détection : 10 minutes
maxretry = 5           ; 5 échecs → bannissement
backend  = systemd

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 3
bantime  = 86400       ; 24 h pour SSH

[nginx-http-auth]
enabled  = true
port     = http,https
filter   = nginx-http-auth
logpath  = /var/log/nginx/error.log

[nginx-badbots]
enabled  = true
port     = http,https
filter   = nginx-badbots
logpath  = /var/log/nginx/access.log
maxretry = 2
# Gestion du service
systemctl enable --now fail2ban
fail2ban-client status
fail2ban-client status sshd

# Débannir une IP
fail2ban-client set sshd unbanip 203.0.113.42

# Tester un filtre contre un log
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf

# Voir les IP bannies (via iptables)
iptables -L f2b-sshd -n --line-numbers

fail2ban et nftables

Depuis fail2ban 0.11, le backend nftables est disponible. Dans [DEFAULT], définir banaction = nftables-multiport et banaction_allports = nftables-allports. Les règles sont alors gérées dans une table nftables dédiée, plus performante qu’iptables sur les grandes listes d’IP.


Démonstrations Python#

Diagramme de flux Netfilter#

sns.set_theme(style="white", palette="muted", font_scale=1.0)

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")

def nœud(ax, x, y, w, h, txt, color="#4e79a7", fontsize=9, bold=False):
    r = plt.Rectangle((x - w/2, y - h/2), w, h,
                       facecolor=color + "33", edgecolor=color, linewidth=2, zorder=3)
    ax.add_patch(r)
    ax.text(x, y, txt, ha="center", va="center",
            fontsize=fontsize, fontweight="bold" if bold else "normal", zorder=4)

def lien(ax, x1, y1, x2, y2, label="", color="#888888"):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.5), zorder=2)
    if label:
        ax.text((x1+x2)/2 + 0.1, (y1+y2)/2 + 0.15, label,
                fontsize=7.5, color=color, ha="left")

# Réseau entrant
nœud(ax, 1.2, 7.5, 1.8, 0.7, "Réseau\n(entrée)", "#888888", bold=True)

# PRE_ROUTING
nœud(ax, 3.5, 7.5, 2.2, 0.8, "PRE_ROUTING\n[raw, mangle, nat]", "#f28e2b", bold=True)
lien(ax, 2.1, 7.5, 2.4, 7.5)

# Décision de routage
nœud(ax, 6.0, 7.5, 1.8, 0.8, "Décision\nde routage", "#76b7b2", bold=True)
lien(ax, 4.6, 7.5, 5.1, 7.5)

# LOCAL_IN
nœud(ax, 9.0, 6.2, 2.2, 0.8, "LOCAL_IN\n[mangle, filter]", "#59a14f", bold=True)
lien(ax, 6.9, 7.2, 8.0, 6.5, "dest = local")

# Process local
nœud(ax, 11.5, 6.2, 2.0, 0.7, "Processus\nlocal", "#4e79a7", bold=True)
lien(ax, 10.1, 6.2, 10.5, 6.2)

# FORWARD
nœud(ax, 9.0, 4.5, 2.2, 0.8, "FORWARD\n[mangle, filter]", "#e15759", bold=True)
lien(ax, 6.9, 7.2, 8.0, 4.8, "transit")

# LOCAL_OUT
nœud(ax, 9.0, 2.8, 2.2, 0.8, "LOCAL_OUT\n[raw, mangle, nat, filter]", "#59a14f", bold=True)
lien(ax, 11.5, 5.85, 10.5, 3.1, "généré\nlocalement")

# Décision routage sortie
nœud(ax, 6.5, 2.8, 1.8, 0.8, "Décision\nde routage", "#76b7b2", bold=True)
lien(ax, 8.1, 2.8, 7.4, 2.8)
lien(ax, 10.1, 4.5, 8.5, 3.1, color="#aaaaaa")

# POST_ROUTING
nœud(ax, 4.0, 2.8, 2.2, 0.8, "POST_ROUTING\n[mangle, nat]", "#f28e2b", bold=True)
lien(ax, 5.6, 2.8, 5.1, 2.8)

# Réseau sortant
nœud(ax, 1.8, 2.8, 1.8, 0.7, "Réseau\n(sortie)", "#888888", bold=True)
lien(ax, 2.9, 2.8, 2.7, 2.8)

# Légende tables
tables = [("raw", "#aaaaaa"), ("mangle", "#76b7b2"), ("nat", "#f28e2b"), ("filter", "#e15759")]
for i, (name, color) in enumerate(tables):
    ax.add_patch(plt.Rectangle((0.3 + i*3.1, 0.3), 0.4, 0.35,
                                facecolor=color + "55", edgecolor=color, linewidth=1.5))
    ax.text(0.85 + i*3.1, 0.48, f"table {name}", fontsize=8.5, va="center")

ax.set_title("Flux de paquets dans Netfilter — hooks et tables",
             fontsize=13, fontweight="bold", pad=14)
plt.show()
_images/81beeb59394d7288bdd7508fd5d40524953cda61557af70fd019e3467ded84ed.png

Analyse d’un ruleset iptables simulé#

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Simuler un ruleset iptables typique
regles = pd.DataFrame([
    {"Chaîne": "INPUT",   "Proto": "tcp",  "Port": 22,   "Source": "0.0.0.0/0",     "Cible": "ACCEPT", "Paquets": 12_453},
    {"Chaîne": "INPUT",   "Proto": "tcp",  "Port": 80,   "Source": "0.0.0.0/0",     "Cible": "ACCEPT", "Paquets": 234_876},
    {"Chaîne": "INPUT",   "Proto": "tcp",  "Port": 443,  "Source": "0.0.0.0/0",     "Cible": "ACCEPT", "Paquets": 891_234},
    {"Chaîne": "INPUT",   "Proto": "tcp",  "Port": 5432, "Source": "10.0.0.0/8",    "Cible": "ACCEPT", "Paquets": 45_670},
    {"Chaîne": "INPUT",   "Proto": "icmp", "Port": 0,    "Source": "0.0.0.0/0",     "Cible": "ACCEPT", "Paquets": 8_901},
    {"Chaîne": "INPUT",   "Proto": "all",  "Port": 0,    "Source": "0.0.0.0/0",     "Cible": "DROP",   "Paquets": 56_789},
    {"Chaîne": "FORWARD", "Proto": "all",  "Port": 0,    "Source": "0.0.0.0/0",     "Cible": "DROP",   "Paquets": 1_234},
    {"Chaîne": "OUTPUT",  "Proto": "tcp",  "Port": 443,  "Source": "0.0.0.0/0",     "Cible": "ACCEPT", "Paquets": 543_210},
    {"Chaîne": "OUTPUT",  "Proto": "udp",  "Port": 53,   "Source": "0.0.0.0/0",     "Cible": "ACCEPT", "Paquets": 23_456},
    {"Chaîne": "OUTPUT",  "Proto": "all",  "Port": 0,    "Source": "0.0.0.0/0",     "Cible": "ACCEPT", "Paquets": 89_012},
])

color_cible = {"ACCEPT": "#59a14f", "DROP": "#e15759", "REJECT": "#f28e2b", "LOG": "#4e79a7"}

fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# Tableau des règles
ax = axes[0]
ax.axis("off")
affiche = regles[["Chaîne", "Proto", "Port", "Source", "Cible"]].copy()
affiche["Port"] = affiche["Port"].apply(lambda x: "—" if x == 0 else str(x))
tbl = ax.table(
    cellText=affiche.values,
    colLabels=affiche.columns,
    loc="center", cellLoc="center"
)
tbl.auto_set_font_size(False)
tbl.set_fontsize(9)
tbl.scale(1.15, 1.75)
for i, (_, row) in enumerate(regles.iterrows()):
    c = color_cible.get(row["Cible"], "#aaaaaa") + "44"
    for j in range(5):
        tbl[(i+1, j)].set_facecolor(c)
for j in range(5):
    tbl[(0, j)].set_facecolor("#333333")
    tbl[(0, j)].set_text_props(color="white", fontweight="bold")
ax.set_title("Ruleset iptables (table filter)", fontweight="bold", pad=12)

# Volume de trafic par règle
ax2 = axes[1]
labels = []
for _, r in regles.iterrows():
    p = f"/{r['Port']}" if r["Port"] != 0 else ""
    labels.append(f"{r['Chaîne']} {r['Proto']}{p}\n{r['Cible']}")
colors = [color_cible.get(r["Cible"], "#aaaaaa") for _, r in regles.iterrows()]
x = np.arange(len(labels))
bars = ax2.bar(x, regles["Paquets"] / 1000, color=colors, edgecolor="white", linewidth=0.5)
ax2.set_xticks(x)
ax2.set_xticklabels(labels, rotation=45, ha="right", fontsize=7.5)
ax2.set_ylabel("Paquets traités (×1000)")
ax2.set_title("Volume par règle", fontweight="bold")
for bar, val in zip(bars, regles["Paquets"]):
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5,
             f"{val//1000}k", ha="center", va="bottom", fontsize=7.5)

patches = [mpatches.Patch(color=v, label=k) for k, v in color_cible.items()]
axes[1].legend(handles=patches, loc="upper right", fontsize=8)
plt.suptitle("Analyse du ruleset iptables", fontsize=13, fontweight="bold")
plt.show()
_images/68b31861080f59ba3b08ba0ede1dbf073dcf8fa774b1eee200578a6c3c4141aa.png

Simulation de logs fail2ban#

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

import random

random.seed(42)

# Simuler des IPs bloquées avec pays d'origine simulé
pays_dist = [
    ("CN", 38), ("RU", 22), ("US", 12), ("BR", 8),
    ("KR", 6), ("DE", 5), ("FR", 4), ("IN", 3), ("NL", 2),
]

ip_bans = []
for pays, count in pays_dist:
    for _ in range(count):
        ip = f"{random.randint(1,254)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}"
        ip_bans.append({"ip": ip, "pays": pays,
                        "tentatives": random.randint(5, 50),
                        "service": random.choice(["sshd", "sshd", "nginx-http-auth", "sshd", "postfix"])})

df_bans = pd.DataFrame(ip_bans)

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# Barplot pays
ax = axes[0]
pays_counts = df_bans["pays"].value_counts()
palette = sns.color_palette("muted", len(pays_counts))
bars = ax.bar(pays_counts.index, pays_counts.values, color=palette, edgecolor="white")
ax.set_xlabel("Pays d'origine")
ax.set_ylabel("IP bannies")
ax.set_title("IP bloquées par pays", fontweight="bold")
for bar, val in zip(bars, pays_counts.values):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
            str(val), ha="center", fontsize=8.5)

# Distribution des tentatives
ax2 = axes[1]
ax2.hist(df_bans["tentatives"], bins=15, color="#4e79a7", edgecolor="white")
ax2.set_xlabel("Nombre de tentatives")
ax2.set_ylabel("Fréquence")
ax2.set_title("Distribution des tentatives", fontweight="bold")
ax2.axvline(df_bans["tentatives"].mean(), color="#e15759", linestyle="--", linewidth=1.5,
            label=f"Moy. {df_bans['tentatives'].mean():.1f}")
ax2.legend()

# Services ciblés
ax3 = axes[2]
svc_counts = df_bans["service"].value_counts()
colors_svc = ["#e15759", "#f28e2b", "#59a14f"]
wedges, texts, autotexts = ax3.pie(
    svc_counts.values, labels=svc_counts.index,
    autopct="%1.0f%%", colors=colors_svc,
    startangle=90, pctdistance=0.75
)
for at in autotexts:
    at.set_fontweight("bold")
ax3.set_title("Services ciblés", fontweight="bold")

plt.suptitle(f"fail2ban — analyse de {len(df_bans)} IP bannies",
             fontsize=13, fontweight="bold")
plt.show()

print(f"\nTop 5 IP les plus actives :")
print(df_bans.nlargest(5, "tentatives")[["ip","pays","tentatives","service"]].to_string(index=False))
_images/7fe49e357502588e879215046c9dc635cf6ad12dc40f1f6928ef127102eabfef.png
Top 5 IP les plus actives :
             ip pays  tentatives         service
 80.209.167.104   KR          49 nginx-http-auth
  42.189.181.54   CN          47 nginx-http-auth
194.102.159.103   CN          47 nginx-http-auth
 134.161.133.53   RU          47 nginx-http-auth
  62.135.202.34   RU          47 nginx-http-auth

Comparaison iptables / nftables / ufw#

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)

criteres = [
    "Syntaxe",
    "IPv4 + IPv6\nunifiés",
    "Performance\nhaut débit",
    "Modification\n à chaud",
    "Facilité\nd'utilisation",
    "Support\ndistributions",
    "Logging\nnatif",
    "Sets /\nDictionnaires",
]

scores = {
    "iptables": [3, 1, 3, 2, 3, 5, 4, 2],
    "nftables": [4, 5, 5, 5, 3, 4, 4, 5],
    "ufw":      [5, 4, 3, 3, 5, 4, 3, 2],
    "firewalld":[4, 4, 4, 5, 4, 4, 3, 3],
}
colors = {"iptables": "#e15759", "nftables": "#4e79a7", "ufw": "#59a14f", "firewalld": "#f28e2b"}

fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# Heatmap des scores
ax = axes[0]
df_scores = pd.DataFrame(scores, index=criteres)
im = ax.imshow(df_scores.T.values, cmap="YlGn", aspect="auto", vmin=1, vmax=5)
ax.set_yticks(range(len(scores)))
ax.set_yticklabels(list(scores.keys()), fontsize=10)
ax.set_xticks(range(len(criteres)))
ax.set_xticklabels(criteres, fontsize=8, rotation=30, ha="right")
for i, outil in enumerate(scores.keys()):
    for j, val in enumerate(scores[outil]):
        ax.text(j, i, str(val), ha="center", va="center",
                fontsize=11, fontweight="bold",
                color="white" if val >= 4 else "#333333")
plt.colorbar(im, ax=ax, label="Score (1-5)")
ax.set_title("Comparaison outils pare-feu", fontweight="bold")

# Radar-like barplot par outil
ax2 = axes[1]
x = np.arange(len(criteres))
width = 0.2
offsets = [-1.5, -0.5, 0.5, 1.5]
for i, (outil, vals) in enumerate(scores.items()):
    ax2.bar(x + offsets[i]*width, vals, width,
            label=outil, color=colors[outil], edgecolor="white", linewidth=0.5)
ax2.set_xticks(x)
ax2.set_xticklabels(criteres, rotation=30, ha="right", fontsize=8)
ax2.set_ylabel("Score (1=faible, 5=excellent)")
ax2.set_title("Scores par critère", fontweight="bold")
ax2.legend(loc="upper right", fontsize=9)
ax2.set_ylim(0, 6.5)

plt.suptitle("Outils pare-feu Linux — analyse comparative", fontsize=13, fontweight="bold")
plt.show()
_images/ae843a703fa59c2ac6ef2cb38cafa2f3eb94446b00f20d393fa201788429eeab.png

Résumé#

Le filtrage réseau sous Linux s’organise autour de Netfilter, framework noyau universel sur lequel s’appuient tous les outils de haut niveau.

Points essentiels à retenir :

  • Netfilter expose cinq hooks dans la pile IP. Toute règle de filtrage s’y accroche via une priorité définie à la création de la chaîne.

  • iptables reste largement déployé mais est en fin de vie active ; nftables est son successeur, plus performant et plus expressif (tables inet unifiant IPv4/IPv6).

  • ufw est adapté aux configurations simples sur serveurs Ubuntu ; firewalld est l’outil natif RHEL/Fedora avec gestion de zones.

  • Le suivi de connexion (conntrack) est la fonctionnalité la plus importante pour un pare-feu stateful : accepter ESTABLISHED,RELATED en début de chaîne INPUT réduit massivement la complexité des règles.

  • Le NAT (MASQUERADE, DNAT) permet le partage de connexion et le port forwarding sans matériel dédié.

  • fail2ban complète le pare-feu statique par une protection dynamique contre les attaques par force brute, en lisant les logs applicatifs.

  • La politique par défaut DROP sur INPUT et FORWARD est la pratique de sécurité de référence : on autorise explicitement ce qui est permis, tout le reste est bloqué.