07 — Pare-feu avancé et segmentation réseau#

La sécurité réseau repose sur la capacité à contrôler précisément quels flux sont autorisés entre quels composants. Ce chapitre explore les modèles de filtrage modernes, la segmentation en profondeur (DMZ, VLAN, micro-segmentation), les Web Application Firewalls et les techniques de détection passive comme les honeypots.

Prérequis

Ce chapitre approfondit linux/10_parefeu.md. Familiarité avec nftables, les couches OSI (L3–L7), les Network Policies Kubernetes et les protocoles HTTP/TLS requise.


Modèles de filtrage#

Filtrage stateless (ACL)#

Le filtrage sans état évalue chaque paquet indépendamment, sans mémoire des échanges précédents. Chaque règle examine uniquement les champs de l’en-tête IP/TCP/UDP (src_ip, dst_ip, src_port, dst_port, proto).

  • Avantage : extrêmement rapide, linéaire, implémentable en matériel (ASIC).

  • Limite : incapable de distinguer un paquet de réponse légitime d’un paquet entrant malveillant avec les mêmes en-têtes.

Utilisé principalement sur les routeurs de cœur de réseau et les ACL de switches L3.

Filtrage stateful (conntrack)#

Le pare-feu avec état maintient une table de connexions (conntrack) qui suit l’état de chaque flux TCP/UDP/ICMP :

État conntrack

Description

NEW

Premier paquet d’une nouvelle connexion

ESTABLISHED

Connexion établie (réponse légitime attendue)

RELATED

Connexion annexe liée à une connexion établie (FTP, ICMP error)

INVALID

Paquet ne correspondant à aucun flux connu

Grâce à conntrack, une règle ct state established,related accept autorise automatiquement les réponses sans règle explicite en retour.

Filtrage applicatif L7 (NGFW)#

Les pare-feu Next Generation (NGFW) inspectent le contenu applicatif : identification du protocole au-delà du port (DPI), contrôle par application, déchiffrement TLS pour inspection du contenu HTTPS.

Comparaison#

Caractéristique

Stateless

Stateful

L7 / NGFW

Couche OSI

L3–L4

L3–L4

L7

Performance

Très haute

Haute

Moyenne–élevée

Contexte de connexion

Non

Oui

Oui + contenu

Détection applicative

Non

Non

Oui

Déchiffrement TLS

Non

Non

Optionnel


nftables — filtrage moderne sous Linux#

nftables remplace iptables depuis Linux 3.13 et est l’outil de référence pour le filtrage sous Debian/Ubuntu/RHEL modernes.

Architecture : tables, familles, chaînes, règles#

famille : inet (ipv4+ipv6), ip, ip6, arp, bridge
    └── table : nom libre (ex. firewall)
            ├── chaîne input    (trafic à destination de la machine)
            ├── chaîne output   (trafic sortant de la machine)
            ├── chaîne forward  (trafic routé à travers la machine)
            └── chaîne prerouting / postrouting (NAT)

Configuration nftables de référence#

# /etc/nftables.conf

table inet firewall {

    # Sets nommés (listes d'IPs autorisées)
    set admin_ips {
        type ipv4_addr
        elements = { 10.0.0.5, 192.168.1.100 }
    }

    chain input {
        type filter hook input priority 0; policy drop;

        # Trafic loopback autorisé
        iif lo accept

        # Connexions établies / liées
        ct state established,related accept

        # ICMP limité
        ip protocol icmp icmp type { echo-request, echo-reply } limit rate 10/second accept
        meta l4proto ipv6-icmp accept

        # SSH uniquement depuis admin_ips
        tcp dport 22 ip saddr @admin_ips ct state new accept

        # HTTPS public
        tcp dport { 80, 443 } ct state new accept

        # Journaliser les refus avant drop
        limit rate 5/second log prefix "[nft-DROP] " flags all
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        # Politique zero-trust : tout refusé sauf règles explicites
    }

    chain output {
        type filter hook output priority 0; policy accept;
        # Sortie autorisée par défaut (restriction possible par service)
    }
}

