Per anni la sicurezza applicativa è stata un accumulo di pezze sparse: un rate limiter dentro il backend, qualche header impostato a mano nel middleware, una lista di IP in un file di config, un check di autenticazione replicato in tre servizi. Funziona finché non devi cambiare origine — da Railway/Vercel ad AWS, per dire — e scopri che metà delle tue difese sono incollate al runtime che stai buttando via.
La tesi di questo articolo è semplice: il perimetro è il primo punto di controllo, e conviene concentrarci CDN, WAF, rate limiting, security headers e accessi Zero Trust invece di spargerli nell’app. Racconto come l’ho fatto davvero, migrando uno stack di produzione dietro Cloudflare, con i trabocchetti che nei tutorial non trovi.
Un edge davanti a origini eterogenee
Il punto di forza di Cloudflare non è la CDN in sé, è che diventa l’unico layer davanti a origini diverse. Nel mio caso avevo (e in parte ho ancora) un mix: parte del traffico su un ALB in AWS, parte su Vercel durante la transizione. Mettere WAF, cache, rate limiting e Zero Trust Access all’edge significa che la policy di sicurezza vive in un solo posto, versionata, indipendente da quale runtime sta dietro.
Ma “davanti a tutto” ha una condizione tecnica che è la prima trappola da capire.
Proxied (orange) vs grey-cloud: la trappola del doppio proxy
WAF, rate limiting e transform rules bitano solo sugli hostname proxati (orange-cloud). Se un record DNS è in grey-cloud (DNS-only), Cloudflare risolve e basta: le tue regole non lo vedono nemmeno.
Il caso concreto: l’endpoint API e il dominio apex sono orange, quindi protetti. Il frontend, servito da Vercel, resta un CNAME in grey-cloud. Motivo hard: mettere orange davanti a Vercel significa doppio proxy, e il risultato sono errori SSL 521/525 con l’app rotta. Il flip a orange è possibile solo dopo che l’origine diventa AWS/ALB con un certificato valido.
Da qui la tecnica che uso e che quasi nessuno spiega: per proteggere una rotta servita da un frontend grey-cloud, non metti la regola sulla pagina — è invisibile all’edge — punti l’API backend che è proxata. Le rotte pubbliche “shared” del frontend chiamano comunque l’endpoint API (proxato); mettendo il rate limit lì copro di fatto la rotta pubblica senza toccare Vercel.
WAF: il ciclo log→block, e gli override chirurgici
Attivare un WAF managed in block dal primo minuto è il modo migliore per rompere la produzione con i tuoi stessi falsi positivi. Il ciclo che seguo è sempre lo stesso:
- Deploy del Managed Ruleset (+ OWASP Core) come ruleset di zona, in modalità log.
- Osservazione di 24-48h di traffico reale.
- Flip a block — ma tenendo in
logsolo le singole regole che generano falsi positivi.
La parte che fa la differenza è il punto 3. Quando una regola colpisce un endpoint legittimo, la tentazione è disabilitarla. È l’errore classico e pericoloso: apri un buco globale per risolvere un caso locale. La cosa giusta è l’override per-regola-id: mantieni il block globale e degradi a log la singola rule.
Due esempi veri dal mio deploy: una regola XSS-Script-Tag scattava su un endpoint di upload documenti legittimo (i documenti contengono markup), e una regola React-RCE colpiva i beacon di analytics. Entrambe finite in override action="log" per quel rule id, con il resto del ruleset che continua a bloccare tutto.
overrides {
rules {
id = "<rule-id-xss-script-tag>"
action = "log" # FP su un endpoint di upload legittimo, non disabilitare il ruleset
}
}
Nota di catalogo: nella lista /rulesets i ruleset kind=managed sono il catalogo Cloudflare (OWASP/DDoS) e non vanno importati in Terraform; gestisci solo quelli kind=zone con la tua config.
Rate limiting: mirror dell’app, e il muro dei 600 secondi
Le mie regole di rate limiting all’edge sono nate come mirror dei rate limiter applicativi già presenti nel backend: auth sull’endpoint, rotte shared pubbliche in lettura, download di documenti con soglia più stretta, dettaglio proposta. Una volta che la regola vive all’edge e la vedo bloccare correttamente, il rate limiter in-codice diventa ridondante e lo posso rimuovere. Meno codice, meno stato applicativo, difesa spostata più a monte.
Il trabocchetto che ti fa perdere un pomeriggio: senza l’add-on Advanced Rate Limiting il periodo massimo è 600 secondi. Provi a mettere una finestra da un’ora e l’API ti risponde con un criptico not entitled to use the period 3600. Soluzione: finestre ≤ 10 minuti, oppure l’add-on. Non è un bug tuo, è entitlement.
Zero Trust Access self-hosted
Per le applicazioni interne e i tool uso Cloudflare Access con policy riusabili, referenziate per [{id, precedence}] e non scritte inline: così una policy la definisci una volta e la riferisci ovunque. IdP OAuth esterno (Google) referenziato per id in allowed_idps, perché il secret è write-only e non si importa pulito.
Un paio di note dal campo con il provider v5: non combinare self_hosted_domains con destinations — in v5 usi destinations, altrimenti ottieni Invalid Attribute Combination. E la session_duration: l’API la restituisce a null ma il provider forza il default a 24h, quindi mettila esplicita se ti serve altro.
Punto di igiene operativa che ripeto sempre: i bypass temporanei per un pentest (tipo “skip WAF su staging per la finestra del test”) vanno tracciati come issue e rimossi a fine pentest. Un bypass dimenticato è esattamente il tipo di debito che ti si ritorce contro sei mesi dopo.
Security posture e TLS: strict solo dopo il cert
Min TLS lo porto sempre da 1.0 a 1.2. La modalità SSL invece la lascio a full e passo a strict solo dopo il cutover sull’ALB con certificato ACM valido e in uso da un listener. Fare strict prima che l’origine abbia un cert buono significa segarsi i piedi da soli. Security headers e transform rules stanno anch’essi all’edge, non nel middleware applicativo.
Tutto as-code, apply manuale
La config Cloudflare la gestisco in Terraform/Terragrunt con provider ~>5, state in un bucket dedicato fuori dalla pipeline CI. L’import è a zero-destroy con cf-terraforming (attenzione ai flag -a per Access e -z per DNS/ruleset di zona, sono mutuamente esclusivi). E soprattutto: apply manuale via SSO, non auto-approve — qui si tocca traffico live, quindi plan-file e apply del plan-file, sempre. WAF managed attivo sulla zona di produzione; la zona di staging/tooling ha un suo ciclo log→block separato, non lo stesso.
Difesa in profondità: edge + DB + app
L’edge è il primo controllo, non l’unico. Sotto ci sta il layer dati: nel mio rlsgrid tengo matrici di test RLS e un fuzzer cross-tenant per Postgres/Supabase, perché il perimetro non ti salva da un tenant che legge i dati di un altro. E lato bot, con ai-crawler-bots governo all’edge quali crawler AI (GPTBot, ClaudeBot, PerplexityBot) leggono il sito. Perimetro, database e applicazione: tre anelli, non uno.
Takeaway
- Concentra le difese all’edge: cambiare runtime non deve significare riscrivere la sicurezza.
- WAF sempre
log→block, e per i falsi positivi override per-regola-id, mai disable del ruleset. - Le regole edge bitano solo su host proxati: dietro Vercel resta grey-cloud, e proteggi l’API backend, non la pagina FE.
- Rate limit edge come mirror dell’app → poi rimuovi il codice ridondante; ricorda il muro dei 600s.
stricte bypass di pentest: il primo solo dopo un cert valido, i secondi tracciati e rimossi.- Config as-code, ma apply manuale sul traffico live. L’automazione totale qui è un lusso che non ti puoi permettere.