Sécurité réseau#

La sécurité réseau est l’ensemble des pratiques, technologies et politiques destinées à protéger l’infrastructure de communication contre les accès non autorisés, les modifications malveillantes et les interruptions de service. Dans ce chapitre, nous examinons les menaces les plus courantes couche par couche, puis les contre-mesures que les ingénieurs déploient pour y faire face.

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
import numpy as np
import pandas as pd
import seaborn as sns

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

Modèle de menaces : CIA et surface d’attaque#

La triade CIA#

Le cadre fondamental de la sécurité de l’information repose sur trois propriétés, connues sous l’acronyme CIA :

  • Confidentialité (Confidentiality) : seules les entités autorisées peuvent lire les données.

  • Intégrité (Integrity) : les données ne peuvent pas être modifiées à l’insu des parties légitimes.

  • Disponibilité (Availability) : les ressources restent accessibles quand les utilisateurs en ont besoin.

Une attaque peut cibler l’une ou plusieurs de ces propriétés simultanément. Par exemple, une attaque DDoS vise la disponibilité ; l’écoute passive (sniffing) vise la confidentialité ; une attaque Man-in-the-Middle peut cibler les trois.

fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title("La triade CIA — piliers de la sécurité de l'information", fontsize=14, fontweight='bold', pad=20)

triangle = plt.Polygon([[5, 8.5], [1.5, 2.5], [8.5, 2.5]], closed=True,
                        fill=True, facecolor='#e8f4f8', edgecolor='#2c7bb6', linewidth=2.5)
ax.add_patch(triangle)

labels = [
    (5, 9.2, "Confidentialité", "#d73027", "Seules les parties\nautorisées lisent\nles données"),
    (0.8, 1.8, "Intégrité", "#1a9850", "Les données ne\nsont pas altérées\nà l'insu des parties"),
    (9.2, 1.8, "Disponibilité", "#4575b4", "Les ressources\nrestent accessibles\naux utilisateurs"),
]
for x, y, titre, couleur, desc in labels:
    ax.text(x, y, titre, ha='center', va='center', fontsize=13, fontweight='bold', color=couleur)
    ax.text(x, y - 0.9, desc, ha='center', va='center', fontsize=8.5, color='#333333')

exemples = [
    (3.2, 5.8, "Chiffrement\nTLS", "#d73027"),
    (6.8, 5.8, "Signatures\nnumériques", "#1a9850"),
    (5, 3.2, "Redondance\nDDoS mitigation", "#4575b4"),
]
for x, y, texte, couleur in exemples:
    ax.text(x, y, texte, ha='center', va='center', fontsize=8, color=couleur,
            style='italic', bbox=dict(boxstyle='round,pad=0.3', facecolor='white', edgecolor=couleur, alpha=0.8))

plt.tight_layout()
plt.savefig('_static/cia_triad.png', dpi=100, bbox_inches='tight')
plt.show()
_images/42c0487da096a106125d43ea6e43c7afa47f553fae1a3cf6e6b08667ef1d93f4.png

Surface d’attaque et vecteurs#

La surface d’attaque désigne l’ensemble des points d’entrée qu’un attaquant peut exploiter. Elle comprend :

  • Les interfaces réseau : ports ouverts, protocoles exposés (HTTP, SSH, RDP…).

  • Les protocoles réseau eux-mêmes : ARP, DNS, BGP ont été conçus sans authentification.

  • Les logiciels : serveurs web, bibliothèques, firmware des équipements.

  • Les utilisateurs : phishing, ingénierie sociale.

Principe du moindre privilège

Réduire la surface d’attaque passe avant tout par le principe du moindre privilège : n’exposer que les services strictement nécessaires, fermer les ports inutilisés, et limiter les permissions des processus.


Attaques sur la couche liaison#

ARP poisoning / spoofing#

Le protocole ARP (Address Resolution Protocol) fait correspondre une adresse IP à une adresse MAC sur un réseau local. Comme ARP ne prévoit aucune authentification, n’importe quelle machine peut envoyer une réponse ARP gratuite (gratuitous ARP) pour empoisonner le cache ARP des autres hôtes.

Mécanisme :

  1. L’attaquant envoie des réponses ARP non sollicitées à la victime A : « L’IP de B correspond à ma MAC. »

  2. Il envoie également des réponses à B : « L’IP de A correspond à ma MAC. »

  3. Tout le trafic entre A et B transite désormais par l’attaquant (MitM).

# Simulation pédagogique d'un cache ARP et de son empoisonnement
import struct
import time

class CacheARP:
    """Cache ARP simplifié — associe IP → MAC avec horodatage."""

    def __init__(self):
        self._table: dict[str, tuple[str, float]] = {}

    def apprendre(self, ip: str, mac: str):
        self._table[ip] = (mac, time.time())

    def resoudre(self, ip: str) -> str | None:
        entree = self._table.get(ip)
        return entree[0] if entree else None

    def afficher(self):
        print(f"{'IP':<18} {'MAC':<20} {'Âge (s)':<10}")
        print("-" * 50)
        now = time.time()
        for ip, (mac, ts) in self._table.items():
            print(f"{ip:<18} {mac:<20} {now - ts:.2f}")

