Riconoscere un attacco DDoS: come averne la certezza
Non ogni sovraccarico è un attacco. Con ss, i contatori dei pacchetti, i messaggi del kernel e i log del webserver distingui senza ambiguità un attacco DDoS da un picco di carico o da un bug del software.
Il servizio non risponde più, l'indicatore del carico è schizzato in alto e in chat arriva la domanda: ci stanno attaccando? È una domanda che si può misurare. Si può misurare anche piuttosto in fretta, se sai quali quattro numeri guardare, in che ordine e quale controprova trasforma un sospetto in un risultato certo.
Questo articolo tratta esclusivamente la diagnosi. Non si parla di respingere un attacco, ma di distinguerlo senza ambiguità da un problema di carico, da un bug del software o semplicemente dal successo. Se vuoi sapere che cos'è tecnicamente un attacco DDoS, lo trovi in Che cos'è un attacco DDoS. Se invece vuoi agire dopo la diagnosi, prosegui con Proteggere il server dagli attacchi DDoS.
Quattro sospettati, un solo sintomo
"Il server è lento" non è una diagnosi, è un sintomo con almeno quattro cause plausibili. Prima di digitare un solo comando dovresti sapere quali schemi vuoi davvero distinguere.
| Osservazione | Attacco | Vero afflusso di visitatori | Bug del software |
|---|---|---|---|
| Inizio | improvviso, nel giro di pochi secondi | crescita nell'arco di minuti, spesso con una curva | subito dopo un deploy, un cronjob o un aggiornamento |
| Carico di rete in ingresso | da alto a estremo, spesso molti pacchetti piccoli | moderato, in uscita nettamente più alto che in ingresso | nulla di anomalo |
| Connessioni per IP sorgente | moltissime da pochi IP oppure pochissime da moltissimi IP | distribuite in modo uniforme, poche per IP | normale |
| Referrer nel log | quasi sempre vuoto | siti di notizie, social network, motori di ricerca | normale |
| Risposta su 127.0.0.1 | veloce (il problema è la rete) | lenta (il problema è l'applicazione) | lenta oppure errore |
| Dopo il riavvio del servizio | il carico torna subito | il carico torna, il quadro dell'errore resta identico | il problema sparisce spesso per qualche minuto |
Il quarto sospettato manca in questa tabella perché non ha bisogno di una colonna propria: le operazioni pianificate. Backup, reindicizzazioni, rotazioni dei log e aggiornamenti dei pacchetti girano a orari prevedibili. Un'occhiata a systemctl list-timers e alle crontab costa dieci secondi e chiude un numero sorprendente di presunti attacchi.
I primi 60 secondi: quattro numeri
Rileva quattro valori in quest'ordine. È la combinazione a essere significativa, il singolo valore preso da solo non lo è.
cat /proc/loadavg
ss -s
cat /proc/net/dev
curl -o /dev/null -s -w '%{time_total}\n' http://127.0.0.1/
L'interpretazione:
- Carico alto, carico di rete alto, molte connessioni semiaperte, ma 127.0.0.1 risponde in millisecondi: il problema sta davanti all'applicazione, quindi a livello di rete. È il quadro classico di un attacco.
- Carico alto, carico di rete normale, 127.0.0.1 risponde lentamente: applicazione o database. Non è un attacco, è lavoro per chi sviluppa.
- Carico basso, carico di rete alto: molto sospetto. Una tempesta di pacchetti che non arriva nemmeno fino all'applicazione consuma poca CPU e molta linea.
- Tutto basso, servizio comunque irraggiungibile dall'esterno: vedi più avanti, la sezione sui casi senza traccia misurabile.
Per il valore di carico vale una precisazione: /proc/loadavg conta anche i processi in attesa di input e output. Un valore di 40 con quattro core può essere sia un attacco sia un disco sovraccarico. vmstat 1 5 separa le due cose in modo pulito: la colonna r mostra i processi pronti al calcolo, b quelli bloccati e wa la quota di tempo di attesa.
Contare le connessioni: ss al posto di netstat
Quasi tutte le guide in rete iniziano con netstat. Su un sistema attuale finisce così:
Command 'netstat' not found, but can be installed with:
apt install net-tools
netstat fa parte del pacchetto net-tools, che nelle installazioni standard di Debian 12, Debian 13, Ubuntu 22.04 e Ubuntu 24.04 non è più incluso. Nemmeno sulla famiglia Red Hat (AlmaLinux, Rocky, RHEL, Oracle Linux). Puoi installarlo a posteriori, ma ha più senso ss da iproute2: è presente praticamente su ogni server, è nettamente più veloce con molte connessioni e fornisce esattamente le stesse informazioni.
ss -s
Perché i comandi seguenti funzionino tutti ti serve una manciata di pacchetti, e i nomi cambiano a seconda della famiglia di distribuzione. È il punto in cui più spesso una guida copiata si blocca già alla prima riga su AlmaLinux:
| Strumento | Debian e Ubuntu | AlmaLinux, Rocky, Oracle Linux |
|---|---|---|
| ss, nstat, ip | iproute2 | iproute |
| netstat | net-tools | net-tools |
| vmstat, free, top | procps | procps-ng |
| sar | sysstat | sysstat |
| dig | dnsutils | bind-utils |
| tcpdump | tcpdump | tcpdump |
Su Debian e Ubuntu quindi apt-get install -y iproute2 net-tools procps sysstat dnsutils tcpdump, sulla famiglia Red Hat dnf -y install iproute net-tools procps-ng sysstat bind-utils tcpdump. Meglio non aggiungere lì anche curl all'elenco: è già installato come curl-minimal e il pacchetto completo va in conflitto (curl-minimal ... conflicts with curl). Se ti serve comunque, la soluzione è dnf -y --allowerasing install curl.
La prima riga indica il numero complessivo dei socket, la riga TCP scompone il dato: estab, closed, orphaned, timewait. Un valore alto di timewait, da solo, non è un indizio di attacco: è la normale conseguenza di molte connessioni HTTP brevi.
Più interessante è la distribuzione tra gli stati:
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn
Salta all'occhio un blocco grande in SYN-RECV. Sono connessioni iniziate e mai confermate, che è esattamente lo schema di un SYN flood. Si contano direttamente così:
ss -Htn state syn-recv | wc -l
La trappola delle colonne su cui fallisce la maggior parte delle one-liner
Non appena passi a ss un filtro di stato, la colonna dello stato sparisce dall'output. L'estremità remota si trova allora nella colonna 4 invece che nella colonna 5. È proprio per questo che le one-liner copiate dai forum restituiscono regolarmente risultati privi di senso: contano porte al posto di indirizzi IP oppure stampano righe vuote. Da tenere a mente: senza filtro l'estremità remota è $5, con filtro $4. Il -H sopprime inoltre la riga di intestazione, così wc -l torna corretto senza aggiustamenti.
Connessioni per IP sorgente, a prova di IPv6:
ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -20
Il sed taglia solo l'ultimo due punti insieme alla porta. L'approccio diffuso con cut -d: -f1 funziona con IPv4, ma spezza gli indirizzi IPv6 dopo il primo blocco e rende l'analisi inutile. Con IPv6 restano le parentesi quadre, cosa che non disturba il conteggio.
L'interpretazione richiede buon senso. Cento connessioni da un singolo IP possono essere un attacco, ma anche il NAT di un'azienda, il carrier NAT di un operatore mobile o un reverse proxy davanti al tuo server. Se davanti c'è una rete di distribuzione dei contenuti o un loadbalancer, vedrai comunque solo i suoi indirizzi e dovrai ripiegare sul log del webserver, guardando X-Forwarded-For.
Per UDP vale la stessa struttura con -u. E per sapere quale servizio è in ascolto su quale porta:
ss -tulnp
Misurare correttamente carico di rete e frequenza dei pacchetti
La banda in megabit è il numero che citano tutti. Quello più significativo è la frequenza dei pacchetti. Un attacco con 200.000 pacchetti minuscoli al secondo mette in ginocchio un server, anche se la banda sembra innocua.
Per prima cosa il nome dell'interfaccia, perché eth0 non è affatto la risposta giusta dappertutto. Nomi comuni sono ens3, enp1s0 oppure eth0:
ip -br link
Poi due misurazioni a un secondo di distanza, senza alcun pacchetto aggiuntivo. Il nome dell'interfaccia viene ricavato dalla route di default invece di scriverlo a mano:
IF=$(ip -o route get 1.1.1.1 | awk '{print $5}')
A=$(cat /sys/class/net/$IF/statistics/rx_packets) || exit 1; sleep 1; B=$(cat /sys/class/net/$IF/statistics/rx_packets); echo "$((B-A)) pacchetti/s in ingresso su $IF"
Questa complicazione ha un motivo molto concreto. Se scrivi qui eth0 in modo fisso mentre l'interfaccia si chiama in realtà ens3 o enp1s0, come accade sulla maggior parte dei server KVM, il comando stampa sì due volte cat: /sys/class/net/eth0/statistics/rx_packets: No such file or directory, ma poi annuncia tranquillamente 0 pacchetti/s in ingresso e termina con codice di ritorno 0. In un articolo sul riconoscimento degli attacchi è la variante più pericolosa in assoluto: leggi zero pacchetti, dai il cessato allarme e l'attacco continua. Il || exit 1 impedisce esattamente questo.
La stessa cosa con rx_bytes dà i byte al secondo. Dai due valori calcoli la dimensione media dei pacchetti, e questa rivela il tipo di attacco:
- sotto i 100 byte in media con frequenza di pacchetti molto alta: SYN flood, ACK flood o UDP flood. L'obiettivo è l'elaborazione dei pacchetti, non la linea.
- da 1200 a 1500 byte con banda elevata, porte sorgente 53, 123, 389 o 11211: attacco di riflessione e amplificazione. Gli IP sorgente sono server estranei, non gli aggressori.
- distribuzione normale delle dimensioni, richieste HTTP pulite: livello applicativo. In quel caso decide il log del webserver, non il contatore dei pacchetti.
Più comodo con sar dal pacchetto sysstat:
sar -n DEV 1 3
Punto critico: la misurazione dal vivo funziona subito dopo l'installazione, l'analisi storica con sar -f invece no. Su Debian e Ubuntu la raccolta dei dati è disattivata di fabbrica, in /etc/default/sysstat c'è scritto ENABLED="false". Chi se ne accorge solo durante un attacco non ha valori di confronto dell'altro ieri. È il motivo per cui questo pacchetto va attivato per tempo e non nel momento critico.
E la limitazione più importante in assoluto: dentro il server misuri solo ciò che è passato. Se davanti c'è un filtraggio, vedi una frazione del volume reale oppure niente del tutto. Il numero affidabile sta nel grafico del traffico nell'area clienti, non in /proc/net/dev.
Leggere i messaggi del kernel
Il kernel registra le situazioni di sovraccarico che a livello applicativo restano invisibili. Su Ubuntu e sulle versioni recenti di Debian il ring buffer è bloccato per gli utenti normali, senza sudo compare:
dmesg: read kernel buffer failed: Operation not permitted
Non è un malfunzionamento, è kernel.dmesg_restrict=1. Con i permessi di root e i timestamp:
dmesg -T | grep -Ei 'syn flood|conntrack|neighbour|drop'
Il messaggio che quasi tutti cercano suona letteralmente così:
TCP: request_sock_TCP: Possible SYN flooding on port 443. Sending cookies. Check SNMP counters.
Ci sono due varianti e una differenza di formato che conviene conoscere:
- Sending cookies significa che i SYN cookie sono attivi e che le connessioni continuano a essere servite. Dropping request compare invece quando
net.ipv4.tcp_syncookiesè a 0. In quel caso le richieste vengono scartate, visitatori veri compresi. - I kernel più vecchi indicano solo il numero di porta, quelli più recenti anche l'indirizzo in ascolto nella forma
0.0.0.0:443. Su Debian 12 vedrai quindi la forma breve, su Debian 13 e Ubuntu 24.04 quella lunga. Chi cerca con grep il vecchio testo esatto sui sistemi nuovi non trova nulla.
Importante per l'onestà della diagnosi: questo messaggio non dimostra un attacco. Compare anche quando un'applicazione lavora con un listen backlog troppo piccolo e una legittima ondata di carico la travolge. È un indizio, che va sostenuto da altri valori misurati.
I contatori corrispondenti li fornisce nstat:
nstat -az TcpExtSyncookiesSent
nstat -az TcpExtListenDrops
nstat -az TcpExtListenOverflows
Anche qui un punto critico: nstat a ogni invocazione memorizza uno stato intermedio e alla volta successiva mostra soltanto la differenza. È voluto e per la misurazione è persino comodo, ma sorprende chiunque veda improvvisamente degli zeri alla seconda invocazione. Con -a forzi i valori assoluti, con -s eviti l'aggiornamento dello stato.
Altri due messaggi che nei casi di attacco compaiono con regolarità e che portano entrambi a perdita di pacchetti per i visitatori legittimi:
nf_conntrack: table full, dropping packet
neighbour: arp_cache: neighbor table overflow!
Il livello di riempimento del connection tracking lo controlli con cat /proc/sys/net/netfilter/nf_conntrack_count confrontandolo con nf_conntrack_max. Entrambi i file esistono solo se il modulo è caricato, quindi se è attivo un firewall. Su un sistema senza regole mancano, ed è normale.
Log del webserver: schemi al posto delle sensazioni
Su Debian e Ubuntu i log si trovano in /var/log/nginx/access.log ovvero /var/log/apache2/access.log. Sulla famiglia Red Hat il percorso di Apache è /var/log/httpd/access_log, senza punto prima del suffisso. Per cominciare bastano tre analisi:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
La distinzione tra attacco e vero afflusso nasce dal rapporto tra queste tre liste:
- Rapporto tra richieste e IP univoci. Diecimila richieste da 6.000 indirizzi sono pubblico. Diecimila richieste da 30 indirizzi non lo sono.
- Richieste successive. Un visitatore vero, dopo la pagina HTML, carica anche fogli di stile, script e immagini. Se nell'elenco dei percorsi compare esclusivamente un unico URL e nemmeno un file statico, non c'era nessun browser al lavoro.
- Provenienza. Con un successo pubblicitario nel campo referrer c'è una fonte: un sito di notizie, un social network, un motore di ricerca. In un attacco quel campo è tipicamente vuoto oppure riempito con un indirizzo inventato.
- Identificativo del programma. Un unico user agent, esattamente identico su decine di migliaia di richieste, è uno strumento automatico. Sospetti anche gli identificativi di versioni di browser molto vecchie.
- Codici di stato. Una netta prevalenza di 200 parla a favore del pubblico, un muro di 404 o 499 indica invece scansioni automatiche ovvero client che interrompono prima della risposta.
Attenzione con i motori di ricerca: capita spesso che un IP dichiari nello user agent di essere Googlebot. L'unico modo per verificarlo è un test di risoluzione inversa seguito da una risoluzione diretta. Solo quando il nome inverso punta a un dominio del fornitore e quel nome si risolve di nuovo sullo stesso IP, l'affermazione è vera.
dig -x 66.249.66.1 +short
Per un indirizzo Googlebot autentico torna un nome come crawl-66-249-66-1.googlebot.com., che poi fai risolvere di nuovo sullo stesso IP con dig +short crawl-66-249-66-1.googlebot.com. Se non torna nulla, la causa può essere anche un server senza risoluzione DNS in uscita e quindi non è una prova.
E la frase più importante in assoluto sui log: un attacco a livello di rete non compare nel log del webserver. Un SYN flood non raggiunge mai l'applicazione e non vi lascia nemmeno una riga. Un log vuoto quindi non smentisce nulla, delimita solo il livello.
La controprova: così il sospetto diventa un risultato
Fino a qui hai indizi. Diventano solidi grazie alle controprove, ognuna capace di smentire esattamente un'ipotesi.
- Fermare il servizio. Ferma il webserver per 30 secondi. Se la frequenza dei pacchetti in ingresso resta invariata e alta, l'attacco sta sotto l'applicazione. Se crolla, erano richieste alla tua applicazione, malintenzionate o meno.
- Interno contro esterno. Se
curlrisponde su 127.0.0.1 in millisecondi mentre la chiamata dall'esterno va in timeout, l'applicazione è sana e il problema è la linea. - Seconda misurazione. Ripeti la misurazione dei pacchetti due minuti dopo. Gli attacchi durano oppure tornano a ondate. Un picco isolato era un picco.
- Sguardo dall'esterno. Un test di raggiungibilità esterno da più località separa "irraggiungibile per tutti" da "irraggiungibile solo per te". Il secondo caso è quasi sempre un problema di routing o del provider di chi guarda, non un attacco.
- Verificare la direzione. Confronta
rx_packetscontx_packets. Se quella anomala è la frequenza in uscita, non stai subendo un attacco: è il tuo server ad attaccare. In quel caso è compromesso oppure viene abusato come riflettore, e il caso cambia immediatamente urgenza.
Da cosa capisci che la diagnosi regge: riesci a dire in una frase quale protocollo colpisce quale porta e con quale frequenza, se in ingresso o in uscita, e hai due misurazioni indipendenti che mostrano la stessa cosa. Tutto ciò che sta sotto questo livello è un'ipotesi.
Quando non c'è nessuna traccia misurabile
Quattro situazioni che creano confusione con regolarità:
- Non riesci più ad accedere al server. Con la linea satura non passa nemmeno SSH. L'accesso avviene allora tramite la console nell'area clienti, che funziona indipendentemente dalla connettività di rete del sistema. Serve esattamente a questo.
- Il servizio era irraggiungibile, ma nel server non c'è nulla. Con un filtraggio a monte è la regola, non l'eccezione. Se l'attacco viene intercettato nella rete, il sistema vede solo una breve flessione. La prova sta allora nel grafico del traffico nell'area clienti.
- Il log si interrompe nel bel mezzo dell'incidente. Controlla se nel frattempo è girata una rotazione dei log: il materiale più vecchio si trova accanto come
access.log.1oppureaccess.log.2.gz. Seaccess_logè disattivato o bufferizzato nella configurazione, le ultime righe semplicemente mancano. - Il kernel tace. Sui sistemi basati su container il ring buffer appartiene al sistema host, lì
dmesgnon mostra nulla di proprio. Su un server root KVM con kernel proprio il problema non si pone.
Cosa documentare prima di contattare il provider
Una segnalazione del tipo "il server era lento questo pomeriggio" allunga la lavorazione di parecchi giri. Una segnalazione con valori misurati viene lavorata subito. Raccogli i dati mentre l'incidente è in corso, perché i contatori in /proc vengono azzerati al riavvio.
mkdir -p /root/incidente && cd /root/incidente
date -u > 01-ora.txt
ss -s > 02-sockets.txt
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn > 03-stati.txt
ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -50 > 04-top-ips.txt
ip -s link > 05-interfacce.txt
sar -n DEV 1 10 > 06-frequenza-pacchetti.txt
dmesg -T | tail -100 > 07-kernel.txt
nstat -az > 08-contatori.txt
Se la linea lo consente, ci va anche una breve cattura di pacchetti. Limitala: una cattura senza limiti su una linea satura riempie il disco in pochi minuti e peggiora il problema:
tcpdump -D
tcpdump -ni "$IF" -s 96 -c 2000 -w /root/incidente/cattura.pcap
tcpdump -D elenca le interfacce disponibili, nel caso la variabile $IF della sezione di misurazione più sopra non fosse più impostata. Entrambe le chiamate richiedono i permessi di root.
Nel ticket vanno poi:
- Momento di inizio e di fine, con il fuso orario.
date -uevita qualsiasi discussione in merito. - Indirizzo IP interessato e porta interessata.
- Frequenza dei pacchetti e banda misurate, espressamente con l'indicazione della direzione.
- Distribuzione dei protocolli e, se riconoscibili, le porte sorgente delle controparti.
- Una manciata di indirizzi sorgente di esempio, con l'avvertenza che i mittenti possono essere falsificati.
- Estratto del log del webserver con lo schema di richiesta ricorrente: bastano da tre a cinque righe.
- Che cosa è stato cambiato poco prima: deploy, cambio DNS, campagna pubblicitaria, nuova apertura di porta.
- Esito della controprova: il servizio era raggiungibile su 127.0.0.1 mentre dall'esterno non rispondeva?
Errori di ragionamento frequenti
- Prendere molte connessioni TIME-WAIT come prova di un attacco. Sono la normale conseguenza di connessioni HTTP brevi e spariscono da sole.
- Prendere il messaggio di SYN flooding come prova. Un listen backlog configurato troppo piccolo lo genera anche con un'ondata di carico legittima.
- Bloccare un IP con molte connessioni senza verificare. Dietro un NAT aziendale, un NAT mobile o un proxy a monte finisci per escludere interi gruppi di clienti.
- Dedurre un attacco dalla banda elevata. Verifica prima la direzione. I picchi in uscita sono quasi sempre un backup o un download molto richiesto.
- Dedurre "nessun attacco" da log vuoti. Gli attacchi a livello di rete non raggiungono mai l'applicazione.
- Riavviare mentre l'incidente è in corso. Il riavvio cancella tutti i contatori che ti sarebbero serviti per la segnalazione, e dopo l'attacco prosegue immutato.
Chi ha fatto pratica una volta con questa sequenza non impiega cinque minuti per arrivare a un risultato solido. E chi allega i valori misurati nel ticket salta le domande di chiarimento e arriva direttamente alla soluzione. In tema: la checklist per i nuovi server root e Configurare Fail2ban per il lavoro successivo a livello applicativo.
Domande frequenti
Come distinguo un attacco DDoS da un vero afflusso di visitatori?
Perché netstat non è installato sul mio server?
Il messaggio Possible SYN flooding on port 443 indica sempre un attacco?
Perché non trovo nessuna traccia sul server anche se il servizio è stato brevemente irraggiungibile?
Come accedo al server se SSH non risponde più durante l'incidente?
Perché la one-liner con ss che ho copiato restituisce numeri sbagliati?
Cosa faccio se la frequenza dei pacchetti in uscita è quella anomala?
2026 KernelHost GmbH. Tutti i diritti riservati. Questa guida è protetta dal diritto d'autore. La ripubblicazione su altri siti web, anche parziale o in forma modificata, non è consentita senza il nostro consenso scritto. Le citazioni con indicazione della fonte e un link sono le benvenute.