# Activation
# nft -f /etc/nftables.conf
# systemctl enable --now nftables

Sets, maps et compteurs#

# Set dynamique (liste noire mise à jour à chaud)
nft add set inet firewall blacklist { type ipv4_addr \; flags dynamic,timeout \; timeout 1h \; }

# Ajouter une IP à la liste noire
nft add element inet firewall blacklist { 1.2.3.4 }

# Règle de blocage référençant le set
nft insert rule inet firewall input ip saddr @blacklist drop

# Counter nommé pour statistiques
nft add counter inet firewall ssh_attempts
nft add rule inet firewall input tcp dport 22 counter name ssh_attempts
nft list counter inet firewall ssh_attempts

nftables vs iptables

nftables offre une syntaxe cohérente, des sets/maps natifs (pas besoin d’ipset), des compteurs nommés et une meilleure performance. Migrer avec iptables-translate pour les règles existantes.


Segmentation réseau#

DMZ (Zone Démilitarisée)#

La DMZ isole les serveurs exposés à internet (web, mail, DNS) du réseau interne. Deux pare-feu distincts (ou un pare-feu avec trois interfaces) contrôlent les flux :

Internet ─── [Pare-feu externe] ─── DMZ ─── [Pare-feu interne] ─── LAN interne
                                     │
                              Serveurs web, reverse proxy, bastion

Règle fondamentale : aucun flux ne passe directement d’Internet vers le LAN interne sans transiter par la DMZ.

VLAN (Virtual LAN)#

Les VLANs (IEEE 802.1Q) segmentent logiquement un réseau physique en plusieurs domaines de broadcast isolés :

# Création VLAN sur un switch Linux (bridge)
ip link add link eth0 name eth0.100 type vlan id 100   # VLAN 100 : serveurs
ip link add link eth0 name eth0.200 type vlan id 200   # VLAN 200 : utilisateurs
ip link set eth0.100 up
ip link set eth0.200 up

Le routage inter-VLAN est contrôlé par le pare-feu/routeur : par défaut, les VLANs sont isolés.

Micro-segmentation et Zero Trust Networking#

La micro-segmentation pousse le principe jusqu’au niveau de la charge de travail individuelle (VM, conteneur, processus). Chaque workload n’est autorisé à communiquer qu’avec les services dont il a besoin, indépendamment de la topologie réseau.

Outils : Network Policies Kubernetes, eBPF/Cilium, VMware NSX, HashiCorp Consul Connect (mTLS entre services).

East-West vs North-South#

Trafic

Direction

Exemple

Risque

North-South

Externe ↔ Interne

Client → API publique

Exposition directe à internet

East-West

Interne ↔ Interne

Service A → Base de données

Propagation latérale (lateral movement)

Le trafic East-West est quantitativement dominant dans les architectures microservices (80–90% du trafic total dans les datacenters). Historiquement sous-protégé, il est le vecteur principal de propagation post-compromission.

Lateral Movement

Un attaquant ayant compromis un service peu exposé peut se déplacer latéralement vers des services critiques si le trafic East-West n’est pas filtré. La micro-segmentation limite ce risque : chaque service ne voit que ses dépendances autorisées.


WAF — Web Application Firewall#

Principes#

Un WAF opère au niveau HTTP (L7) et filtre les requêtes web en fonction de signatures de vulnérabilités applicatives :

  • Injections SQL, XSS, SSRF, XXE, path traversal, deserialization, CSRF…

  • Limite de taille des requêtes, rate limiting par IP/user.

  • Protection contre les scanners et bots automatisés.

ModSecurity avec OWASP CRS#

ModSecurity est le WAF open-source de référence, intégrable dans Nginx, Apache et HAProxy. Les règles OWASP Core Rule Set (CRS) couvrent les OWASP Top 10.

# nginx.conf avec ModSecurity (ngx_http_modsecurity_module)
server {
    listen 443 ssl;
    server_name api.example.com;

    modsecurity on;
    modsecurity_rules_file /etc/modsecurity/modsec_includes.conf;

    location / {
        proxy_pass http://backend:8080;
    }
}
# /etc/modsecurity/crs-setup.conf
SecRuleEngine DetectionOnly          # Mode détection (logs, no block)
# SecRuleEngine On                   # Mode prévention (block)