# État initial — réseau légitime
cache_a = CacheARP()
cache_a.apprendre("192.168.1.1", "aa:bb:cc:dd:ee:01")  # Routeur légitime
cache_a.apprendre("192.168.1.2", "aa:bb:cc:dd:ee:02")  # Hôte B légitime

print("=== Cache ARP de A (état légitime) ===")
cache_a.afficher()

# Empoisonnement : l'attaquant (MAC ff:ff:ff:ff:ff:aa) se fait passer pour le routeur et pour B
print("\n=== Après ARP poisoning par l'attaquant ===")
cache_a.apprendre("192.168.1.1", "ff:ff:ff:ff:ff:aa")  # Le routeur pointe vers l'attaquant
cache_a.apprendre("192.168.1.2", "ff:ff:ff:ff:ff:aa")  # B pointe vers l'attaquant
cache_a.afficher()

print("\n⚠  Tout le trafic de A vers le routeur et vers B transite maintenant par l'attaquant.")
=== Cache ARP de A (état légitime) ===
IP                 MAC                  Âge (s)   
--------------------------------------------------
192.168.1.1        aa:bb:cc:dd:ee:01    0.00
192.168.1.2        aa:bb:cc:dd:ee:02    0.00

=== Après ARP poisoning par l'attaquant ===
IP                 MAC                  Âge (s)   
--------------------------------------------------
192.168.1.1        ff:ff:ff:ff:ff:aa    0.00
192.168.1.2        ff:ff:ff:ff:ff:aa    0.00

⚠  Tout le trafic de A vers le routeur et vers B transite maintenant par l'attaquant.
# Construction manuelle d'une trame ARP gratuite avec struct (pédagogique)
def forge_arp_gratuit(mac_src: str, ip_src: str) -> bytes:
    """
    Forge une réponse ARP gratuite (non envoyée — uniquement illustratif).
    Format : Ethernet header + ARP payload.
    """
    def mac_bytes(mac: str) -> bytes:
        return bytes(int(x, 16) for x in mac.split(':'))

    def ip_bytes(ip: str) -> bytes:
        return bytes(int(x) for x in ip.split('.'))

    mac_src_b = mac_bytes(mac_src)
    mac_bcast  = b'\xff\xff\xff\xff\xff\xff'
    ip_src_b   = ip_bytes(ip_src)

    # En-tête Ethernet : dst(6) src(6) type(2)
    eth = mac_bcast + mac_src_b + b'\x08\x06'

    # Payload ARP (RFC 826)
    # htype=1 (Ethernet), ptype=0x0800 (IPv4), hlen=6, plen=4
    # oper=2 (reply), sha, spa, tha, tpa
    arp = struct.pack('!HHBBH',
                      1,       # htype : Ethernet
                      0x0800,  # ptype : IPv4
                      6,       # hlen
                      4,       # plen
                      2)       # oper : reply
    arp += mac_src_b + ip_src_b   # SHA + SPA (expéditeur)
    arp += mac_bcast + ip_src_b   # THA + TPA (cible = broadcast pour gratuitous)

    trame = eth + arp
    return trame

trame = forge_arp_gratuit("de:ad:be:ef:00:01", "192.168.1.1")
print(f"Trame ARP gratuite forgée : {len(trame)} octets")
print(f"Hexdump : {trame.hex(' ')}")
print(f"\nEn-tête Ethernet (14 octets) : {trame[:14].hex(' ')}")
print(f"Payload ARP (28 octets)      : {trame[14:].hex(' ')}")
Trame ARP gratuite forgée : 42 octets
Hexdump : ff ff ff ff ff ff de ad be ef 00 01 08 06 00 01 08 00 06 04 00 02 de ad be ef 00 01 c0 a8 01 01 ff ff ff ff ff ff c0 a8 01 01

En-tête Ethernet (14 octets) : ff ff ff ff ff ff de ad be ef 00 01 08 06
Payload ARP (28 octets)      : 00 01 08 00 06 04 00 02 de ad be ef 00 01 c0 a8 01 01 ff ff ff ff ff ff c0 a8 01 01

Contre-mesures ARP

  • Dynamic ARP Inspection (DAI) sur les commutateurs managés : valide les mappages ARP contre une table DHCP snooping.

  • ARP statique pour les équipements critiques (routeurs, serveurs).

  • Détection : surveiller les changements fréquents de MAC pour une même IP (journaux du commutateur).

MAC flooding#

Un commutateur maintient une table CAM (Content Addressable Memory) associant port ↔ MAC. Si un attaquant inonde le commutateur avec des milliers de fausses MAC, la table sature et le commutateur se comporte comme un concentrateur (hub), diffusant tout le trafic sur tous les ports.

Contre-mesure : Port Security — limiter le nombre de MAC autorisées par port.


Attaques sur la couche réseau#

IP spoofing#

L’en-tête IP ne contient pas de mécanisme d’authentification de la source. Un attaquant peut donc forger l’adresse IP source de ses paquets.

Usages malveillants :

  • Masquer l’origine réelle lors d’une attaque DDoS.

  • Exploiter des protocoles qui font confiance à l’adresse source (rsh, anciennes imprimantes).

  • Amplification DNS/NTP (les réponses sont envoyées à la victime usurpée).

Contre-mesure : BCP 38 / uRPF (Unicast Reverse Path Forwarding) — les routeurs vérifient que le paquet entrant provient bien d’une interface par laquelle ils pourraient atteindre la source déclarée.

