Il lab precedente#
Dopo aver configurato rete e firewall (Lab 04 e 05), aggiungiamo la sicurezza applicativa.
nginx in ns-sofia#
nginx non viene installato "dentro" il namespace (anche perchè non esiste un file system )— viene avviato nel contesto di rete di ns-sofia usando ip netns exec. Il processo gira sul filesystem dell'host Ubuntu, ma il suo stack di rete e' quello di ns-sofia. Stesso meccanismo di Docker sotto il cofano: ogni container ha dentro di se' un network namespace Linux.
# installa nginx sul sistema Ubuntu
sudo apt install -y nginx
# ferma il nginx di sistema se e' gia' in ascolto sulla 80
sudo systemctl stop nginxPer avviarlo in ns-sofia serve una configurazione separata: IP specifico (10.10.10.2, non 0.0.0.0), pid file dedicato e log dedicati per evitare conflitti con il nginx di sistema.
sudo tee /etc/nginx/nginx-sofia.conf > /dev/null << 'EOF'
worker_processes 1;
pid /run/nginx-sofia.pid;
error_log /var/log/nginx/sofia-error.log;
events { worker_connections 128; }
http {
include /etc/nginx/mime.types;
access_log /var/log/nginx/sofia-access.log combined;
server {
listen 10.10.10.2:80;
server_name corsobitcoin.com;
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
}
EOF
# pagina di test per corsobitcoin.com
sudo tee /var/www/html/index.html > /dev/null << 'EOF'
<!DOCTYPE html>
<html>
<head><title>corsobitcoin.com</title></head>
<body>
<h1>corsobitcoin.com</h1>
<p>Lezioni: Bitcoin 101, Lightning Network, Wallets.</p>
</body>
</html>
EOF
# avvia nginx nel namespace ns-sofia
sudo ip netns exec ns-sofia nginx -c /etc/nginx/nginx-sofia.confVerifica — nginx e' in ascolto su ns-sofia:
sudo ip netns exec ns-sofia ss -tlnp | grep 80
# LISTEN 0 511 10.10.10.2:80 0.0.0.0:* users:(("nginx",...))Test da ns-sofia verso se stesso:
sudo ip netns exec ns-sofia curl -s http://10.10.10.2/
# <h1>corsobitcoin.com</h1>Test da Kali — primo HTTP reale:
curl -s http://10.10.10.2/
# <h1>corsobitcoin.com</h1>
# <p>Lezioni: Bitcoin 101, Lightning Network, Wallets.</p>Log di accesso:
sudo tail -3 /var/log/nginx/sofia-access.log
# 10.10.10.2 - - [10/Jun/2026:16:36:09 +0000] "GET / HTTP/1.1" 200 174 "-" "curl/8.5.0"
# 192.168.64.200 - - [10/Jun/2026:16:36:15 +0000] "GET / HTTP/1.1" 200 174 "-" "curl/8.18.0"La prima riga e' un curl fatto da dentro ns-sofia su se stessa — source IP e' 10.10.10.2 (Sofia stessa).
La seconda riga e' il curl da Kali: nginx vede 192.168.64.200, l'IP reale di Kali. Ubuntu fa routing, non NAT — il router cambia solo i MAC address a ogni hop, il source IP del pacchetto rimane intatto. nginx vede il client originale direttamente. NAT avrebbe rimpiazzato 192.168.64.200 con 10.0.0.1, ma qui non c'e' SNAT configurato.
Contatori ns-fw1 dopo il curl:
sudo ip netns exec ns-fw1 iptables -L FORWARD -v -n
# 6 360 ACCEPT 6 -- veth-fw1-out veth-fw1-dmz tcp dpt:80
# 0 0 ACCEPT 6 -- veth-fw1-out veth-fw1-dmz tcp dpt:443
# 6 408 ACCEPT 0 -- veth-fw1-dmz veth-fw1-out state RELATED,ESTABLISHEDSYN, SYN-ACK, ACK, GET, risposta, FIN — sei pacchetti, tre per direzione.
mindmap
root((nginx in ns-sofia))
Concetto
Processo gira su filesystem Ubuntu
Stack di rete e di ns-sofia
ip netns exec cambia solo la rete
Configurazione
Listen 10.10.10.2 80
PID file dedicato
Log dedicati
root var www html
Analogia Docker
Stesso meccanismo sotto il cofano
Container ha network namespace dentro
Test
curl da ns-sofia localhost
curl da Kali 10.10.10.2
Log access con IP reale Kali
WAF - ModSecurity su nginx#
FW1 vede TCP sulla porta 80 — sa che la connessione e' verso ns-sofia, non sa cosa contiene. ModSecurity legge la request HTTP e blocca pattern noti di attacco: SQL injection, XSS, path traversal.
FW1 vede: TCP 192.168.64.200:54321 → 10.10.10.2:80 [PERMIT]
WAF vede: GET /login?id=' OR 1=1-- HTTP/1.1 [BLOCK — SQL injection]Il firewall controlla chi parla con chi (L3/L4). Il WAF controlla cosa dicono (L7). Sono due difese complementari — togliere una non cambia niente all'altra.
mindmap
root((WAF ModSecurity))
Differenza da iptables
FW1 vede L3 L4 chi parla con chi
WAF vede L7 cosa dicono
Ortogonali e indipendenti
OWASP CRS
Regole 910-944 rilevano aggiungono score
Regola 949110 giudice finale
Anomaly scoring meno falsi positivi
Attacchi bloccati
SQL Injection score 8
XSS score 23
Soglia default 5
Installazione
libnginx-mod-http-modsecurity
coreruleset da GitHub
main.conf SecRuleEngine On
Installazione#
# modulo ModSecurity per nginx
sudo apt install -y libnginx-mod-http-modsecurity
# regole OWASP CRS — il set standard di regole ModSecurity
sudo git clone --depth 1 https://github.com/coreruleset/coreruleset \
/etc/nginx/modsecurity/crs
sudo cp /etc/nginx/modsecurity/crs/crs-setup.conf.example \
/etc/nginx/modsecurity/crs/crs-setup.conf
# configurazione ModSecurity: abilita il motore + carica le regole CRS.
# NON includere modsecurity.conf base: definisce SecDefaultAction gia' presente
# in crs-setup.conf — il doppio Include causa errore "Phase 1 was informed already".
sudo tee /etc/nginx/modsecurity/main.conf > /dev/null << 'EOF'
SecRuleEngine On
Include /etc/nginx/modsecurity/crs/crs-setup.conf
Include /etc/nginx/modsecurity/crs/rules/*.conf
EOFmindmap
root((Installazione ModSecurity))
Pacchetti
libnginx-mod-http-modsecurity
coreruleset da GitHub
File di configurazione
crs-setup.conf copiato da example
main.conf con SecRuleEngine On
Errore comune
Non includere modsecurity.conf base
Causa Phase 1 was informed already
nginx + ModSecurity#
Aggiorna la configurazione di ns-sofia per attivare il WAF:
# aggiorna nginx-sofia.conf aggiungendo modsecurity on + modsecurity_rules_file.
# load_module va in cima al file — necessario quando nginx gira con -c custom config
# perche' /etc/nginx/modules-enabled/ non viene caricato automaticamente.
sudo tee /etc/nginx/nginx-sofia.conf > /dev/null << 'EOF'
load_module modules/ngx_http_modsecurity_module.so;
worker_processes 1;
pid /run/nginx-sofia.pid;
error_log /var/log/nginx/sofia-error.log;
events { worker_connections 128; }
http {
include /etc/nginx/mime.types;
access_log /var/log/nginx/sofia-access.log combined;
server {
listen 10.10.10.2:80;
server_name corsobitcoin.com;
root /var/www/html;
index index.html;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsecurity/main.conf;
location / {
try_files $uri $uri/ =404;
}
}
}
EOF
# riavvia nginx con la nuova configurazione
sudo pkill -F /run/nginx-sofia.pid 2>/dev/null; sleep 1
sudo ip netns exec ns-sofia nginx -c /etc/nginx/nginx-sofia.confmindmap
root((nginx con ModSecurity))
Modifiche alla config
load_module ngx http modsecurity
modsecurity on nel server block
modsecurity rules file main.conf
Riavvio
pkill nginx sofia pid
ip netns exec ns-sofia nginx -c
Test WAF — attacchi reali#
SQL injection:
# da Kali VM
curl -s "http://10.10.10.2/?id=%27%20OR%201%3D1--"Risposta:
<html><head><title>403 Forbidden</title></head>
<body><h1>403 Forbidden</h1></body></html>ModSecurity ha riconosciuto il pattern ' OR 1=1-- come SQL injection e ha restituito 403 prima che la request raggiungesse nginx.
XSS:
curl -s "http://10.10.10.2/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E"
# <html><head><title>403 Forbidden</title></head>...Richiesta legittima — non bloccata:
curl -s "http://10.10.10.2/"
# <h1>corsobitcoin.com</h1> <- 200 OK, nessun bloccoLog ModSecurity — SQL injection e XSS:
sudo grep "ModSecurity" /var/log/nginx/sofia-error.log | tail -5
# [error] *1 [client 192.168.64.200] ModSecurity: Access denied with code 403 (phase 2).
# Matched "Operator `Ge' with parameter `5' against variable `TX:BLOCKING_INBOUND_ANOMALY_SCORE' (Value: `8')
# [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 8)"]
# [uri "/?id=%27%20OR%201%3D1--"] ...
#
# [error] *2 [client 192.168.64.200] ModSecurity: Access denied with code 403 (phase 2).
# Matched "Operator `Ge' with parameter `5' against variable `TX:BLOCKING_INBOUND_ANOMALY_SCORE' (Value: `23')
# [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 23)"]
# [uri "/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E"] ...CRS v4 non blocca regola per regola — usa anomaly scoring. Ogni regola che matcha aggiunge punti a TX:BLOCKING_INBOUND_ANOMALY_SCORE. Quando il totale supera la soglia (default: 5), la regola 949110 blocca la request in phase 2.
SQL injection ha totalizzato 8, XSS 23 — entrambi sopra soglia. Una request normale totalizza 0.
Il vantaggio del modello a punteggio: un singolo header sospetto non basta a bloccare — serve una combinazione. Meno falsi positivi rispetto al blocco per singola regola. Il client: 192.168.64.200 nei log conferma che nginx vede ancora l'IP reale di Kali.
Il log contiene tutto quello che serve per l'analisi: regola che ha bloccato (949110), score totale, URI, IP sorgente, ID univoco della request. In un SOC reale questo log va a un SIEM — e' la firma dell'attacco, tracciabile e correlabile con altri eventi.
mindmap
root((Test WAF attacchi reali))
SQL Injection
id OR 1 1 URL encoded
Risposta 403 Forbidden
Score 8 sopra soglia 5
XSS
script alert 1 script URL encoded
Risposta 403 Forbidden
Score 23 sopra soglia 5
Richiesta legittima
GET slash
200 OK nessun blocco
Score 0
Wazuh - gli alert ModSecurity nel SIEM#
Un WAF che blocca e logga ma non manda niente a un SIEM e' un sistema cieco: l'attacco viene fermato, ma nessuno lo sa. Wazuh raccoglie i log di ModSecurity, li decodifica, li correla con MITRE ATT&CK e genera alert con severity. In un Blue Team reale questa e' la differenza tra un sistema che blocca in silenzio e uno che allerta il SOC.
Configurazione — localfile in ossec.conf#
Wazuh agent monitora file di log tramite il componente wazuh-logcollector. Basta aggiungere un blocco <localfile> a /var/ossec/etc/ossec.conf:
sudo sed -i 's|</ossec_config>| <localfile>\n <log_format>syslog</log_format>\n <location>/var/log/nginx/sofia-error.log</location>\n </localfile>\n\n</ossec_config>|' /var/ossec/etc/ossec.conf
sudo systemctl restart wazuh-agentlog_format syslog dice al logcollector come parsare il file. Wazuh ha un decoder specifico nginx-errorlog che riconosce il formato del nginx error log e lo smonta nei campi strutturati.
mindmap
root((Configurazione localfile))
Componente
wazuh-logcollector
Blocco localfile in ossec.conf
Parametri
log format syslog
location sofia-error.log
Riavvio
systemctl restart wazuh-agent
Decoder
nginx-errorlog built-in
Estrae srcip id msg uri
Il flusso nginx → Wazuh#
flowchart LR
KALI["🖥 Kali
192.168.64.200"]:::ext
SOFIA["🌐 ns-sofia
nginx + ModSecurity"]:::dmz
LOG["📄 sofia-error.log
/var/log/nginx/"]:::host
LC["wazuh-logcollector
legge il file"]:::host
MGR["🔍 Wazuh Manager
decoder + rules engine"]:::siem
DASH["📊 Dashboard
alert + MITRE"]:::siem
KALI -->|"GET /?id=' OR 1=1--"| SOFIA
SOFIA -->|"403 + log entry"| LOG
LOG -->|"tail -f inotify"| LC
LC -->|"TLS — evento raw"| MGR
MGR -->|"rule 31333 fired"| DASH
classDef ext fill:#4a1942,color:#fff,stroke:#c084fc
classDef dmz fill:#1a3a2a,color:#fff,stroke:#4ade80
classDef host fill:#2a2a2a,color:#fff,stroke:#9ca3af
classDef siem fill:#1e3a5f,color:#fff,stroke:#60a5fa
Wazuh logcollector usa inotify — non fa polling: viene notificato dal kernel ogni volta che il file cresce. La latenza tra il blocco ModSecurity e l'alert in dashboard e' di pochi secondi.
L'alert in dettaglio#

input.type: log
agent.name: HIDS-Barno
agent.id: 003
manager.name: wazuh.manager
data.srcip: 192.168.64.200
rule.id: 31333
rule.level: 7
rule.description: ModSecurity rejected a query
rule.groups: nginx, web, modsecurity
rule.mitre.id: T1083
rule.mitre.tactic: Discovery
decoder.name: nginx-errorlog
location: /var/log/nginx/sofia-error.logOgni campo ha un significato preciso:
rule.id: 31333 — e' la regola Wazuh (non CRS) che matcha gli eventi ModSecurity nei log nginx. Wazuh ha regole proprie per nginx/ModSecurity nel file 0260-nginx_rules.xml. La regola 31333 matcha specificamente gli Access denied di ModSecurity.
rule.level: 7 — scala 0-15 di Wazuh. Livello 7 = "Bad word match". Sopra 10 scatta email/alert critico. 7 e' significativo ma non critico — giusto per un attacco rilevato e bloccato.
decoder.name: nginx-errorlog — Wazuh ha un decoder built-in che riconosce il formato del nginx error log e estrae i campi: srcip, id, msg, uri. Senza decoder il log sarebbe una stringa opaca.
rule.mitre.id: T1083 / rule.mitre.tactic: Discovery — Wazuh mappa automaticamente gli alert su MITRE ATT&CK. T1083 e' "File and Directory Discovery" — la regola Wazuh associa i tentativi di SQL injection/path traversal a questa tattica perche' l'attaccante potrebbe star cercando di enumerare la struttura dell'applicazione.
mindmap
root((Alert in dettaglio))
Campi chiave
rule id 31333 regola Wazuh nginx
rule level 7 Bad word match
decoder nginx-errorlog
data srcip 192.168.64.200
MITRE mapping
T1083 File and Directory Discovery
Tattica Discovery
Automatico da Wazuh
Soglie
Livello 7 significativo non critico
Sopra 10 email e alert critico
Perche' si chiama 949110#
Il numero 949110 viene dal sistema di numerazione del CRS, non di Wazuh. Le regole CRS sono organizzate in file con un prefisso numerico:
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf — esclusioni
REQUEST-910-IP-REPUTATION.conf — blocklist IP
REQUEST-913-SCANNER-DETECTION.conf — scanner, crawler
REQUEST-920-PROTOCOL-ENFORCEMENT.conf — violazioni HTTP
REQUEST-930-APPLICATION-ATTACK-LFI.conf — Local File Inclusion
REQUEST-932-APPLICATION-ATTACK-RCE.conf — Remote Code Execution
REQUEST-941-APPLICATION-ATTACK-XSS.conf — XSS (aggiunge score)
REQUEST-942-APPLICATION-ATTACK-SQLI.conf — SQL injection (aggiunge score)
REQUEST-949-BLOCKING-EVALUATION.conf — il "giudice"Le regole 910-944 rilevano gli attacchi e aggiungono punti a TX:BLOCKING_INBOUND_ANOMALY_SCORE. Non bloccano da sole.
La regola 949110 e' il giudice finale — in phase 2 legge il punteggio accumulato e se supera la soglia (default: 5) blocca con 403. Il nome del file REQUEST-949-BLOCKING-EVALUATION lo dice esplicitamente: e' la fase di valutazione del blocco, non la fase di rilevamento.
Questo e' il motivo per cui nel log vedi sempre 949110 come regola bloccante — indipendentemente da cosa ha triggerato l'attacco (SQL injection, XSS, RCE). Le regole specifiche lavorano in silenzio accumulando score; 949110 e' l'unica che genera l'Access denied.
Kali manda: GET /?id=' OR 1=1--
CRS phase 1/2:
942100 SQL injection via libinjection → +5 score
942190 SQL injection keyword OR → +3 score
-----------------------------------------------
TX:BLOCKING_INBOUND_ANOMALY_SCORE = 8
CRS phase 2:
949110 score 8 >= soglia 5 → ACCESS DENIED 403mindmap
root((949110 CRS))
File CRS per categoria
910 IP Reputation
941 XSS aggiunge score
942 SQL Injection aggiunge score
949 Blocking Evaluation
Logica
Regole 910-944 rilevano solo
Regola 949110 giudice finale
Legge TX BLOCKING INBOUND ANOMALY SCORE
Esempio SQL injection
942100 libinjection +5
942190 keyword OR +3
Score totale 8 soglia 5
949110 ACCESS DENIED 403
ossec.log vs nginx error log#
Una distinzione importante da tenere in mente:
| File | Chi scrive | Cosa contiene |
|---|---|---|
/var/log/nginx/sofia-error.log | nginx/ModSecurity | eventi applicativi — blocchi WAF, errori HTTP |
/var/ossec/logs/ossec.log | wazuh-agent | log operativo dell'agent — avvii, errori di config, stato |
ossec.log non contiene gli eventi raccolti. Gli eventi viaggiano via TLS al manager e appaiono solo nella dashboard. Se cerchi ModSecurity in ossec.log non trovi niente — non e' un bug, e' by design.
mindmap
root((ossec.log vs nginx error log))
sofia-error.log
Scritto da nginx e ModSecurity
Contiene eventi applicativi
Blocchi WAF errori HTTP
ossec.log
Scritto da wazuh-agent
Log operativo avvii errori config
Non contiene eventi raccolti
Dove vanno gli eventi
Via TLS al Wazuh Manager
Appaiono solo in dashboard
Test end-to-end#
Il percorso completo: Kali → nginx con WAF → MySQL simulato.
# Step 1 — Kali → FW1: raggiungibilita' base
ping -c3 10.0.0.2
# 64 bytes from 10.0.0.2: ttl=63 ✓ (Ubuntu ha fatto un hop L3)
# Step 2 — Kali → Sofia/nginx: HTTP attraverso FW1
curl -s http://10.10.10.2/
# <h1>corsobitcoin.com</h1> ✓
# Step 3 — Sofia → Giulia: MySQL attraverso FW2
# apri listener MySQL simulato su ns-giulia
sudo ip netns exec ns-giulia nc -l -p 3306 &
# connettiti da ns-sofia
sudo ip netns exec ns-sofia bash -c "echo 'SELECT * FROM corsi' | nc 10.30.30.2 3306"
# SELECT * FROM corsi (compare su ns-giulia) ✓Kali (192.168.64.200)
│
│ curl http://10.10.10.2/ ✓ (ns-fw1: tcp 80 ACCEPT)
│ TTL=62 — Ubuntu + ns-fw1
▼
ns-sofia / nginx + WAF (10.10.10.2)
│
│ nc 10.30.30.2 3306 ✓ (ns-fw2: tcp 3306 da 10.10.10.2 ACCEPT)
▼
ns-giulia / MySQL simulato (10.30.30.2)Cosa Kali NON puo' fare:
# raggiungere il database direttamente
nc -w3 10.30.30.2 3306
# timeout — FW1 blocca, nessuna rotta nella catena
# SSH su ns-sofia
nc -w3 10.10.10.2 22
# timeout — FW1 non ha una regola per la 22
# SQL injection attraverso il WAF
curl "http://10.10.10.2/?id=%27%20OR%201%3D1--"
# 403 Forbidden — ModSecurity interviene prima di nginxContatori finali — prova che il traffico ha attraversato le regole corrette:
sudo ip netns exec ns-fw1 iptables -L FORWARD -v -n
# porta 80: hitcnt in crescita per ogni curl
# porta 443: 0 se non abbiamo fatto richieste HTTPS
# ESTABLISHED,RELATED: speculare al traffico in entrata
sudo ip netns exec ns-fw2 iptables -L FORWARD -v -n
# tcp dpt:3306: hitcnt per le connessioni nc di test
# ESTABLISHED,RELATED: le risposte MySQLmindmap
root((Test end-to-end))
Percorso positivo
Kali ping ns-fw1 TTL 63
Kali curl nginx 200 OK
Sofia nc MySQL SELECT passato
Cosa Kali NON puo fare
nc 10.30.30.2 3306 timeout
nc 10.10.10.2 22 timeout
SQL injection 403 dal WAF
Contatori iptables
ns-fw1 porta 80 hitcnt crescente
ns-fw2 porta 3306 hitcnt crescente
ESTABLISHED speculare
Cosa ho imparato#
I namespace Linux isolano la rete, non il processo. nginx gira sul filesystem di Ubuntu ma vede solo le interfacce di ns-sofia. ip netns exec non e' un container — e' solo un cambio di stack di rete. Docker usa namespace + cgroups + union filesystem insieme: e' lo stesso mattone di base, assemblato in modo piu' completo.
iptables e' simmetrico, il security-level ASA non lo e'. Su Cisco, inside (100) raggiunge la DMZ senza ACL perche' scende di livello. Con FORWARD DROP, ns-giulia non raggiunge ns-sofia e viceversa finche' non c'e' una regola esplicita per ogni direzione. Piu' verboso, zero comportamento implicito da ricordare.
ESTABLISHED,RELATED e' il stateful inspection. L'ASA traccia le connessioni automaticamente. iptables lo fa con il modulo state — va aggiunto esplicitamente. Senza quella regola ogni risposta del server verrebbe droppata: il SYN passa, il SYN-ACK no.
L'ambiente ospite complica il lab. Docker e ufw occupano la FORWARD chain dell'host con policy DROP prima che la configuriamo. In un Ubuntu pulito questo problema non esiste. Ma in produzione e' sempre cosi': piu' servizi significano piu' regole iptables, spesso in conflitto. Capire chi ha aggiunto quale regola richiede di leggere le chain nell'ordine in cui vengono processate — non basta guardare iptables -L.
Il WAF e' orthogonale al firewall. FW1 non sa che il payload contiene ' OR 1=1--. Lo sa solo il WAF perche' opera a L7, dopo che il firewall ha gia' deciso di lasciare passare la connessione. Togliere il WAF non cambia niente alle regole iptables — e viceversa. Sono due layer indipendenti che si sommano.
Lo script e' il lab. In Packet Tracer salvi il .pkt e riapri tutto. Qui setup-dmz.sh e' la versione salvata della topologia — la stessa idea del file .pkt, ma testuale e modificabile. L'infrastruttura come codice parte esattamente da qui: una topologia di rete descritta come codice, riproducibile con un comando.
mindmap
root((Cosa ho imparato))
Namespace
Isolano rete non il processo
ip netns exec non e un container
Docker usa namespace piu cgroups
iptables vs Cisco ASA
iptables simmetrico tutto bloccato
ASA asimmetrico inside raggiunge fuori
Zero comportamento implicito
ESTABLISHED RELATED
Stateful inspection in iptables
Va aggiunto esplicitamente
Senza il SYN ACK viene droppato
Ambiente ospite complica il lab
Docker ufw occupano FORWARD chain
In Ubuntu pulito non esiste problema
WAF ortogonale a firewall
FW controlla L3 L4
WAF controlla L7
Due layer indipendenti
Script e la topologia salvata
Equivalente del pkt di Packet Tracer
Infrastructure as code da qui parte
Script e file di configurazione del lab: github.com/Barno/u-random-dev