SecDefaultAction "phase:2,log,auditlog,pass"
SecRequestBodyLimit 13107200         # 12.5 MB max
SecRequestBodyNoFilesLimit 131072    # 128 KB hors fichiers

# Niveau de paranoïa CRS (1=défaut, 2-4=plus strict, plus de faux positifs)
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=1"

Modes : détection vs prévention#

Mode

Comportement

Usage recommandé

DetectionOnly

Log uniquement, ne bloque pas

Phase initiale, calibrage des règles

On (Enforcement)

Bloque et log

Production après calibrage

Le passage en mode prévention sans calibrage préalable génère des faux positifs qui bloquent du trafic légitime (ex. CRS PL2+ sur des API JSON avec payloads atypiques).

Bypasses courants et contre-mesures#

Technique de bypass

Mécanisme

Contre-mesure

Encodage URL double (%2527)

Décodage incomplet

Normalisation des entrées

Fragmentation JSON

WAF ne reconstruit pas

Inspection du body complet

Unicode/UTF-8 non-BMP

Confusion de décodage

Normalisation Unicode

Protocol-level smuggling

HTTP/1.1 vs HTTP/2

Proxy frontend uniforme

Padding/whitespace

Contournement de regex

Règles basées sur la sémantique

WAF ≠ sécurité applicative

Un WAF est une couche de défense en profondeur, pas un substitut à la sécurisation du code. Il n’empêche pas les vulnérabilités logiques (IDOR, broken auth) et peut être contourné par des attaquants déterminés. Il doit compléter, non remplacer, les revues de code et tests SAST/DAST.


Network Policies Kubernetes#

Les Network Policies Kubernetes implémentent la micro-segmentation pour les Pods. Sans Network Policy, tous les Pods peuvent communiquer entre eux (politique ouverte par défaut).

# Politique "default deny" : isoler le namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}        # Sélectionne tous les Pods
  policyTypes:
    - Ingress
    - Egress

---
# Autoriser uniquement api → database sur le port 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api
      ports:
        - protocol: TCP
          port: 5432

CNI avec support Network Policies

Les Network Policies Kubernetes nécessitent un CNI (Container Network Interface) qui les implémente réellement : Calico, Cilium, Weave Net. Le CNI par défaut de kubeadm (flannel) ne les supporte pas.


Honeypots et honeynets#

Principes#

Un honeypot est un système délibérément vulnérable et attractif, conçu pour détecter et étudier les attaquants. Tout accès légitime à un honeypot est inexistant par définition — toute interaction est suspecte.

Types :

  • Low-interaction : simule des services (ports ouverts, bannières), capture les scans et tentatives d’exploitation de base (ex. Cowrie pour SSH, HoneyPy).

  • High-interaction : système réel en sandbox, capture des attaques sophistiquées et des outils d’attaquants.

  • Honeynet : réseau complet de honeypots interconnectés.

Honeytokens#

Des honeytokens sont des credentials ou ressources factices semés dans les systèmes légitimes :

# Créer un AWS API key honeytoken (Canarytokens)
# Si cette clé est utilisée, une alerte est déclenchée
# AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE   (semé dans config.js)
# AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI...    (clé piège, surveillée via CloudTrail)

Légalité des honeypots

En France (RGPD, Code pénal art. 323-x), les honeypots doivent être clairement isolés des systèmes de production, ne pas collecter de données personnelles sans base légale, et leur déploiement doit être documenté. Consulter le RSSI/DPO avant déploiement.


Cellules Python#

Simulation moteur de règles pare-feu#

Hide code cell source

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import seaborn as sns
import ipaddress
import random
# --- Moteur de règles nftables-like ---