ICMP redirect#

Un routeur peut légitimement envoyer un message ICMP Redirect pour indiquer à un hôte qu’il existe un meilleur chemin. Un attaquant sur le même segment peut exploiter ce mécanisme pour rediriger le trafic vers lui-même.

Contre-mesure : désactiver l’acceptation des ICMP Redirect sur les hôtes Linux (sysctl -w net.ipv4.conf.all.accept_redirects=0).

BGP hijacking#

BGP (Border Gateway Protocol) est le protocole de routage inter-domaines d’Internet. Il repose sur la confiance mutuelle entre opérateurs (AS — Autonomous Systems). Un AS malveillant (ou compromis) peut annoncer des préfixes IP qu’il ne possède pas légitimement, détournant ainsi le trafic mondial.

Exemples historiques

  • 2010 : China Telecom (AS23724) a annoncé ~50 000 préfixes étrangers pendant 18 minutes, capturant du trafic destiné à des services US, européens et asiatiques.

  • 2018 : Détournement BGP visant des serveurs DNS Amazon Route53 pour dérober des cryptomonnaies.

  • 2022 : Multiples incidents liés à des erreurs de configuration (route leaks) chez des opérateurs européens.

Contre-mesure : RPKI (Resource Public Key Infrastructure) — lie cryptographiquement chaque préfixe IP à son AS légitimes via des Route Origin Authorizations (ROA).

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

# --- Schéma BGP hijacking ---
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis('off')
ax.set_title("BGP Hijacking — mécanisme", fontsize=12, fontweight='bold')

as_legit  = dict(xy=(2, 6), label="AS légitime\n(10.0.0.0/8)", color='#2ca02c')
as_victime = dict(xy=(8, 6), label="Victime\n(destinataire)", color='#1f77b4')
as_attaq  = dict(xy=(2, 2), label="AS attaquant\n(annonce 10.0.0.0/8)", color='#d62728')
as_isp    = dict(xy=(5, 4), label="ISP / IX\n(reçoit 2 annonces)", color='#ff7f0e')

for nd in [as_legit, as_victime, as_attaq, as_isp]:
    x, y = nd['xy']
    ax.add_patch(mpatches.FancyBboxPatch((x-1.2, y-0.5), 2.4, 1.1,
                 boxstyle="round,pad=0.1", facecolor=nd['color'], alpha=0.2,
                 edgecolor=nd['color'], linewidth=2))
    ax.text(x, y + 0.05, nd['label'], ha='center', va='center', fontsize=8.5, fontweight='bold')

flèches = [
    (as_legit['xy'],  as_isp['xy'],  "Annonce légitime", '#2ca02c', '--'),
    (as_attaq['xy'],  as_isp['xy'],  "Annonce forgée", '#d62728', '-'),
    (as_isp['xy'],    as_victime['xy'], "Trafic dévié →\nvers attaquant", '#d62728', '-'),
]
for (x1,y1),(x2,y2),label,col,ls in flèches:
    ax.annotate("", xy=(x2,y2), xytext=(x1,y1),
                arrowprops=dict(arrowstyle='->', color=col, lw=2, linestyle=ls))
    mx, my = (x1+x2)/2, (y1+y2)/2
    ax.text(mx+0.1, my+0.15, label, fontsize=7.5, color=col, ha='center')

# --- Évolution du déploiement RPKI ---
ax2 = axes[1]
années = [2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
couverture = [5, 10, 18, 28, 40, 52, 62, 70]
ax2.bar(années, couverture, color='#4575b4', alpha=0.75, edgecolor='white')
ax2.plot(années, couverture, 'o-', color='#d73027', linewidth=2, markersize=6)
ax2.set_xlabel("Année", fontsize=11)
ax2.set_ylabel("% préfixes IPv4 couverts par RPKI", fontsize=11)
ax2.set_title("Adoption de RPKI dans le monde", fontsize=12, fontweight='bold')
ax2.set_ylim(0, 100)
for x, y in zip(années, couverture):
    ax2.text(x, y+1.5, f"{y}%", ha='center', fontsize=8.5, color='#333333')

plt.tight_layout()
plt.savefig('_static/bgp_hijacking.png', dpi=100, bbox_inches='tight')
plt.show()
_images/2d10d1734d64e1653953ef6ad4a258e4b1cb6543e693bdaa224862cc953ce57c.png

Man-in-the-Middle (MitM)#

Un attaquant positionné entre deux parties peut lire, modifier et réinjecter des paquets à la volée. La technique repose souvent sur une combinaison d’ARP poisoning (pour se placer sur le chemin) et d’interception applicative.

SSL stripping#

Le SSL stripping (Moxie Marlinspike, 2009) consiste à dégrader une connexion HTTPS en HTTP :

  1. L’utilisateur tape http://banque.fr (ou clique un lien non-HTTPS).

  2. L’attaquant MitM intercepte la requête, établit une connexion HTTPS avec le serveur, mais sert de l’HTTP à la victime.

  3. La victime croit naviguer en clair ; l’attaquant voit tout.

HSTS — contre-mesure#

HTTP Strict Transport Security (RFC 6797) force le navigateur à n’utiliser HTTPS que pour un domaine donné, pendant une durée maximale. Après une première visite HTTPS, le navigateur refuse tout accès HTTP à ce domaine.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

La liste HSTS Preload va plus loin : les navigateurs embarquent en dur une liste de domaines qui sont toujours HTTPS, même pour la toute première visite.

fig, ax = plt.subplots(figsize=(12, 5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.axis('off')
ax.set_title("Mécanisme Man-in-the-Middle avec SSL stripping", fontsize=13, fontweight='bold', pad=15)

entités = [
    (1.2, 3, "Victime\n(navigateur)", '#4575b4'),
    (6,   3, "Attaquant\n(MitM)", '#d62728'),
    (10.8, 3, "Serveur\n(HTTPS)", '#1a9850'),
]
for x, y, label, col in entités:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.9, y-0.6), 1.8, 1.2,
                 boxstyle="round,pad=0.1", facecolor=col, alpha=0.15,
                 edgecolor=col, linewidth=2))
    ax.text(x, y, label, ha='center', va='center', fontsize=10, fontweight='bold', color=col)

