Hardening della sicurezza di Nginx su Ubuntu e Debian
Rafforza Nginx oltre la configurazione predefinita con header di sicurezza, TLS 1.3, HSTS, restrizione dei metodi HTTP e controlli di accesso. Ogni direttiva è collegata all'attacco specifico che previene.
Un'installazione Nginx predefinita serve traffico. Non lo protegge. La configurazione di default espone la versione del server, accetta qualsiasi metodo HTTP, non invia header di sicurezza e usa le impostazioni TLS fornite da OpenSSL.
Questa guida rafforza Nginx su Debian 12 e Ubuntu 24.04. Ogni sezione nomina prima la minaccia, poi mostra la direttiva che la blocca. Puoi applicare queste modifiche su un server in produzione senza downtime.
Prerequisiti:
- Nginx installato e che serve almeno un sito via HTTPS (Configurare Let's Encrypt SSL/TLS per Nginx su Debian 12 e Ubuntu 24.04)
- Accesso root o sudo
- Familiarità di base con la struttura di configurazione di Nginx (Struttura dei file di configurazione Nginx)
Conserviamo tutte le direttive di hardening in un unico file include. Questo mantiene i blocchi server puliti e semplifica l'audit:
sudo touch /etc/nginx/snippets/security-hardening.conf
Ogni blocco server che necessita di hardening riceve una sola riga:
include /etc/nginx/snippets/security-hardening.conf;
Dopo ogni modifica in questa guida, testa e ricarica:
sudo nginx -t && sudo systemctl reload nginx
Come nascondo la versione del server Nginx?
Aggiungi server_tokens off; nel blocco http in /etc/nginx/nginx.conf. Questo rimuove il numero di versione dall'header di risposta Server e dalle pagine di errore predefinite. Gli attaccanti scansionano versioni specifiche con CVE noti. Nascondere la versione non corregge le vulnerabilità, ma aumenta il costo degli attacchi mirati.
Aggiungi in /etc/nginx/nginx.conf all'interno del blocco http:
server_tokens off;
Questo va nella configurazione principale, non nello snippet, perché si applica globalmente.
Dopo il ricaricamento, controlla l'header di risposta:
curl -sI https://your-domain.com | grep -i server
Server: nginx
Nessun numero di versione. Prima di questa modifica, mostrava qualcosa come Server: nginx/1.24.0.
Come blocco le richieste a hostname sconosciuti?
Quando qualcuno accede direttamente all'indirizzo IP del tuo server o usa un hostname non riconosciuto, Nginx serve il primo blocco server che trova. Questo apre la porta ad attacchi DNS rebinding e permette agli scanner di identificare i tuoi servizi.
Un blocco server predefinito che rifiuta tutte le richieste non corrispondenti risolve il problema. Aggiungilo come primo blocco server caricato da Nginx:
sudo nano /etc/nginx/sites-available/00-default-deny.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
# Self-signed or snakeoil cert just to complete the TLS handshake
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
return 444;
}
Il codice di stato 444 è specifico di Nginx. Chiude la connessione immediatamente senza inviare risposta.
Attivalo:
sudo ln -s /etc/nginx/sites-available/00-default-deny.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Su Debian 12 e Ubuntu 24.04, installa il pacchetto ssl-cert se il certificato snakeoil manca:
sudo apt install ssl-cert
Testalo accedendo direttamente via IP:
curl -sI -k https://YOUR_SERVER_IP
La connessione si chiude con una risposta vuota o un errore curl (codice di uscita 52). Nessun contenuto servito.
Quale configurazione TLS dovrei usare per Nginx nel 2026?
Usa il profilo Intermediate di Mozilla: TLS 1.2 e 1.3 con suite di cifratura a forward secrecy. Questo copre client fino a Firefox 27 e Android 4.4.2 eliminando i protocolli insicuri. TLS 1.0 e 1.1 sono vulnerabili a POODLE e BEAST e sono deprecati dalla RFC 8996 (2021). Se tutti i tuoi client supportano TLS 1.3, usa invece il profilo Modern (ssl_protocols TLSv1.3;).
L'hardening TLS è diventato più urgente a inizio 2026. CVE-2026-1642 (CVSS 5.9) ha dimostrato che la gestione TLS upstream di Nginx aveva una race condition che permetteva l'iniezione MitM prima del completamento dell'handshake. La correzione è arrivata in Nginx 1.28.2 e 1.29.5. Controlla la tua versione con nginx -v e aggiorna se necessario.
| Profilo | Protocolli | Client più vecchio | Caso d'uso |
|---|---|---|---|
| Modern | Solo TLS 1.3 | Firefox 63, Chrome 70 | API, applicazioni web moderne |
| Intermediate | TLS 1.2 + 1.3 | Firefox 27, Android 4.4 | Server generici |
| Old | TLS 1.0 + 1.1 + 1.2 + 1.3 | IE 8 su XP | Solo compatibilità legacy |
Genera i parametri DH per le suite di cifratura DHE nel profilo Intermediate. Richiede qualche minuto:
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
sudo chmod 644 /etc/nginx/dhparam.pem
Aggiungi l'hardening TLS in /etc/nginx/snippets/security-hardening.conf:
# TLS protocols - Mozilla Intermediate profile
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites - Mozilla Intermediate profile (version 5.7)
# TLS 1.3 suites are configured automatically by OpenSSL
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
Perché ssl_prefer_server_ciphers off? Con solo cifrature forti nella lista, la preferenza del client offre prestazioni migliori. I client scelgono la cifratura che il loro hardware accelera meglio.
Perché ssl_session_tickets off? I ticket di sessione usano una chiave a livello di server. Se quella chiave viene compromessa, un attaccante può decifrare tutte le sessioni registrate. Senza ticket, la cache di sessione condivisa fornisce il ripristino solo sullo stesso server, che è sufficiente per configurazioni a server singolo.
Nota su OCSP stapling: Let's Encrypt ha chiuso il suo servizio OCSP ad agosto 2025. Se usi certificati Let's Encrypt, rimuovi le direttive ssl_stapling. Causeranno avvisi nei log. Let's Encrypt ora pubblica le informazioni di revoca esclusivamente tramite CRL.
Ricarica e testa dalla tua macchina locale:
openssl s_client -connect your-domain.com:443 -tls1_2 </dev/null 2>/dev/null | grep "Protocol\|Cipher"
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES256-GCM-SHA384
Conferma che TLS 1.1 viene rifiutato:
openssl s_client -connect your-domain.com:443 -tls1_1 </dev/null 2>&1 | head -5
L'handshake fallisce. Per un audit completo, invia il tuo dominio a SSL Labs. Punta a un grado A o A+.
Come attivo HSTS in Nginx?
HSTS (HTTP Strict Transport Security) dice ai browser di connettersi solo via HTTPS per un periodo stabilito. Senza di esso, un attaccante MitM può intercettare la richiesta HTTP iniziale e degradare la connessione. Una volta che un browser vede l'header HSTS, rifiuta le connessioni HTTP in chiaro al tuo dominio fino alla scadenza di max-age.
Aggiungi in /etc/nginx/snippets/security-hardening.conf:
# HSTS - 2 years, include subdomains
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
Il flag always assicura che Nginx invii questo header anche nelle risposte di errore (4xx, 5xx). Senza always, una risposta 403 o 500 non includerebbe l'header, lasciando una finestra per attacchi di downgrade.
Prima di aggiungere includeSubDomains, conferma che tutti i tuoi sottodomini supportano HTTPS. Questa direttiva si applica a ogni sottodominio. Un sottodominio senza certificato valido diventa irraggiungibile.
Precaricamento HSTS: Aggiungere preload all'header e inviare il tuo dominio a hstspreload.org incorpora l'applicazione HTTPS nei browser in modo permanente. La rimozione richiede mesi. Aggiungi preload solo quando sei certo che ogni sottodominio servirà sempre HTTPS.
Dopo il ricaricamento:
curl -sI https://your-domain.com | grep -i strict
strict-transport-security: max-age=63072000; includeSubDomains
Quali header di sicurezza dovrebbe avere ogni server Nginx?
Cinque header di risposta proteggono contro attacchi comuni lato browser: MIME sniffing, clickjacking, cross-site scripting, leak del referrer e abuso delle API del browser. Aggiungi tutti e cinque con il flag always perché si applichino anche alle risposte di errore. L'header deprecato X-XSS-Protection non dovrebbe essere incluso. I browser moderni hanno rimosso i loro auditor XSS, e l'header stesso in alcuni casi introduceva vulnerabilità.
Aggiungi in /etc/nginx/snippets/security-hardening.conf:
# Prevent MIME type sniffing - stops browsers from interpreting files as a
# different content type than declared, blocking drive-by download attacks
add_header X-Content-Type-Options "nosniff" always;
# Clickjacking protection - prevents your pages from being embedded in
# iframes on other sites
add_header X-Frame-Options "SAMEORIGIN" always;
# Content Security Policy - controls which sources can load scripts, styles,
# images, and other resources. Start restrictive, loosen as needed.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
# Referrer Policy - controls how much URL information the browser sends
# when navigating away from your site
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions Policy - disables browser features you don't use, preventing
# compromised scripts from accessing camera, microphone, etc.
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
interest-cohort=() compariva nelle vecchie guide per bloccare Google FLoC. FLoC è stato abbandonato nel 2023. I browser moderni non riconoscono questa direttiva e mostrano avvisi nella console se la includi.
| Header | Valore | Minaccia mitigata |
|---|---|---|
| X-Content-Type-Options | nosniff |
Attacchi per confusione MIME, download furtivi |
| X-Frame-Options | SAMEORIGIN |
Clickjacking tramite iframe |
| Content-Security-Policy | default-src 'self' |
XSS, iniezione dati, caricamento risorse non autorizzato |
| Referrer-Policy | strict-origin-when-cross-origin |
Leak URL verso terze parti |
| Permissions-Policy | camera=(), microphone=()... |
Abuso API browser da script iniettati |
Regolazione CSP: L'esempio sopra è restrittivo. La maggior parte delle applicazioni necessita di aggiustamenti. Se il tuo sito carica script da un CDN, aggiungilo: script-src 'self' https://cdn.example.com;. Usa la console sviluppatori del browser per trovare le violazioni CSP e allenta la policy incrementalmente. Non usare mai unsafe-eval senza comprendere il rischio.
La trappola dell'ereditarietà di add_header
Nginx scarta tutte le direttive add_header dei blocchi genitori quando un blocco location figlio definisce il proprio add_header. Se aggiungi un header dentro un blocco location, tutti gli header di sicurezza del livello server o http scompaiono dalle risposte di quella location.
Due modi per gestirlo:
- Nginx 1.29.3+: Usa
add_header_inherit merge;nel blocco figlio per preservare gli header genitori. - Versioni precedenti: Ripeti la riga
include /etc/nginx/snippets/security-hardening.conf;in ogni blocco location che aggiunge i propri header.
Ricarica e controlla:
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://your-domain.com | grep -iE "x-content-type|x-frame|content-security|referrer-policy|permissions-policy"
Tutti e cinque gli header appaiono nell'output. Controlla anche un percorso location specifico, non solo la root, per individuare problemi di ereditarietà.
Come disattivo i metodi HTTP non sicuri in Nginx?
La maggior parte dei siti ha bisogno solo di GET, HEAD e POST. Metodi come DELETE, PUT, TRACE e OPTIONS (quando non usati) ampliano la superficie di attacco. TRACE in particolare può esporre cookie e token di autenticazione tramite attacchi XST (cross-site tracing).
L'approccio più pulito in Nginx standard è limit_except, progettato specificamente per la restrizione dei metodi. Inseriscilo nei blocchi location:
location / {
limit_except GET POST {
deny all;
}
# ... your existing config
}
limit_except GET permette implicitamente HEAD (HEAD è un GET senza corpo secondo la specifica HTTP). Qualsiasi metodo non elencato restituisce 403 Forbidden.
Per un approccio globale con map (aggiungi in nginx.conf nel blocco http):
map $request_method $method_not_allowed {
default 1;
GET 0;
HEAD 0;
POST 0;
}
Poi nel blocco server:
if ($method_not_allowed) {
return 405;
}
L'approccio limit_except è preferito quando serve controllo per location. L'approccio map funziona quando vuoi una policy uniforme su tutto il server.
Testa con un metodo bloccato:
curl -sI -X DELETE https://your-domain.com
Restituisce HTTP/1.1 403 Forbidden o HTTP/1.1 405 Not Allowed.
curl -sI -X GET https://your-domain.com
Restituisce HTTP/1.1 200 OK (o il tuo codice di risposta normale).
Come limito l'accesso ai percorsi di amministrazione per indirizzo IP?
Pannelli di amministrazione, dashboard di monitoraggio e endpoint di stato non dovrebbero essere raggiungibili da internet. Le direttive allow e deny di Nginx limitano l'accesso per indirizzo IP a livello di location.
location /admin {
allow 203.0.113.10; # Your office IP
allow 198.51.100.0/24; # Your VPN range
deny all;
# ... proxy_pass or other directives
}
location /nginx-status {
stub_status;
allow 127.0.0.1;
allow ::1;
deny all;
}
L'ordine conta. Nginx valuta allow e deny dall'alto verso il basso e si ferma alla prima corrispondenza. Metti deny all per ultimo.
Se il tuo IP cambia spesso, considera di limitare l'accesso tramite firewall o usa una VPN. Le ACL basate su IP in Nginx sono un secondo livello di difesa, non un sostituto dell'autenticazione.
Da un IP consentito:
curl -sI https://your-domain.com/admin
Restituisce la tua risposta normale (200, 302, ecc.).
Da un altro IP (o tramite proxy), la stessa richiesta restituisce HTTP/1.1 403 Forbidden.
Controlla il log di accesso per le richieste negate:
sudo tail -5 /var/log/nginx/access.log | grep admin
Quali limiti di buffer e timeout prevengono attacchi DoS in Nginx?
Le dimensioni dei buffer e i timeout predefiniti sono generosi. Un attaccante può sfruttarlo inviando header grandi, richieste lente o body sovradimensionati per occupare le connessioni worker. Restringere questi valori limita il danno che una singola connessione può causare.
Aggiungi in /etc/nginx/snippets/security-hardening.conf:
# Maximum allowed request body size - reject uploads larger than 10MB
client_max_body_size 10m;
# Buffer for reading client request body
client_body_buffer_size 16k;
# Buffer for reading large client headers
large_client_header_buffers 4 16k;
# Timeouts - how long Nginx waits for client data
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
| Direttiva | Valore | Troppo alto | Troppo basso |
|---|---|---|---|
client_max_body_size |
10m |
Permette upload enormi che riempiono il disco | Rompe i form di upload |
client_body_buffer_size |
16k |
Spreco memoria per connessione | Forza file temporanei per POST piccoli |
large_client_header_buffers |
4 16k |
Spreco memoria sotto attacchi header | Rompe app con cookie/URL grandi |
client_body_timeout |
30s |
Connessioni slow loris restano aperte | Disconnette utenti su reti lente |
client_header_timeout |
30s |
Come sopra | Come sopra |
send_timeout |
30s |
Occupa connessioni per client lenti | Interrompe download grandi |
keepalive_timeout |
65s |
Esaurimento del pool di connessioni | Handshake TLS aggiuntivi |
Regola client_max_body_size in base alla tua applicazione. Se gestisci upload di file, impostalo secondo le aspettative della tua app. Un valore predefinito di 10 MB è ragionevole per la maggior parte delle applicazioni web.
Ricarica e testa il limite di dimensione del body:
dd if=/dev/zero bs=1M count=11 2>/dev/null | curl -s -X POST --data-binary @- -o /dev/null -w "%{http_code}" https://your-domain.com/
Atteso: 413 (Request Entity Too Large).
Per il rate limiting e una protezione DDoS più approfondita, consulta Rate Limiting con Nginx e protezione DDoS.
Come testo la configurazione di hardening completa?
Una volta applicate tutte le modifiche, ricarica e passa in rassegna i punti chiave. Inizia con un test di configurazione e ricaricamento:
sudo nginx -t && sudo systemctl reload nginx
Controlla header e informazioni di versione con una singola richiesta:
curl -sI https://your-domain.com
L'header Server dovrebbe mostrare nginx senza numero di versione. Tutti e sei gli header di sicurezza (HSTS, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy, Permissions-Policy) dovrebbero apparire nella risposta.
Per TLS, conferma protocollo e cifratura dalla tua macchina locale:
openssl s_client -connect your-domain.com:443 </dev/null 2>/dev/null | grep -E "Protocol|Cipher"
Vuoi TLSv1.2 o TLSv1.3 con una cifratura GCM o CHACHA20.
Accedi direttamente all'IP del server per confermare che il blocco di rifiuto predefinito funziona:
curl -sk https://YOUR_SERVER_IP -o /dev/null -w "%{http_code}"
Un codice di risposta 000 significa che la connessione è stata chiusa senza risposta, che è corretto.
Prova un metodo HTTP bloccato:
curl -sI -X TRACE https://your-domain.com -o /dev/null -w "%{http_code}"
Dovrebbe restituire 403 o 405.
Per un audit esterno, invia il tuo dominio a SSL Labs (punta al grado A o A+) e a securityheaders.com per un report specifico sugli header.
Snippet di hardening completo
Il file /etc/nginx/snippets/security-hardening.conf completo con tutto quello trattato in questa guida:
# /etc/nginx/snippets/security-hardening.conf
# Nginx security hardening - include in each server block
# --- TLS Hardening (Mozilla Intermediate profile v5.7) ---
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;
# TLS session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# --- HSTS ---
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# --- Security Headers ---
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
# --- Buffer Limits ---
client_max_body_size 10m;
client_body_buffer_size 16k;
large_client_header_buffers 4 16k;
# --- Timeouts ---
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
Ricorda di impostare anche server_tokens off; nel blocco http di /etc/nginx/nginx.conf e di creare il blocco server di rifiuto predefinito separatamente.
Risoluzione dei problemi
Nginx non ricarica dopo le modifiche:
sudo nginx -t
Leggi l'errore. Indica il file e il numero di riga. Cause comuni: punto e virgola mancanti, errori di battitura nei nomi delle direttive, riferimento a un file DH param inesistente.
Header mancanti su alcuni percorsi:
La trappola dell'ereditarietà di add_header. Se un blocco location ha il proprio add_header, tutti gli header genitori vengono scartati. Includi lo snippet in quel blocco location o usa add_header_inherit merge; su Nginx 1.29.3+.
CSP blocca risorse legittime:
Apri la console sviluppatori del browser (F12). Cerca errori Refused to load. Indicano la risorsa bloccata e la direttiva CSP responsabile. Aggiungi la sorgente alla tua policy incrementalmente.
Errori nell'handshake TLS:
journalctl -u nginx -f
Osserva errori SSL durante la connessione. Verifica che la catena di certificati sia completa:
openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null 2>/dev/null | grep "Verify return code"
Atteso: Verify return code: 0 (ok).
Posizione dei log:
# Error log
sudo tail -20 /var/log/nginx/error.log
# Real-time monitoring
sudo journalctl -u nginx -f
Prossimi passi: Con il server rafforzato, configura il Rate Limiting con Nginx e protezione DDoS per gestire pattern di traffico abusivi. Per le configurazioni per sito, consulta i Nginx Server Block: ospita più domini su un VPS.
Correlati: Amministrazione di Nginx su un VPS | Struttura dei file di configurazione Nginx | Configurare Let's Encrypt SSL/TLS per Nginx su Debian 12 e Ubuntu 24.04