RULES = [
    # (src_network, dst_network, dst_port, proto, action, description)
    ("0.0.0.0/0",    "0.0.0.0/0",     None, "any",  "accept", "loopback (simplification)"),
    ("10.0.0.0/8",   "10.0.0.0/8",    22,   "tcp",  "accept", "SSH interne"),
    ("0.0.0.0/0",    "0.0.0.0/0",     80,   "tcp",  "accept", "HTTP public"),
    ("0.0.0.0/0",    "0.0.0.0/0",     443,  "tcp",  "accept", "HTTPS public"),
    ("10.0.1.0/24",  "10.0.2.0/24",   5432, "tcp",  "accept", "API → PostgreSQL"),
    ("10.0.1.0/24",  "10.0.3.0/24",   6379, "tcp",  "accept", "API → Redis"),
    ("192.168.0.0/16","0.0.0.0/0",    None, "any",  "drop",   "Blocage RFC-1918 externe"),
    ("0.0.0.0/0",    "0.0.0.0/0",     None, "any",  "drop",   "Politique défaut : DROP"),
]


def ip_in_network(ip: str, network: str) -> bool:
    try:
        return ipaddress.ip_address(ip) in ipaddress.ip_network(network, strict=False)
    except ValueError:
        return False


def evaluate_rule(rule, flow: dict) -> bool:
    src_net, dst_net, port, proto, action, _ = rule
    if not ip_in_network(flow["src_ip"], src_net):
        return False
    if not ip_in_network(flow["dst_ip"], dst_net):
        return False
    if port is not None and flow["dst_port"] != port:
        return False
    if proto != "any" and flow["proto"] != proto:
        return False
    return True


def evaluate_firewall(flow: dict) -> tuple[str, str]:
    for rule in RULES:
        if evaluate_rule(rule, flow):
            return rule[4], rule[5]
    return "drop", "Politique défaut"


# Générer des flux synthétiques
random.seed(42)
np.random.seed(42)

def rand_ip(network: str) -> str:
    net = ipaddress.ip_network(network, strict=False)
    n = net.num_addresses
    if n <= 2:
        return str(net.network_address)
    offset = random.randint(1, min(n - 2, 254))
    return str(net.network_address + offset)

test_flows = []
scenarios = [
    # (src_network, dst_network, port, proto)
    ("172.16.0.0/24", "10.0.0.50/32",  443, "tcp"),   # Externe → HTTPS
    ("10.0.0.0/24",   "10.0.0.10/32",  22,  "tcp"),   # Interne → SSH
    ("10.0.1.0/24",   "10.0.2.5/32",   5432,"tcp"),   # API → DB
    ("10.0.1.0/24",   "10.0.3.2/32",   6379,"tcp"),   # API → Redis
    ("8.8.8.8/32",    "10.0.0.1/32",   22,  "tcp"),   # Externe → SSH (bloqué)
    ("192.168.1.0/24","8.8.8.8/32",    80,  "tcp"),   # RFC-1918 sortant bloqué
    ("0.0.0.0/8",     "10.0.0.1/32",   9200,"tcp"),   # Port non autorisé
    ("10.0.0.0/24",   "10.0.0.1/32",   80,  "tcp"),   # Interne → HTTP
]

for _ in range(80):
    s = random.choice(scenarios)
    flow = {
        "src_ip":   rand_ip(s[0]),
        "dst_ip":   rand_ip(s[1]),
        "dst_port": s[2],
        "proto":    s[3],
    }
    action, reason = evaluate_firewall(flow)
    flow["action"] = action
    flow["raison"] = reason
    test_flows.append(flow)

df = pd.DataFrame(test_flows)
print("=== Résultats de l'évaluation du pare-feu ===")
print(df.groupby(["action", "raison"]).size().rename("count").to_string())
print(f"\nTotal : {len(df)} flux | Acceptés : {(df.action=='accept').sum()} | Bloqués : {(df.action=='drop').sum()}")
=== Résultats de l'évaluation du pare-feu ===
action  raison                   
accept  loopback (simplification)    80

Total : 80 flux | Acceptés : 80 | Bloqués : 0

Heatmap de trafic inter-zones#

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

# Matrice de trafic : zones × zones
zones = ["Internet", "DMZ", "LAN API", "LAN DB", "LAN Admin", "LAN Redis"]