étapes = [
    (1.2, 6, "① GET http://banque.fr", 6, '#4575b4', '→'),
    (6,   6, "② GET https://banque.fr", 10.8, '#d62728', '→'),
    (10.8, 6, "③ 200 OK (HTTPS chiffré)", 6, '#1a9850', '←'),
    (6,   6, "④ 200 OK (HTTP en clair!)", 1.2, '#d62728', '←'),
]
y_pos = [5.2, 4.6, 4.0, 3.4]
for i, ((x1,_,texte,x2,col,_), y) in enumerate(zip(étapes, y_pos)):
    x_from = x1
    x_to   = x2
    ax.annotate("", xy=(x_to, y), xytext=(x_from, y),
                arrowprops=dict(arrowstyle='->', color=col, lw=1.8))
    ax.text((x_from+x_to)/2, y+0.18, texte, ha='center', fontsize=8.5, color=col)

ax.text(6, 1.5,
        "L'attaquant voit les données en clair\n(identifiants, cookies…)\nalors que le serveur croit communiquer légitimement.",
        ha='center', va='center', fontsize=9, color='#d62728',
        bbox=dict(boxstyle='round,pad=0.4', facecolor='#fff0f0', edgecolor='#d62728'))

plt.tight_layout()
plt.savefig('_static/mitm_ssl_strip.png', dpi=100, bbox_inches='tight')
plt.show()
_images/427bbbdb7f6b49fe0fcf11a74286c3d3a6c0d109df7189fa776d304683ebb02d.png

DDoS : attaques par déni de service distribué#

Une attaque DDoS (Distributed Denial of Service) vise à rendre un service indisponible en le submergeant de trafic illégitime provenant de milliers de sources (botnet).

Amplification DNS/NTP#

Ces attaques exploitent des protocoles UDP qui retournent des réponses beaucoup plus volumineuses que les requêtes :

Protocole

Facteur d’amplification typique

DNS (ANY)

×50 à ×100

NTP (monlist)

×200 à ×700

Memcached

×10 000 à ×51 000

SSDP

×30

Le principe : l’attaquant envoie de petites requêtes UDP avec l’IP source usurpée (celle de la victime). Les serveurs réflecteurs envoient des réponses volumineuses directement à la victime.

SYN flood#

Exploite le three-way handshake TCP : l’attaquant envoie des millions de SYN avec des IP sources forgées. Le serveur alloue des ressources pour chaque connexion semi-ouverte, épuisant sa mémoire.

Contre-mesure : SYN cookies — le serveur encode l’état de la connexion dans le numéro de séquence et n’alloue des ressources qu’après réception du ACK final.

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

# Amplification
ax1 = axes[0]
protocoles = ['DNS\n(ANY)', 'NTP\n(monlist)', 'SSDP', 'Memcached']
facteurs = [75, 450, 30, 30000]
couleurs_amp = ['#4575b4', '#d73027', '#fee090', '#d62728']

