Infrastructure
5 VMs segmentées avec routage inter-VLAN contrôlé
Observabilité
Métriques, logs et traces centralisés avec Prometheus + Loki
Sécurité
IDS/IPS, audit de surface d'attaque, blocage automatique
Incidents
Simulation, détection et réponse structurée aux incidents réseau
Architecture cible
| VM | Rôle | IP | OS recommandé | RAM |
|---|---|---|---|---|
VM-GW | Routeur / Firewall nftables | 192.168.{10,20,30}.1 | Debian 12 | 512 MB |
VM-APP | Serveur applicatif (Nginx) | 192.168.10.10 | Debian 12 | 1 GB |
VM-DB | Base de données (PostgreSQL) | 192.168.10.20 | Debian 12 | 1 GB |
VM-MON | Monitoring (Prometheus+Grafana+Loki) | 192.168.20.10 | Debian 12 | 2 GB |
VM-SEC | Sécurité (Suricata+Fail2ban+Lynis) | 192.168.30.10 | Debian 12 | 1 GB |
Prérequis
- VMware Workstation / VirtualBox avec support des réseaux internes personnalisés
- Au minimum 8 GB de RAM disponible sur la machine hôte
- Image ISO Debian 12 (Bookworm) disponible en local
- Accès internet sur la machine hôte pour télécharger les paquets
- Compréhension des bases du routage IP statique (ip route, ip addr)
- Connaissance minimale de YAML pour les fichiers de configuration
Infrastructure & Segmentation
~2hCréer cinq VMs Debian 12 minimalistes. Lors de l'installation, choisir uniquement « SSH server » et « standard system utilities ».
| Réseau | Sous-réseau | DHCP |
|---|---|---|
| VMnet10 | 192.168.10.0/24 | Désactivé |
| VMnet20 | 192.168.20.0/24 | Désactivé |
| VMnet30 | 192.168.30.0/24 | Désactivé |
Assigner les interfaces réseau à chaque VM :
VM-GW: eth0 (NAT/WAN), eth1 (VMnet10), eth2 (VMnet20), eth3 (VMnet30)VM-APP/VM-DB: une seule interface sur VMnet10VM-MON: une seule interface sur VMnet20VM-SEC: eth0 (VMnet30) + eth1 (VMnet10 en mode promiscuous pour IDS)
# /etc/network/interfaces sur VM-APP
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 192.168.10.10
netmask 255.255.255.0
gateway 192.168.10.1
dns-nameservers 8.8.8.8
# Sur VM-GW
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
sysctl -p
# Vérification
cat /proc/sys/net/ipv4/ip_forward
# Doit retourner : 1
Configurer VM-GW comme routeur central. Chaque VM des segments LAN/DMZ/MGMT doit pouvoir joindre les autres uniquement selon la politique de sécurité définie.
# Sur VM-GW — Vérifier les interfaces actives
ip addr show
ip route show
# Ajouter une route par défaut NAT vers internet
ip route add default via <IP_HOTE_NAT>
# Sur VM-APP — Tester la connectivité vers VM-MON
ping -c 4 192.168.20.10
traceroute 192.168.20.10
- VM-APP peut joindre VM-GW (192.168.10.1) →
ping OK - VM-APP peut joindre VM-MON (via routage GW) →
ping OK - VM-MON ne peut PAS joindre VM-DB directement (réglé au pare-feu étape 1.3)
- VM-GW peut joindre toutes les VMs → accès SSH d'administration
Remplacer le pare-feu basique par une configuration nftables complète avec : stateful inspection, rate limiting, logging différencié par chaîne et protection contre les scans de ports.
# /etc/nftables.conf — Configuration complète VM-GW
flush ruleset
table inet filter {
# Set pour les IPs bannies dynamiquement (alimenté par Fail2ban)
set blocklist {
type ipv4_addr
flags dynamic, timeout
timeout 1h
}
chain input {
type filter hook input priority 0 ; policy drop
# Connexions établies
ct state established,related accept
ct state invalid drop
# Blocklist dynamique
ip saddr @blocklist drop
# Loopback
iif "lo" accept
# ICMP limité
ip protocol icmp icmp type echo-request limit rate 5/second accept
ip protocol icmp drop
# SSH uniquement depuis MGMT
ip saddr 192.168.30.0/24 tcp dport 22 accept
tcp dport 22 log prefix "[FW-SSH-DENY] " drop
# Protection scan de ports (SYN flood)
tcp flags syn tcp option maxseg size 1-500 drop
tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|psh|ack|urg drop
tcp flags & (fin|syn) == fin|syn drop
log prefix "[FW-INPUT-DROP] " drop
}
chain forward {
type filter hook forward priority 0 ; policy drop
ct state established,related accept
ip saddr @blocklist drop
# LAN → DMZ : autoriser metrics (9090, 3000, 3100)
ip saddr 192.168.10.0/24 ip daddr 192.168.20.0/24 \
tcp dport { 9090, 3000, 3100, 9100 } accept
# DMZ → LAN : interdit sauf réponses (géré par ct state)
# MGMT → toutes : SSH uniquement
ip saddr 192.168.30.0/24 tcp dport 22 accept
# LAN → WAN : NAT sortant (HTTP/HTTPS)
ip saddr 192.168.10.0/24 tcp dport { 80, 443 } accept
log prefix "[FW-FWD-DROP] " drop
}
chain output {
type filter hook output priority 0 ; policy accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority 100
ip saddr 192.168.10.0/24 oif "eth0" masquerade
}
}
# Appliquer et activer au démarrage
nft -f /etc/nftables.conf
systemctl enable nftables
nft list ruleset
# Vérifier les logs en temps réel
journalctl -f -k | grep "FW-"
curl http://192.168.20.10:3000 → doit être bloqué et logué. Depuis VM-APP, le même curl doit aboutir.Stack d'Observabilité Full-Stack
~3hDéployer Prometheus sur VM-MON avec une configuration scrape couvrant tous les services. Ajouter les exporteurs spécialisés sur chaque VM cible.
# Node Exporter (métriques système)
wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xzf node_exporter-*.tar.gz && cp node_exporter-*/node_exporter /usr/local/bin/
# Nginx Exporter
apt install nginx -y
wget https://github.com/nginxinc/nginx-prometheus-exporter/releases/download/v1.3.0/nginx-prometheus-exporter_1.3.0_linux_amd64.tar.gz
# Process Exporter (surveiller des processus spécifiques)
wget https://github.com/ncabatoff/process-exporter/releases/download/v0.8.3/process-exporter-0.8.3.linux-amd64.tar.gz
# /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
environment: 'tp-lab'
# Règles d'alerte
rule_files:
- "/etc/prometheus/rules/*.yml"
# Alertmanager
alerting:
alertmanagers:
- static_configs:
- targets: ['localhost:9093']
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node'
static_configs:
- targets:
- '192.168.10.10:9100' # VM-APP
- '192.168.10.20:9100' # VM-DB
- '192.168.20.10:9100' # VM-MON elle-même
- '192.168.30.10:9100' # VM-SEC
relabel_configs:
- source_labels: [__address__]
regex: '(.+):\d+'
target_label: instance
- job_name: 'nginx'
static_configs:
- targets: ['192.168.10.10:9113']
- job_name: 'postgres'
static_configs:
- targets: ['192.168.10.20:9187']
- job_name: 'blackbox-http'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- http://192.168.10.10
- http://192.168.10.10/health
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: localhost:9115
- job_name: 'blackbox-icmp'
metrics_path: /probe
params:
module: [icmp]
static_configs:
- targets: ['192.168.10.10', '192.168.10.20', '192.168.30.10']
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: localhost:9115
# Taux d'erreurs HTTP sur Nginx (sur 5 min)
rate(nginx_http_requests_total{status=~"5.."}[5m]) /
rate(nginx_http_requests_total[5m]) * 100
# Saturation CPU avec alerte si > 80% pendant 5 min
100 - (avg by(instance) (
rate(node_cpu_seconds_total{mode="idle"}[2m])
) * 100) > 80
# Connexions TCP en état CLOSE_WAIT (fuite de connexions)
node_netstat_Tcp_CurrEstab - node_netstat_Tcp_PassiveOpens
# Jitter estimé : écart-type de la latence ICMP
stddev_over_time(probe_duration_seconds{job="blackbox-icmp"}[10m]) * 1000
Loki permet d'agréger les logs de toutes les VMs dans Grafana, corrélables avec les métriques Prometheus en utilisant les mêmes labels (instance, job).
# Sur VM-MON — Installer Loki
wget https://github.com/grafana/loki/releases/download/v3.2.0/loki-linux-amd64.zip
unzip loki-linux-amd64.zip && mv loki-linux-amd64 /usr/local/bin/loki
chmod +x /usr/local/bin/loki
# /etc/loki/loki-config.yml
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /var/loki
storage:
filesystem:
chunks_directory: /var/loki/chunks
rules_directory: /var/loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
allow_structured_metadata: false
# Sur CHAQUE VM cible — Installer Promtail
wget https://github.com/grafana/loki/releases/download/v3.2.0/promtail-linux-amd64.zip
unzip promtail-linux-amd64.zip && mv promtail-linux-amd64 /usr/local/bin/promtail
# /etc/promtail/promtail-config.yml — VM-APP
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://192.168.20.10:3100/loki/api/v1/push
scrape_configs:
- job_name: system
static_configs:
- targets: ['localhost']
labels:
job: syslog
instance: vm-app
__path__: /var/log/syslog
- job_name: nginx
static_configs:
- targets: ['localhost']
labels:
job: nginx
instance: vm-app
__path__: /var/log/nginx/*.log
pipeline_stages:
- regex:
expression: '(?P<ip>\S+) .+ "(?P<method>\w+) (?P<path>\S+).+" (?P<status>\d{3})'
- labels:
method:
status:
- job_name: nftables
static_configs:
- targets: ['localhost']
labels:
job: firewall
instance: vm-gw
__path__: /var/log/kern.log
pipeline_stages:
- match:
selector: '{job="firewall"}'
stages:
- regex:
expression: '\[(?P<fw_chain>FW-[A-Z-]+)\]'
- labels:
fw_chain:
http://localhost:3100), puis explorer les logs avec la requête LogQL : {job="nginx"} |= "status"Créer un dashboard unifié qui affiche simultanément les métriques Prometheus et les logs Loki pour diagnostiquer un incident sans changer d'outil.
| Panel | Type | Requête |
|---|---|---|
| CPU par instance | Time series | 100 - avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[2m])*100) |
| Latence ICMP | Gauge | probe_duration_seconds{job="blackbox-icmp"}*1000 |
| Drops firewall/min | Stat | rate(nftables_dropped_packets_total[1m]) |
| Logs erreurs Nginx | Logs (Loki) | {job="nginx"} |= "error" or `status="5` |
| Connexions TCP actives | Time series | node_netstat_Tcp_CurrEstab |
| Disponibilité services | State timeline | probe_success{job="blackbox-http"} |
Sécurité & Détection d'Intrusion
~2h30Installer Suricata sur VM-SEC avec son interface eth1 en mode promiscuous sur le segment LAN. Suricata analysera tout le trafic du LAN en temps réel et générera des alertes JSON.
# Sur VM-SEC
apt install suricata -y
# Mettre eth1 en mode promiscuous (miroir du trafic LAN)
ip link set eth1 promisc on
echo 'SUBSYSTEM=="net", ACTION=="add", KERNEL=="eth1", RUN+="/sbin/ip link set eth1 promisc on"' > /etc/udev/rules.d/99-promisc.rules
# Télécharger les règles Emerging Threats (gratuites)
suricata-update
suricata-update list-sources
suricata-update enable-source et/open
suricata-update update
# /etc/suricata/suricata.yaml — sections clés à modifier
# Interface à surveiller
af-packet:
- interface: eth1
cluster-id: 99
cluster-type: cluster_flow
defrag: yes
# Format des alertes
outputs:
- eve-log:
enabled: yes
filetype: regular
filename: /var/log/suricata/eve.json
types:
- alert:
payload: yes
payload-buffer-size: 4kb
http-body: yes
- http:
extended: yes
- dns:
- tls:
extended: yes
# Ajouter des règles personnalisées
# /etc/suricata/rules/local.rules
# Détecter un scan de ports (>15 SYN vers ports différents en 2 sec)
alert tcp any any -> 192.168.10.0/24 any (msg:"PORT SCAN DETECTED"; flags:S; threshold: type both, track by_src, count 15, seconds 2; sid:1000001; rev:1;)
# Détecter tentative de connexion SSH brute force
alert tcp any any -> 192.168.10.0/24 22 (msg:"SSH BRUTE FORCE"; flow:to_server; threshold: type both, track by_src, count 5, seconds 30; sid:1000002; rev:1;)
# Détecter exfiltration de données (gros transfert sortant inhabituel)
alert tcp 192.168.10.0/24 any -> !192.168.0.0/16 any (msg:"LARGE DATA EXFILTRATION"; dsize:>65000; sid:1000003; rev:1;)
# Démarrer Suricata et surveiller les alertes en temps réel
systemctl start suricata
tail -f /var/log/suricata/eve.json | python3 -m json.tool | grep -A5 '"event_type":"alert"'
Configurer Fail2ban pour lire le fichier eve.json de Suricata et bannir automatiquement les IPs malveillantes via le set nftables blocklist créé en phase 1.
# Sur VM-GW
apt install fail2ban -y
# /etc/fail2ban/filter.d/suricata.conf
[Definition]
failregex = .*"src_ip":"<HOST>".*"event_type":"alert".*
ignoreregex =
# /etc/fail2ban/jail.d/suricata.conf
[suricata]
enabled = true
filter = suricata
logpath = /var/log/suricata/eve.json
maxretry = 3
findtime = 60
bantime = 3600
action = nftables-multiport[name=suricata, port="0:65535", protocol=tcp]
# Action personnalisée pour utiliser notre blocklist nftables
[suricata-nft]
enabled = true
filter = suricata
logpath = /var/log/suricata/eve.json
maxretry = 3
findtime = 60
bantime = 3600
action = %(action_)s
nft add element inet filter blocklist { <ip> timeout 1h }
# Vérifier le statut des bans
fail2ban-client status
fail2ban-client status suricata
nft list set inet filter blocklist
Lynis effectue un audit de sécurité complet d'un système Linux. Lancer l'audit sur VM-APP et VM-DB, analyser le score et appliquer les corrections prioritaires.
# Sur VM-APP et VM-DB
apt install lynis -y
lynis audit system --quiet 2>&1 | tee /tmp/lynis-report.txt
grep "Hardening index" /tmp/lynis-report.txt
grep "WARNING\|SUGGESTION" /tmp/lynis-report.txt | head -30
# 1. Désactiver les services inutiles
systemctl disable --now avahi-daemon cups bluetooth 2>/dev/null || true
# 2. Sécuriser SSH
cat >> /etc/ssh/sshd_config << 'EOF'
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
ClientAliveInterval 300
AllowUsers votre_user
EOF
# 3. Restreindre les permissions sysctl
cat >> /etc/sysctl.conf << 'EOF'
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.tcp_syncookies = 1
EOF
sysctl -p
# 4. Relancer l'audit pour mesurer l'amélioration
lynis audit system --quiet | grep "Hardening index"
Alerting & Réponse aux Incidents
~2h# /etc/prometheus/rules/network.yml
groups:
- name: network-alerts
rules:
- alert: HostDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Host {{ $labels.instance }} is DOWN"
description: "Prometheus ne peut plus scraper {{ $labels.instance }} depuis 1 min."
- alert: HighLatency
expr: probe_duration_seconds{job="blackbox-icmp"} * 1000 > 100
for: 2m
labels:
severity: warning
annotations:
summary: "Latence élevée vers {{ $labels.instance }}"
description: "Latence ICMP : {{ $value | humanizeDuration }}"
- alert: PacketLoss
expr: probe_success{job="blackbox-icmp"} == 0
for: 30s
labels:
severity: critical
annotations:
summary: "Perte de paquets vers {{ $labels.instance }}"
- alert: FirewallDropSpike
expr: rate(nftables_dropped_packets_total[2m]) > 50
for: 1m
labels:
severity: warning
annotations:
summary: "Spike de drops firewall détecté"
description: "{{ $value }} paquets/s droppés — possible scan ou attaque"
- alert: CPUSaturation
expr: 100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[2m]))*100) > 90
for: 3m
labels:
severity: critical
annotations:
summary: "CPU saturé sur {{ $labels.instance }}"
# /etc/alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 1h
receiver: 'webhook-local'
routes:
- match:
severity: critical
receiver: 'webhook-critical'
receivers:
- name: 'webhook-local'
webhook_configs:
- url: 'http://localhost:5001/alert'
send_resolved: true
- name: 'webhook-critical'
webhook_configs:
- url: 'http://localhost:5001/critical'
send_resolved: true
# /opt/alert-receiver/server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import json, datetime
class AlertHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers['Content-Length'])
body = json.loads(self.rfile.read(length))
ts = datetime.datetime.now().isoformat()
for alert in body.get('alerts', []):
status = alert['status'].upper()
name = alert['labels'].get('alertname', '?')
inst = alert['labels'].get('instance', '?')
sev = alert['labels'].get('severity', '?')
print(f"[{ts}] [{status}] [{sev.upper()}] {name} — {inst}")
self.send_response(200)
self.end_headers()
HTTPServer(('0.0.0.0', 5001), AlertHandler).serve_forever()
# Sur VM-APP (serveur)
iperf3 -s -p 5201
# Sur VM-GW (client) — montée en charge progressive
for bw in 10M 50M 100M 500M 1G; do
echo "=== Test bande passante: $bw ==="
iperf3 -c 192.168.10.10 -p 5201 -b $bw -t 30 -J > /tmp/iperf-$bw.json
sleep 10
done
# Sur VM-GW — Appliquer des dégradations graduelles sur eth0
# Étape 1 : latence seule (100ms ± 20ms)
tc qdisc add dev eth1 root netem delay 100ms 20ms
sleep 120 # Observer dans Grafana
# Étape 2 : latence + perte de paquets
tc qdisc change dev eth1 root netem delay 200ms 50ms loss 5%
sleep 120
# Étape 3 : corruption + réordonnancement
tc qdisc change dev eth1 root netem delay 300ms corrupt 2% reorder 10% 25%
sleep 120
# Nettoyage
tc qdisc del dev eth1 root
# Sur VM-SEC — Nmap pour déclencher les règles Suricata
apt install nmap -y
# Scan SYN furtif (doit déclencher l'alerte PORT SCAN)
nmap -sS -T4 --top-ports 1000 192.168.10.10
# Vérifier que Suricata a détecté le scan
grep "PORT SCAN" /var/log/suricata/fast.log
# Vérifier que Fail2ban a banni l'IP
nft list set inet filter blocklist
# Sur VM-SEC — Requêtes HTTP massives vers VM-APP
apt install apache2-utils -y
ab -n 50000 -c 200 http://192.168.10.10/
# Observer simultanément dans Grafana :
# — CPU de VM-APP (panel CPU par instance)
# — Logs Nginx avec explosion des status 200 puis 503
# — Alertes Prometheus si CPU > 90%
Pour chaque scénario simulé, rédiger un runbook de réponse structuré. C'est l'exercice central du TP : apprendre à diagnostiquer méthodiquement depuis les alertes jusqu'à la cause racine.
## Runbook — Incident : [NOM DE L'ALERTE]
### 1. Détection
- Alerte déclenchée : [nom_alerte, seuil, heure]
- Source de détection : Prometheus / Suricata / Loki
### 2. Triage (< 5 min)
- Impact utilisateur : OUI / NON / PARTIEL
- Services affectés : [liste]
- Urgence : P1 / P2 / P3
### 3. Diagnostic (requêtes à exécuter)
# Vérifier la disponibilité générale
$ ping -c 10 [instance] && traceroute [instance]
# Regarder les métriques au moment T de l'incident
# (utiliser Grafana en mode historique)
# Corréler avec les logs Loki
{instance="[vm]"} |= "error" | __error__=""
### 4. Cause racine identifiée
[Description précise]
### 5. Remédiation appliquée
[Commandes exécutées]
### 6. Vérification post-incident
- Métriques revenues à la normale : OUI / NON
- Durée totale de l'incident : [T]
- Action corrective permanente : [description]
Livrables & Rapport d'analyse
~1h/etc/nftables.confcomplet de VM-GW avec commentaires/etc/prometheus/prometheus.ymlet tous les fichiers de règles/etc/loki/loki-config.yml+/etc/promtail/promtail-config.yml/etc/suricata/rules/local.rulesavec vos règles personnalisées- Export JSON du dashboard Grafana principal
- Screenshot Grafana : vue pendant la congestion iperf3 (CPU + latence + bande passante)
- Screenshot Grafana : corrélation dégradation netem + probe_duration_seconds
- Screenshot Suricata fast.log pendant le scan Nmap
- Screenshot nftables blocklist après bannissement par Fail2ban
- Rapport Lynis avant/après hardening avec le score d'amélioration
- Quelle différence concrète observez-vous entre une panne totale (
up == 0) et une dégradation silencieuse (probe_duration_secondsqui monte) ? Comment les détecter différemment ? - En quoi le mode stateful du pare-feu (suivi des connexions via
ct state) améliore-t-il la sécurité par rapport à un filtrage statique classique ? - Lors du scénario DoS applicatif, quelles métriques ont augmenté en premier ? Quelle est la chaîne causale observée ?
- Quel est l'intérêt de corréler métriques Prometheus et logs Loki dans le même outil Grafana ? Donnez un exemple de diagnostic que cela accélère.
- Comparez le score Lynis avant et après hardening. Quelles mesures ont eu le plus d'impact sur la réduction de la surface d'attaque ?