# Trafic autorisé (1) / bloqué (0) — politique de filtrage
traffic_matrix = np.array([
    # Internet  DMZ  API   DB  Admin  Redis
    [0,         1,   0,    0,  0,     0],    # Internet →
    [1,         0,   1,    0,  0,     0],    # DMZ →
    [0,         1,   0,    1,  0,     1],    # LAN API →
    [0,         0,   1,    0,  0,     0],    # LAN DB →
    [1,         1,   1,    1,  0,     1],    # LAN Admin →
    [0,         0,   1,    0,  0,     0],    # LAN Redis →
])

fig, ax = plt.subplots(figsize=(9, 7))

cmap = matplotlib.colors.ListedColormap(["#d9534f", "#5cb85c"])
norm = matplotlib.colors.BoundaryNorm([0, 0.5, 1], cmap.N)

im = ax.imshow(traffic_matrix, cmap=cmap, norm=norm, aspect="auto")

# Annotations
for i in range(len(zones)):
    for j in range(len(zones)):
        val = traffic_matrix[i, j]
        txt = "Autorisé" if val == 1 else "Bloqué"
        ax.text(j, i, txt, ha="center", va="center",
                fontsize=9, color="white", fontweight="bold")

ax.set_xticks(range(len(zones)))
ax.set_yticks(range(len(zones)))
ax.set_xticklabels(zones, rotation=30, ha="right", fontsize=10)
ax.set_yticklabels(zones, fontsize=10)
ax.set_xlabel("Zone de destination", fontsize=11)
ax.set_ylabel("Zone source", fontsize=11)
ax.set_title("Matrice de filtrage inter-zones\n(politique de segmentation réseau)",
             fontsize=12, fontweight="bold")

legend_handles = [
    mpatches.Patch(color="#5cb85c", label="Autorisé"),
    mpatches.Patch(color="#d9534f", label="Bloqué"),
]
ax.legend(handles=legend_handles, loc="upper right", fontsize=10,
          bbox_to_anchor=(1.25, 1.0))

plt.savefig("traffic_matrix.png", dpi=120, bbox_inches="tight")
plt.show()
_images/cc74f4fbd2f00d814a683f5aedcffa599625567d28ca6c10b40dd036e5f3cfd6.png

Courbe précision/rappel du WAF selon le seuil CRS#

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

# Simulation : sensibilité des règles CRS vs précision/rappel
np.random.seed(0)

# Niveau de paranoïa CRS : 1 (défaut) à 4 (très strict)
# Plus le niveau monte, plus le rappel augmente (on détecte plus d'attaques)
# Mais aussi plus de faux positifs (précision baisse)
paranoia_levels = np.linspace(1, 4, 50)

# Modélisation heuristique des courbes précision/rappel
recall    = 0.30 + 0.55 * (1 - np.exp(-0.7 * (paranoia_levels - 1)))
precision = 0.95 - 0.45 * (1 - np.exp(-0.5 * (paranoia_levels - 1)))

# Ajouter du bruit réaliste
recall    += np.random.normal(0, 0.015, len(paranoia_levels))
precision += np.random.normal(0, 0.012, len(paranoia_levels))
recall    = np.clip(recall, 0, 1)
precision = np.clip(precision, 0, 1)

# Score F1
f1 = 2 * precision * recall / (precision + recall + 1e-9)

# Trouver le seuil optimal F1
best_idx = np.argmax(f1)
best_pl   = paranoia_levels[best_idx]
best_prec = precision[best_idx]
best_rec  = recall[best_idx]
best_f1   = f1[best_idx]

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# --- Courbe Précision/Rappel ---
ax1 = axes[0]
ax1.plot(recall, precision, color="#4878d0", linewidth=2.5, label="Courbe P/R")
ax1.scatter([best_rec], [best_prec], color="crimson", s=120, zorder=5,
            label=f"Optimal (PL≈{best_pl:.1f})\nF1={best_f1:.2f}")