bars = ax1.barh(protocoles, facteurs, color=couleurs_amp, edgecolor='white', height=0.5)
ax1.set_xscale('log')
ax1.set_xlabel("Facteur d'amplification (échelle log)", fontsize=11)
ax1.set_title("Amplification DDoS par protocole", fontsize=12, fontweight='bold')
for bar, val in zip(bars, facteurs):
    ax1.text(val * 1.1, bar.get_y() + bar.get_height()/2,
             f{val:,}", va='center', fontsize=9, color='#333333')

# SYN flood timeline
ax2 = axes[1]
t = np.linspace(0, 20, 500)
trafic_normal = 100 + 10 * np.sin(2 * np.pi * t / 10) + np.random.normal(0, 5, 500)
trafic_ddos = trafic_normal.copy()
trafic_ddos[200:] = trafic_normal[200:] + 1800 * np.exp(-((t[200:] - 12) ** 2) / 4)

ax2.fill_between(t, trafic_normal, alpha=0.5, color='#4575b4', label='Trafic légitime')
ax2.fill_between(t, trafic_ddos, trafic_normal, alpha=0.6, color='#d62728', label='SYN flood')
ax2.axvline(x=8, color='#d62728', linestyle='--', linewidth=1.5)
ax2.text(8.2, 1600, "Début\nattaque", fontsize=8.5, color='#d62728')
ax2.set_xlabel("Temps (secondes)", fontsize=11)
ax2.set_ylabel("Paquets/seconde", fontsize=11)
ax2.set_title("Impact d'un SYN flood sur le trafic", fontsize=12, fontweight='bold')
ax2.legend(fontsize=10)

plt.tight_layout()
plt.savefig('_static/ddos_viz.png', dpi=100, bbox_inches='tight')
plt.show()
_images/c5883ed181891637cce7066b41bd03360c68ef16ed13e791b47b5ad9eee73903.png

Scanning de ports#

Le scanning de ports est une technique de reconnaissance permettant de déterminer quels services sont exposés sur une machine. C’est à la fois un outil légitime d’audit et une phase préparatoire à une attaque.

Méthodes principales#

Méthode

Description

Avantage

TCP Connect

connect() complet

Fonctionne sans privilèges

TCP SYN (half-open)

Envoie SYN, analyse SYN-ACK ou RST

Moins visible dans les logs

UDP scan

Envoie UDP vide, analyse ICMP port unreachable

Lent, peu fiable

ACK scan

Détecte les firewalls stateful

Cartographie des règles

FIN/NULL/Xmas

Exploite les ambiguïtés RFC

Contourne certains firewalls

Nmap — exemples de commandes#

# Scan TCP SYN rapide des 1000 ports les plus courants
nmap -sS 192.168.1.0/24

# Détection de version et OS
nmap -sV -O 192.168.1.10

# Script NSE pour vulnérabilités SMB
nmap --script smb-vuln-ms17-010 192.168.1.10

# Scan UDP des ports NTP, DNS, SNMP
nmap -sU -p 53,123,161 192.168.1.10

# Scan agressif avec traceroute
nmap -A --traceroute 192.168.1.10

Remarque légale

Le scanning de ports sans autorisation explicite est illégal dans de nombreux pays et peut constituer une tentative d’intrusion. N’utilisez Nmap que sur vos propres systèmes ou avec une autorisation écrite.

import socket

def scan_tcp_connect(hôte: str, ports: list[int], timeout: float = 0.5) -> dict[int, str]:
    """
    Scan TCP Connect sur l'hôte local (127.0.0.1 uniquement pour ce notebook).
    Retourne {port: état} avec état = 'ouvert' ou 'fermé/filtré'.
    """
    résultats = {}
    for port in ports:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            ret = sock.connect_ex((hôte, port))
            résultats[port] = 'ouvert' if ret == 0 else 'fermé/filtré'
        except OSError:
            résultats[port] = 'erreur'
        finally:
            sock.close()
    return résultats

# Scan de quelques ports courants sur localhost
ports_courants = [22, 80, 443, 3000, 5000, 8080, 8888]
résultats = scan_tcp_connect('127.0.0.1', ports_courants)

print(f"{'Port':<8} {'Service':<15} {'État'}")
print("-" * 35)
services = {22: 'SSH', 80: 'HTTP', 443: 'HTTPS', 3000: 'Dev', 5000: 'Dev', 8080: 'HTTP-alt', 8888: 'Jupyter'}
for port, état in résultats.items():
    icône = "✔" if état == 'ouvert' else "✘"
    print(f"{port:<8} {services.get(port,'?'):<15} {icône} {état}")
Port     Service         État
-----------------------------------
22       SSH             ✘ fermé/filtré
80       HTTP            ✔ ouvert
443      HTTPS           ✘ fermé/filtré
3000     Dev             ✘ fermé/filtré
5000     Dev             ✘ fermé/filtré
8080     HTTP-alt        ✘ fermé/filtré
8888     Jupyter         ✘ fermé/filtré
# Détection de scan par comptage de connexions par IP source (IDS simplifié)
from collections import defaultdict
import random

class DétecteurDeScan:
    """Détecte un comportement de scan de ports par comptage de connexions."""

    def __init__(self, seuil_ports: int = 10, fenêtre_s: int = 5):
        self.seuil_ports = seuil_ports
        self.fenêtre_s   = fenêtre_s
        self._connexions: dict[str, list] = defaultdict(list)
        self._alertes: list[str] = []

    def enregistrer(self, ip_src: str, port_dst: int, timestamp: float):
        now = timestamp
        # Nettoyage des entrées hors fenêtre
        self._connexions[ip_src] = [
            (ts, p) for ts, p in self._connexions[ip_src]
            if now - ts <= self.fenêtre_s
        ]
        self._connexions[ip_src].append((now, port_dst))
        ports_uniques = {p for _, p in self._connexions[ip_src]}
        if len(ports_uniques) >= self.seuil_ports:
            alerte = (f"ALERTE SCAN : {ip_src}{len(ports_uniques)} ports "
                      f"distincts en {self.fenêtre_s}s")
            if alerte not in self._alertes:
                self._alertes.append(alerte)
                print(alerte)

# Simulation : trafic légitime + scan de port
détecteur = DétecteurDeScan(seuil_ports=8, fenêtre_s=3)

# Trafic légitime (accès répétés aux mêmes ports)
t = 0.0
for _ in range(20):
    détecteur.enregistrer("10.0.0.1", random.choice([80, 443]), t)
    t += 0.2

# Attaquant scannant de nombreux ports rapidement
ip_attaquant = "10.0.0.99"
for port in range(20, 35):
    détecteur.enregistrer(ip_attaquant, port, t)
    t += 0.05

if not détecteur._alertes:
    print("Aucune alerte générée.")
ALERTE SCAN : 10.0.0.99 → 8 ports distincts en 3s
ALERTE SCAN : 10.0.0.99 → 9 ports distincts en 3s
ALERTE SCAN : 10.0.0.99 → 10 ports distincts en 3s
ALERTE SCAN : 10.0.0.99 → 11 ports distincts en 3s
ALERTE SCAN : 10.0.0.99 → 12 ports distincts en 3s
ALERTE SCAN : 10.0.0.99 → 13 ports distincts en 3s
ALERTE SCAN : 10.0.0.99 → 14 ports distincts en 3s
ALERTE SCAN : 10.0.0.99 → 15 ports distincts en 3s

Firewalls#

Un firewall filtre le trafic réseau selon des règles prédéfinies. Il peut opérer à différents niveaux de la pile OSI.

Stateless vs Stateful#

Critère

Stateless

Stateful

Suivi des connexions

Non

Oui

Performance

Très élevée

Élevée

Sécurité

Basique

Bonne

Exemple

ACL routeur

iptables, nftables

Un firewall stateful maintient une table des connexions établies (connection tracking), ce qui lui permet d’autoriser automatiquement les paquets de retour d’une connexion initiée depuis l’intérieur.

iptables / nftables#

# iptables — politique par défaut DROP, autoriser établi + SSH + HTTP
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Autoriser les connexions établies et associées
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Autoriser SSH (port 22) depuis un réseau de confiance
iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT

# Autoriser HTTP et HTTPS
iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

# Journaliser et bloquer le reste
iptables -A INPUT -j LOG --log-prefix "iptables-DROP: "
iptables -A INPUT -j DROP
# nftables — équivalent moderne
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input ip saddr 192.168.1.0/24 tcp dport 22 accept
nft add rule inet filter input tcp dport { 80, 443 } accept
nft add rule inet filter input log prefix "nft-drop: " drop

Topologie DMZ#

Une DMZ (DeMilitarized Zone) isole les serveurs exposés à Internet des réseaux internes. Le firewall externe protège la DMZ d’Internet, et le firewall interne protège le réseau interne de la DMZ.

fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.axis('off')
ax.set_title("Architecture réseau avec DMZ — deux firewalls", fontsize=13, fontweight='bold', pad=15)

# Zones
zones = [
    (0, 0, 2.5, 7,   "Internet", '#fee0d2', '#d62728'),
    (2.5, 0, 4.5, 7, "DMZ", '#fff7bc', '#d6a500'),
    (4.5, 0, 7, 7,   "Zone\nInterne", '#e5f5e0', '#1a9850'),
]
for x1, y1, x2, y2, label, fc, ec in zones:
    ax.add_patch(mpatches.FancyBboxPatch((x1+0.05, y1+0.1), x2-x1-0.1, y2-y1-0.2,
                 boxstyle="round,pad=0.1", facecolor=fc, edgecolor=ec, linewidth=2, alpha=0.5))
    ax.text((x1+x2)/2, 6.5, label, ha='center', va='center', fontsize=11,
            fontweight='bold', color=ec)

# Firewalls
for x, label in [(2.5, "FW\nextérieur"), (4.5, "FW\nintérieur")]:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.3, 2.8), 0.6, 1.4,
                 boxstyle="round,pad=0.05", facecolor='#525252', edgecolor='#252525', linewidth=1.5))
    ax.text(x, 3.5, label, ha='center', va='center', fontsize=8, color='white', fontweight='bold')

