Pare-feu et filtrage#
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 |
|---|---|
|
À la réception, avant la décision de routage |
|
Paquet destiné à la machine locale |
|
Paquet à router vers une autre interface |
|
Paquet généré localement, avant émission |
|
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 |
|---|---|---|
|
Accepter / rejeter des paquets |
INPUT, FORWARD, OUTPUT |
|
Translation d’adresses |
PREROUTING, OUTPUT, POSTROUTING |
|
Modifier les en-têtes IP (TTL, TOS, marques) |
Tous |
|
Court-circuiter le suivi de connexion |
PREROUTING, OUTPUT |
|
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 |
|---|---|
|
|
Commandes |
|
|
|
|
Adresse/réseau source |
|
Adresse/réseau destination |
|
Port destination (avec |
|
Port source |
|
Interface entrante |
|
Interface sortante |
Cibles (targets)#
Cible |
Effet |
|---|---|
|
Laisser passer le paquet |
|
Silencieusement rejeter |
|
Rejeter avec réponse ICMP |
|
Journaliser sans décision finale |
|
SNAT dynamique (IP source = IP interface) |
|
Modifier l’IP destination |
|
Modifier l’IP source |
|
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()
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()
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))
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()
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
inetunifiant 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 : accepterESTABLISHED,RELATEDen 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é.