Configurare CSP, HSTS e X-Frame-Options sul tuo hosting (dopo un voto basso su Observatory)
Mozilla Observatory ti ha dato una F? Ecco dove e come inserire fisicamente gli header di sicurezza base su Nginx, Apache, Node.js, Vercel e Cloudflare.
Hai appena analizzato il tuo nuovo sito su Mozilla Observatory (o Security Headers) e il risultato è una spietata F. Inserisci il report in un’IA per chiedere come risolvere, e la risposta tipica (nei casi che ho visto) è: configura una Content Security Policy (CSP), attiva HSTS e blocca gli iframe.
Tutto corretto in teoria. Ma l’IA può fermarsi al cosa scrivere, dimenticando il dove metterlo in pratica.
In base a come stai ospitando il sito, quegli header vanno in file o pannelli completamente diversi. In questa guida ti mostro dove incollare queste difese sugli stack più comuni: hosting Apache (anche da Windows via File Manager/FTP), Vercel, Cloudflare, Nginx su VPS e Node.js/Express.
ℹ️ Nota / Prerequisito operativo
Ti serve almeno uno di questi accessi: pannello hosting / File Manager o FTP (Apache.htaccess), dashboard Vercel o Cloudflare, SSH al VPS (Nginx), oppure il codice sorgente Express. Fai sempre un backup del file di configurazione (o una copia del.htaccess) prima di modificarlo.
HSTS solo se HTTPS funziona già su tutto il dominio (e, se usiincludeSubDomains, anche su tutti i sottodomini). Altrimenti rischi di rendere il sito irraggiungibile da browser che hanno già memorizzato la policy.
Se stai ancora al punto “come leggo il voto Observatory”, la guida all’audit di sicurezza gratuito resta una lettura opzionale: qui do per scontato che tu abbia già il report e voglia applicare gli header.
I tre header di sicurezza minimi
Prima di toccare il server, definisco un set di base stringente ma realistico. Se applichi questi tre (più X-Content-Type-Options, che Observatory valuta spesso), il punteggio di solito sale in modo netto — senza garantire di colpo una A+ su ogni sito.
| Header HTTP | Impatto | Cosa fa e policy consigliata in questa guida |
|---|---|---|
| Content-Security-Policy (CSP) Difesa contro XSS | Alto rischio rottura | Impedisce al browser di caricare script esterni o inline non autorizzati. Parti da default-src 'self' (e direttive sorelle sotto). |
| Strict-Transport-Security (HSTS) Forza l'uso di HTTPS | Stabile | Dice al browser di non usare più HTTP in chiaro per questo host. Usa max-age=31536000; includeSubDomains solo se tutti i sottodomini hanno HTTPS valido. |
| X-Frame-Options Previene il Clickjacking | Stabile | Impedisce che altri siti incorporino le tue pagine in un <iframe>. Usa DENY. Nei browser moderni conta anche frame-ancestors 'none' nella CSP: meglio tenerli entrambi. |
Negli esempi aggiungo anche X-Content-Type-Options: nosniff, che riduce lo sniffing del Content-Type e compare spesso nei report Observatory.
🚨 Avviso critico — CSP e stili/script inline
La policydefault-src 'self'(conscript-src/style-srcristretti a'self') blocca script di terze parti (es. analytics) e qualsiasi stile o script inline (style="...",<script>...</script>nell’HTML, e spesso anche CSS/JS generati “a caldo” dai framework). Se il layout o le funzioni si rompono, allenta solo le direttive necessarie (domini o hash/nonce), evitandounsafe-inlinese puoi.
Ecco dove applicarli, partendo dagli ambienti più comuni da PC Windows (File Manager, dashboard, Vercel) e chiudendo con VPS Nginx e Express.
Passaggio 1 — Apache e .htaccess (hosting condiviso)
Su hosting classici (Aruba, SiteGround, Hostinger e simili) basati su Apache spesso non hai i file di sistema, ma puoi modificare il file .htaccess nella cartella principale del sito (document root: di solito public_html, www o htdocs).
ℹ️ Nota / Prerequisito operativo
Serve il modulo Apachemod_headers. Quasi tutti gli hosting condivisi lo hanno già attivo. Se dopo il salvataggio il sito va in errore 500, ripristina il backup e chiedi al supporto semod_headersè abilitato.
Dove trovare e modificare .htaccess (Windows)
- Accedi al pannello del provider (es. cPanel, Plesk, pannello Aruba).
- Apri File Manager (Gestione file) e entra nella cartella root del sito.
- Se non vedi
.htaccess, attiva “mostra file nascosti” / “Show hidden files” (i file che iniziano con il punto spesso sono nascosti). - Se il file non esiste, creane uno nuovo chiamato esattamente
.htaccess(con il punto iniziale). - Alternativa FTP (Windows): con FileZilla o WinSCP connettiti in SFTP/FTP, scarica
.htaccesssul PC (es.C:\Users\TUONOME\Desktop\htaccess-backup\), modifica con Blocco note o VS Code, poi ricarica sul server.
Dove trovare e modificare .htaccess (macOS / Linux)
Stesso flusso sul File Manager del provider, oppure via SFTP (scp / FileZilla). Su Mac il percorso locale tipico del backup è /Users/tuonome/Desktop/htaccess-backup/; su Linux /home/tuonome/....
Aggiungi questo blocco in cima al .htaccess (dopo eventuali direttive già presenti, se preferisci, ma prima di regole che potrebbero interferire):
# Forza HTTPS nei browser che visitano già in HTTPS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Previene Clickjacking
Header always set X-Frame-Options "DENY"
# Disabilita sniffing del Content-Type
Header always set X-Content-Type-Options "nosniff"
# CSP basilare (solo risorse dallo stesso dominio)
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; form-action 'self';"
Le modifiche al .htaccess di solito hanno effetto immediato: ricarica il sito e verifica gli header (Passaggio 6).
Passaggio 2 — Vercel (vercel.json)
Se pubblichi su Vercel (React/Next.js o statico), gli header statici si dichiarano in un file vercel.json nella root del progetto (accanto a package.json).
Su Windows
- Apri il progetto in Esplora file o in VS Code (
C:\Users\TUONOME\progetti\tuosito\o simile). - Crea il file
vercel.jsonse non c’è. - Incolla la configurazione sotto, salva, poi fai deploy (
vercelCLI oppure push su Git collegato a Vercel).
Su macOS / Linux
Stesso file nella root del repo (/Users/... o /home/...). Deploy identico.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; form-action 'self';"
}
]
}
]
}
Dopo il deploy, controlla che gli header compaiano sulla risposta HTTPS di produzione (non solo in anteprima locale, se il tuo setup locale non passa da Vercel).
Passaggio 3 — Cloudflare (Transform Rules + HSTS dedicato)
Se il dominio passa dal proxy Cloudflare (nuvola arancione attiva), puoi aggiungere gli header a bordo Cloudflare, prima che la risposta arrivi al visitatore — utile anche se l’origin è debole o non lo controlli del tutto.
Passi tipici nella dashboard (da browser su Windows, Mac o Linux):
- Entra in dash.cloudflare.com e seleziona il dominio.
- Vai su Rules → Transform Rules.
- Apri la scheda Modify Response Header.
- Crea una regola (es. “Security headers”) con filtro su tutte le richieste del tuo host, ad esempio
(http.host eq "tuosito.com")— adatta il dominio. - Per ogni header usa Set static:
Content-Security-Policy→ valore della policy sottoX-Frame-Options→DENYX-Content-Type-Options→nosniff
- Salva e abilita la regola.
Valore CSP di partenza (stesso della guida):
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; form-action 'self';
💡 Consiglio pratico — HSTS su Cloudflare
Per Strict-Transport-Security preferisci il menu dedicato: SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS). Attivalo solo con HTTPS già ok; parti con unmax-agealto solo quando sei sicuro. Evita di duplicare HSTS anche nelle Transform Rules se l’hai già impostato lì.
Passaggio 4 — Nginx (VPS Linux)
Se gestisci un VPS Ubuntu/Debian con Nginx, gli header vanno nel blocco server HTTPS (porta 443), di solito in /etc/nginx/sites-available/tuosito.com (il nome del file dipende da come hai configurato il sito).
Connessione da Windows
- Apri Windows Terminal o Prompt dei comandi.
- Connettiti in SSH (OpenSSH è incluso nelle versioni recenti di Windows):
ssh utente@IP_DEL_SERVER
(In alternativa: PuTTY.) Poi sul server:
sudo cp /etc/nginx/sites-available/tuosito.com /etc/nginx/sites-available/tuosito.com.bak
sudo nano /etc/nginx/sites-available/tuosito.com
Da macOS / Linux
ssh utente@IP_DEL_SERVER
sudo cp /etc/nginx/sites-available/tuosito.com /etc/nginx/sites-available/tuosito.com.bak
sudo nano /etc/nginx/sites-available/tuosito.com
Nel blocco server che ascolta su 443, aggiungi:
server {
listen 443 ssl;
server_name tuosito.com;
# Altre configurazioni SSL...
# --- INIZIO HEADER DI SICUREZZA ---
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; form-action 'self';" always;
add_header X-Content-Type-Options "nosniff" always;
# --- FINE HEADER DI SICUREZZA ---
location / {
try_files $uri $uri/ =404;
}
}
La parola chiave always è importante: Nginx invia questi header anche sulle risposte di errore (es. 404), non solo sulle 200.
Poi, ancora sulla macchina Linux del VPS:
sudo nginx -t
sudo systemctl reload nginx
Se nginx -t segnala un errore di sintassi, non fare reload: correggi il file (o ripristina il .bak) e ritesta.
Passaggio 5 — Node.js ed Express (Helmet)
Se il backend è Express e non c’è già un reverse proxy che imposta gli header, configurali nell’app. Il modo più robusto è la libreria Helmet.
Installazione (Windows)
Nella cartella del progetto (C:\Users\TUONOME\progetti\mia-app\):
npm install helmet
Output atteso tipico: riga added … packages e helmet presente in package.json → dependencies.
Verifica rapida:
npm list helmet
Installazione (macOS / Linux)
Stessi comandi npm install helmet e npm list helmet dal Terminale, nella root del progetto.
Poi nel file principale (es. server.js o app.js):
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:'],
frameAncestors: ["'none'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
},
},
// Allineato a X-Frame-Options: DENY
frameguard: { action: 'deny' },
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
})
);
app.get('/', (req, res) => {
res.send('Server sicuro in funzione.');
});
app.listen(3000);
Helmet imposta anche altri header minori (es. X-Content-Type-Options). Se davanti a Express c’è già Nginx/Cloudflare con gli stessi header, evita duplicati contraddittori: una sola “fonte di verità” per policy è più facile da mantenere.
Passaggio 6 — Verifica in browser e nuovo scan Observatory
- Apri il sito in HTTPS (Chrome o Edge su Windows:
F12→ scheda Network / Rete; su Mac lo stesso in Chrome/Firefox, o Safari → Sviluppo). - Ricarica la homepage, seleziona il documento HTML, apri Headers e cerca
content-security-policy,strict-transport-security,x-frame-options. - Apri anche la Console: errori rossi legati alla CSP indicano risorse bloccate (font Google, Stripe, analytics, inline). Annota l’URL bloccato e aggiungilo alla direttiva giusta (
script-src,style-src,font-src,connect-src, ecc.). - Torna su observatory.mozilla.org, inserisci il dominio e usa Force a Rescan (altrimenti puoi vedere risultati in cache).
Un salto da F a un voto nettamente migliore è frequente quando prima mancavano proprio questi header; il grado finale dipende anche da cookie, TLS e altri controlli del report.
I limiti: cosa la CSP non risolve
Ottenere una A+ su Mozilla Observatory fa bella figura, ma non rende il sito invulnerabile.
La CSP riduce l’impatto di molti attacchi XSS, perché limita da dove il browser può eseguire script e verso dove può parlare. Non sostituisce la sanitizzazione / escaping dell’input sul server: se stampi HTML crudo dall’utente, un attaccante può comunque alterare la pagina (defacement) restando dentro le risorse consentite da 'self'. Serve difesa in profondità: input pulito e CSP stringente.
Domande Frequenti (FAQ)
Cosa sono CSP, HSTS e X-Frame-Options in una frase ciascuno?
La CSP dice al browser da dove può caricare script, stili e connessioni. HSTS obbliga HTTPS dopo la prima visita sicura. X-Frame-Options (o frame-ancestors in CSP) riduce il rischio di clickjacking impedendo che il sito sia incorporato in iframe non autorizzati.
Dove inserisco questi header su hosting condiviso, Vercel o Cloudflare?
Su Apache spesso in .htaccess; su Nginx nel blocco server; su Vercel in vercel.json; su Cloudflare con Transform Rules (e HSTS dedicato nel pannello SSL/TLS). Su Node/Express puoi usare Helmet. L’importante è che compaiano nelle risposte HTML del dominio, non solo in un CDN “di prova”.
Perché Mozilla Observatory mi dà ancora una F dopo aver aggiunto gli header?
Possibili cause: cache del report (usa Force a Rescan), header solo su alcune route, CSP troppo permissiva (unsafe-inline ovunque), cookie senza flag sicuri, o TLS/redirect HTTP→HTTPS ancora deboli. Controlla in DevTools → Network che gli header siano presenti sulla homepage.
Cosa faccio se la Content-Security-Policy “rompe” CSS o script del sito?
Apri la Console: il messaggio indica quale direttiva ha bloccato quale URL. Aggiungi solo quel dominio (o uno schema https:) alla direttiva citata. Evita di tornare a * o a unsafe-inline globale; per script inline inevitabili preferisci nonce o hash.
Una buona CSP rende il sito invulnerabile agli XSS?
No. Riduce l’impatto di molti XSS limitando da dove partono gli script, ma non sostituisce escaping/sanitizzazione dell’HTML lato server. Serve difesa in profondità: input pulito e header stringenti.
Cosa fare se la CSP “rompe” il sito
- Leggi in Console il messaggio
Refused to load … because it violates the following Content Security Policy directive…. - Aggiungi solo quel dominio (o uno schema
https:) alla direttiva citata. - Preferisci nonce o hash per script inline inevitabili, invece di
unsafe-inlineglobale. - Dopo ogni allentamento, ripeti Network + Observatory.
Quando gli errori di Console sono sotto controllo e gli header compaiono nella risposta, puoi chiudere il cerchio sull’audit header e passare ad altri punti del report (cookie, TLS, redirect HTTP→HTTPS) con più calma.