# Entités
entités_dmz = [
    (3.5, 5.2, "Serveur\nWeb"),
    (3.5, 3.5, "Serveur\nMail"),
    (3.5, 1.8, "Reverse\nProxy"),
]
for x, y, label in entités_dmz:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.6, y-0.4), 1.2, 0.8,
                 boxstyle="round,pad=0.05", facecolor='#fdae61', edgecolor='#d6a500', linewidth=1.5))
    ax.text(x, y, label, ha='center', va='center', fontsize=8)

entités_int = [
    (5.8, 5.2, "Base de\ndonnées"),
    (5.8, 3.5, "Serveur\nApplicatif"),
    (5.8, 1.8, "Postes\nclients"),
]
for x, y, label in entités_int:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.6, y-0.4), 1.2, 0.8,
                 boxstyle="round,pad=0.05", facecolor='#a1d99b', edgecolor='#1a9850', linewidth=1.5))
    ax.text(x, y, label, ha='center', va='center', fontsize=8)

# Internet
ax.add_patch(plt.Circle((1.2, 3.5), 0.6, facecolor='#fc8d59', edgecolor='#d62728', linewidth=2))
ax.text(1.2, 3.5, "🌐", ha='center', va='center', fontsize=18)

# Flèches
ax.annotate("", xy=(2.2, 3.5), xytext=(1.8, 3.5),
            arrowprops=dict(arrowstyle='->', color='#d62728', lw=2))
ax.annotate("", xy=(4.2, 3.5), xytext=(2.8, 3.5),
            arrowprops=dict(arrowstyle='->', color='#1a9850', lw=2))