ax1.set_xlabel("Rappel (détection d'attaques réelles)", fontsize=11)
ax1.set_ylabel("Précision (trafic bloqué = réellement malveillant)", fontsize=11)
ax1.set_title("Courbe Précision/Rappel du WAF\nselon le niveau de paranoïa CRS", fontsize=11, fontweight="bold")
ax1.legend(fontsize=9)
ax1.set_xlim(0, 1.05)
ax1.set_ylim(0, 1.05)

# Annotations zones
ax1.axhspan(0.85, 1.05, alpha=0.08, color="green")
ax1.text(0.05, 0.88, "Haute précision\n(peu de faux positifs)", fontsize=8, color="green")
ax1.axvspan(0.75, 1.05, alpha=0.08, color="blue")
ax1.text(0.78, 0.1, "Haute détection\n(peu d'attaques manquées)", fontsize=8, color="#4878d0")

# --- Précision, Rappel et F1 vs niveau de paranoïa ---
ax2 = axes[1]
ax2.plot(paranoia_levels, precision, color="#4878d0", linewidth=2, label="Précision")
ax2.plot(paranoia_levels, recall,    color="#ee854a", linewidth=2, label="Rappel")
ax2.plot(paranoia_levels, f1,        color="#6acc65", linewidth=2, linestyle="--", label="F1")
ax2.axvline(x=best_pl, color="crimson", linestyle=":", linewidth=1.5)
ax2.text(best_pl + 0.05, 0.25, f"PL opt.≈{best_pl:.1f}", color="crimson", fontsize=9)
ax2.set_xlabel("Niveau de paranoïa CRS (1 = défaut, 4 = strict)", fontsize=11)
ax2.set_ylabel("Score", fontsize=11)
ax2.set_title("Précision, Rappel et F1\nselon la sensibilité des règles WAF", fontsize=11, fontweight="bold")
ax2.legend(fontsize=9)
ax2.set_xlim(1, 4)
ax2.set_ylim(0, 1.05)
ax2.xaxis.set_major_locator(matplotlib.ticker.MultipleLocator(0.5))

plt.savefig("waf_precision_recall.png", dpi=120, bbox_inches="tight")
plt.show()

print(f"Niveau de paranoïa optimal : {best_pl:.2f}")
print(f"  Précision : {best_prec:.3f} | Rappel : {best_rec:.3f} | F1 : {best_f1:.3f}")
_images/eb7fe141ec607df31687a47b7ace918ba87ccce5c9b7c9cf20fec0636fad42dc.png
Niveau de paranoïa optimal : 3.20
  Précision : 0.664 | Rappel : 0.751 | F1 : 0.705

Résumé#

  1. Modèles de filtrage : le filtrage stateless (ACL) offre des performances maximales mais ne distingue pas les états de connexion. Le filtrage stateful (conntrack) autorise automatiquement les réponses légitimes. Les NGFW inspectent jusqu’à L7 pour le contrôle applicatif.

  2. nftables : successeur d’iptables, il unifie la syntaxe, introduit les sets/maps natifs et offre de meilleures performances. La politique default drop et l’usage de ct state established,related accept sont les fondations de toute configuration sécurisée.

  3. Segmentation : la DMZ isole les services exposés, les VLANs créent des domaines de broadcast séparés, et la micro-segmentation protège contre le mouvement latéral en traitant chaque workload comme une zone distincte.

  4. East-West vs North-South : le trafic East-West est majoritaire dans les microservices et historiquement sous-protégé. C’est le vecteur principal de propagation post-compromission — la micro-segmentation est la réponse architecturale.

  5. WAF : ModSecurity avec OWASP CRS protège contre les attaques web connues. Le niveau de paranoïa (1–4) contrôle le compromis précision/rappel. Un WAF ne remplace pas la sécurisation applicative.

  6. Network Policies Kubernetes implémentent la micro-segmentation au niveau Pod. Sans politique explicite, tous les Pods d’un cluster communiquent librement — implémenter default-deny-all dans chaque namespace de production est un prérequis de sécurité.

  7. Honeypots : outils de détection passive. Toute interaction est suspecte par définition. Les honeytokens (credentials factices semés dans les systèmes) détectent les compromissions internes avec un taux de faux positifs quasi-nul.