plt.tight_layout()
plt.savefig('_static/dmz_architecture.png', dpi=100, bbox_inches='tight')
plt.show()
/tmp/ipykernel_11421/1530862344.py:56: UserWarning: Glyph 127760 (\N{GLOBE WITH MERIDIANS}) missing from font(s) DejaVu Sans.
  plt.tight_layout()
/tmp/ipykernel_11421/1530862344.py:57: UserWarning: Glyph 127760 (\N{GLOBE WITH MERIDIANS}) missing from font(s) DejaVu Sans.
  plt.savefig('_static/dmz_architecture.png', dpi=100, bbox_inches='tight')
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 127760 (\N{GLOBE WITH MERIDIANS}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
_images/9f87ba08ff85e33712245995f7d61925f1c4940100c979f0fd5703e47cacbbc3.png

VPN#

Un VPN (Virtual Private Network) crée un tunnel chiffré entre deux points, permettant de sécuriser des communications sur un réseau non fiable (Internet).

WireGuard#

WireGuard est un VPN moderne (noyau Linux depuis 5.6), conçu pour être simple, rapide et sécurisé. Il utilise une cryptographie moderne :

  • ChaCha20-Poly1305 : chiffrement authentifié (AEAD).

  • Curve25519 : échange de clés Diffie-Hellman sur courbe elliptique.

  • BLAKE2s : fonction de hachage.

  • SipHash : protection des tables de hachage internes.

# Génération d'une paire de clés WireGuard
wg genkey | tee privatekey | wg pubkey > publickey

# Configuration serveur (/etc/wireguard/wg0.conf)
[Interface]
PrivateKey = <clé_privée_serveur>
Address    = 10.0.0.1/24
ListenPort = 51820

[Peer]
PublicKey  = <clé_publique_client>
AllowedIPs = 10.0.0.2/32

# Configuration client
[Interface]
PrivateKey = <clé_privée_client>
Address    = 10.0.0.2/24

[Peer]
PublicKey  = <clé_publique_serveur>
Endpoint   = vpn.exemple.fr:51820
AllowedIPs = 0.0.0.0/0   # Tout le trafic passe par le VPN
PersistentKeepalive = 25

IPsec#

IPsec opère en couche réseau (couche 3) et peut fonctionner en deux modes :

  • Mode transport : chiffre uniquement la charge utile IP (entre deux hôtes).

  • Mode tunnel : encapsule l’intégralité du paquet IP (entre deux réseaux, via des passerelles).

Il utilise deux protocoles :

  • AH (Authentication Header) : intégrité et authentification, pas de chiffrement.

  • ESP (Encapsulating Security Payload) : intégrité + chiffrement.

Comparaison#

fig, ax = plt.subplots(figsize=(10, 4))
ax.axis('off')

colonnes = ['Protocole', 'Couche OSI', 'Cryptographie', 'Facilité', 'Performance', 'Usage typique']
données = [
    ['WireGuard', '3 (réseau)', 'ChaCha20/Poly1305\nCurve25519', '★★★★★', '★★★★★', 'VPN moderne, cloud'],
    ['OpenVPN', '3-4', 'TLS (OpenSSL)', '★★★★', '★★★', 'Entreprise, clients légers'],
    ['IPsec/IKEv2', '3 (réseau)', 'AES-GCM, SHA-2', '★★★', '★★★★', 'VPN site-à-site, mobiles'],
    ['L2TP/IPsec', '2-3', 'IPsec pour chiffrement', '★★★', '★★★', 'Héritage, Windows natif'],
]

table = ax.table(cellText=données, colLabels=colonnes,
                 cellLoc='center', loc='center',
                 colWidths=[0.15, 0.12, 0.22, 0.12, 0.14, 0.22])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 2.2)

for (row, col), cell in table.get_celld().items():
    if row == 0:
        cell.set_facecolor('#2c7bb6')
        cell.set_text_props(color='white', fontweight='bold')
    elif row % 2 == 0:
        cell.set_facecolor('#f0f8ff')
    cell.set_edgecolor('#cccccc')

ax.set_title("Comparaison des protocoles VPN", fontsize=13, fontweight='bold', pad=20)
plt.tight_layout()
plt.savefig('_static/vpn_comparison.png', dpi=100, bbox_inches='tight')
plt.show()
_images/95ae42ec5ff5803c95b528af446b05dcc2fcdbf6c7331569c9b1d1200db913b0.png

IDS / IPS#

Un IDS (Intrusion Detection System) surveille le réseau et génère des alertes. Un IPS (Intrusion Prevention System) peut également bloquer le trafic suspect en temps réel.

Snort et Suricata#

Snort (Cisco) et Suricata (OISF) sont les deux IDS/IPS open source les plus utilisés. Suricata supporte nativement le multi-threading et l’inspection TLS.

Exemple de règle Snort/Suricata :

# Détection de scan Nmap (paquet TCP avec flags SYN)
alert tcp any any -> $HOME_NET any (
    msg:"Possible scan de port SYN";
    flags:S;
    threshold: type both, track by_src, count 20, seconds 3;
    sid:1000001; rev:1;
)

# Détection d'une tentative d'exploitation EternalBlue (MS17-010)
alert smb $EXTERNAL_NET any -> $HOME_NET 445 (
    msg:"ET EXPLOIT EternalBlue SMB Remote Code Execution";
    flow:established,to_server;
    content:"|00 00 00 90 ff|SMB";
    depth:9; offset:4;
    sid:2025869; rev:1;
)

Kill chain réseau#

Le modèle Cyber Kill Chain (Lockheed Martin) décrit les phases d’une attaque réseau :

fig, ax = plt.subplots(figsize=(13, 4))
ax.set_xlim(0, 13)
ax.set_ylim(0, 4)
ax.axis('off')
ax.set_title("Cyber Kill Chain — phases d'une attaque réseau ciblée", fontsize=13, fontweight='bold', pad=10)

phases = [
    ("1. Reconnaissance", "Nmap, OSINT\nWHOIS, Shodan"),
    ("2. Armement", "Exploit kit\nPayload forgé"),
    ("3. Livraison", "Phishing\nExploit web"),
    ("4. Exploitation", "RCE, 0-day\nPrivilège"),
    ("5. Installation", "Backdoor\nRootkit, C2"),
    ("6. C2", "Beacon HTTPS\nDNS tunneling"),
    ("7. Objectif", "Exfiltration\nRansomware"),
]
couleurs_kc = ['#4575b4', '#74add1', '#abd9e9', '#fee090', '#fdae61', '#f46d43', '#d73027']

for i, ((titre, desc), col) in enumerate(zip(phases, couleurs_kc)):
    x = 0.8 + i * 1.75
    ax.add_patch(mpatches.FancyBboxPatch((x-0.75, 0.8), 1.5, 2.2,
                 boxstyle="round,pad=0.1", facecolor=col, alpha=0.8,
                 edgecolor='white', linewidth=2))
    ax.text(x, 2.35, titre, ha='center', va='center', fontsize=7.5,
            fontweight='bold', color='white', wrap=True)
    ax.text(x, 1.5, desc, ha='center', va='center', fontsize=7,
            color='white', style='italic')
    if i < len(phases) - 1:
        ax.annotate("", xy=(x+0.85, 1.9), xytext=(x+0.75, 1.9),
                    arrowprops=dict(arrowstyle='->', color='#333333', lw=2))

plt.tight_layout()
plt.savefig('_static/kill_chain.png', dpi=100, bbox_inches='tight')
plt.show()
_images/929e5ca1fe67a30ccd8fc0a64e10e0fe6ffdf69535c0d080a329e7641858966b.png

Résumé#

fig, ax = plt.subplots(figsize=(11, 6))
ax.axis('off')

données_résumé = [
    ['ARP poisoning', 'Liaison (2)', 'Confidentialité\nIntégrité', 'DAI, ARP statique'],
    ['MAC flooding', 'Liaison (2)', 'Disponibilité\nConfidentialité', 'Port Security'],
    ['IP spoofing', 'Réseau (3)', 'Confidentialité', 'BCP38, uRPF'],
    ['BGP hijacking', 'Réseau (3)', 'Disponibilité\nIntégrité', 'RPKI, ROA'],
    ['MitM / SSL strip', 'Transport (4)', 'Confidentialité\nIntégrité', 'HSTS, cert pinning'],
    ['DDoS amplifié', 'Réseau (3-4)', 'Disponibilité', 'Scrubbing center'],
    ['SYN flood', 'Transport (4)', 'Disponibilité', 'SYN cookies'],
    ['Scan de ports', 'Transport (4)', 'Reconnaissance', 'Firewall, IDS/IPS'],
]
cols_résumé = ['Attaque', 'Couche OSI', 'CIA menacé', 'Contre-mesure principale']

table = ax.table(cellText=données_résumé, colLabels=cols_résumé,
                 cellLoc='center', loc='center',
                 colWidths=[0.22, 0.18, 0.25, 0.30])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 2.0)

for (row, col), cell in table.get_celld().items():
    if row == 0:
        cell.set_facecolor('#2c7bb6')
        cell.set_text_props(color='white', fontweight='bold')
    elif row % 2 == 0:
        cell.set_facecolor('#f5f5f5')
    cell.set_edgecolor('#dddddd')

ax.set_title("Synthèse des attaques réseau et contre-mesures", fontsize=13, fontweight='bold', pad=20)
plt.tight_layout()
plt.savefig('_static/synthese_attaques.png', dpi=100, bbox_inches='tight')
plt.show()
_images/255090171a950279f9dabc51f9f1e31b499ef07b21f4f6bf026905ae72b7c12e.png

Points clés du chapitre

  • La sécurité réseau repose sur la triade CIA : Confidentialité, Intégrité, Disponibilité.

  • Les protocoles réseau historiques (ARP, BGP, DNS) ont été conçus sans authentification et doivent être sécurisés par des mécanismes additionnels (DAI, RPKI, DNSSEC).

  • Le MitM reste efficace lorsque TLS n’est pas imposé ; HSTS et le certificate pinning en sont les principales contre-mesures.

  • Les DDoS par amplification exploitent des protocoles UDP réflecteurs ; BCP38 et les scrubbing centers en limitent l’impact.

  • WireGuard représente l’état de l’art pour les VPN : cryptographie moderne, faible surface d’attaque, performances élevées.

  • Les IDS/IPS comme Suricata complètent les firewalls en détectant les comportements anormaux au niveau applicatif.