Aggiungi intestazioni di sicurezza con Lambda@Edge e Terraform in AWS S3/CloudFront

Pubblicato: 2021-04-24

Quando accedi a https://app.sendgrid.com per controllare i dettagli del tuo account o visiti https://mc.sendgrid.com per le campagne di marketing, stai visitando le nostre applicazioni Web front-end ospitate nei bucket AWS S3 con Distribuzioni CloudFront su di loro.

Le nostre applicazioni Web sono essenzialmente costituite da file JavaScript e CSS ridotti e suddivisi in codice, file HTML e file immagine caricati nei bucket S3 mentre CloudFront li memorizza nella cache e li fornisce ai nostri utenti. Ciascuno degli ambienti delle nostre applicazioni Web, che vanno da test, staging e produzione, ha un bucket S3 separato e una distribuzione CloudFront.

Questa infrastruttura AWS S3 e CloudFront funziona bene per le nostre applicazioni Web su larga scala per l'hosting di file su una rete di distribuzione dei contenuti, ma le nostre configurazioni iniziali mancavano di protezioni più rigide sotto forma di intestazioni di sicurezza.

L'aggiunta di queste intestazioni di sicurezza impedirebbe agli utenti di attacchi, come cross-site scripting, sniffing MIME, clickjacking, iniezione di codice e attacchi man-in-the-middle relativi a protocolli non sicuri. Se lasciati incustoditi, ciò avrebbe gravi conseguenze per i dati dei nostri clienti e per la fiducia della nostra azienda nel poter offrire un'esperienza sicura sul web.

Prima di cercare come aggiungere queste intestazioni, abbiamo fatto un passo indietro per vedere dove eravamo. Dopo aver eseguito l'URL della nostra applicazione Web attraverso un sito Web di scansione delle intestazioni di sicurezza , non sorprende che abbiamo ricevuto un voto negativo ma abbiamo visto un utile elenco di intestazioni da esaminare come mostrato di seguito.

Come puoi vedere, c'era molto margine di miglioramento. Abbiamo studiato come configurare le nostre risorse AWS S3 e CloudFront per rispondere con intestazioni di sicurezza per mitigare i rischi e le vulnerabilità menzionati.

Ad alto livello, possiamo ottenere ciò creando una funzione Lambda@Edge che altera le intestazioni della risposta di origine per aggiungere le intestazioni di sicurezza desiderate prima che i file dell'app Web tornino al browser dell'utente.

La strategia consiste prima nel testare il collegamento manuale delle cose tramite la Console AWS. Quindi, inseriremo queste configurazioni in Terraform per salvare questa parte dell'infrastruttura nel codice per riferimenti futuri e condivisibilità tra altri team e applicazioni.

Che tipo di intestazioni di sicurezza vorremmo aggiungere?

Come parte dei consigli del nostro team per la sicurezza dei prodotti, ci è stato affidato il compito di aggiungere intestazioni di sicurezza come "Strict-Transport-Security" e "X-Frame-Options". Ti consigliamo di controllare anche risorse come MDN Web Security Cheatsheet per aggiornarti. Ecco un breve riepilogo delle intestazioni di sicurezza che puoi applicare alle tue applicazioni web.

Sicurezza dei trasporti rigorosi (HSTS)

Questo serve a fornire suggerimenti al browser per accedere alla tua applicazione web tramite HTTPS anziché HTTP.

Criteri di sicurezza dei contenuti (CSP)

Questo serve per impostare elenchi consentiti espliciti sul tipo di risorse che carichi o a cui ti connetti nella tua applicazione Web, come script, immagini, stili, caratteri, richieste di rete e iframe. Questo è stato il più difficile da configurare per noi poiché avevamo script, immagini, stili ed endpoint API di terze parti da registrare in modo esplicito in queste policy.

Suggerimento: utilizza l' intestazione Content-Security-Policy-Report-Only per aiutarti con i test in determinati ambienti. Se alcune risorse hanno violato le politiche, abbiamo osservato un utile output della console delle risorse che dovevamo consentire nelle nostre politiche.

Se desideri evitare un incidente divertente con schermate vuote e app Web che non vengono caricate, ti consigliamo vivamente di sperimentare prima le tue policy in modalità di solo report ed eseguire test approfonditi prima di sentirti abbastanza sicuro da implementare queste policy di sicurezza in produzione.

Opzioni del tipo di contenuto X

Questo serve per mantenere e caricare le risorse con i tipi MIME corretti nella tua pagina web.

Opzioni X-Frame

Questo serve a fornire regole su come la tua applicazione web si carica potenzialmente in un iframe.

X-XSS-Protezione

Ciò interrompe il caricamento delle pagine se in determinati browser è rilevabile un attacco di scripting cross-site.

Referrer-Politica

Gestisce il passaggio dell'intestazione "Referrer" con le informazioni sull'origine della richiesta quando si seguono collegamenti a siti o risorse esterne.

Tenendo presenti queste intestazioni di sicurezza, torniamo a come abbiamo configurato le nostre distribuzioni CloudFront oggi e in che modo le funzioni Lambda@Edge ci aiuteranno a raggiungere il nostro obiettivo.

Utilizzo di Lambda@Edge con le nostre distribuzioni CloudFront

Per le nostre distribuzioni CloudFront, impostiamo cose come:

    • I certificati SSL per i domini da allegare sopra l'URL di CloudFront come https://app.sendgrid.com
    • Origini del secchio S3
    • Gruppi di origine con bucket primari e di replica per il failover automatico
    • Comportamenti nella cache

Questi comportamenti della cache, in particolare, ci consentono di controllare per quanto tempo desideriamo che le risposte per determinati tipi di percorsi e file vengano memorizzate nella cache negli edge server di tutto il mondo. Inoltre, i comportamenti della cache ci forniscono anche un modo per attivare le funzioni AWS Lambda in risposta ai vari eventi, come le richieste di origine e le risposte di origine. Puoi pensare alle funzioni di AWS Lambda come a un codice specifico che definisci che verrà eseguito in risposta a un determinato evento.

Nel nostro caso, possiamo modificare le intestazioni della risposta di origine prima che vengano memorizzate nella cache dai server periferici. La nostra funzione Lambda@Edge aggiungerà intestazioni di sicurezza personalizzate alla risposta di origine prima che alla fine ritorni al server perimetrale e prima che l'utente finale riceva i file JavaScript, CSS e HTML con tali intestazioni.

Abbiamo modellato il nostro approccio su questo post del blog di AWS e l'abbiamo esteso per rendere più semplice apportare modifiche a una specifica policy di sicurezza dei contenuti. Puoi modellare la tua funzione Lambda@Edge seguendo il modo in cui impostiamo le cose nella generazione di elenchi per lo script, lo stile e le origini di connessione. Questa funzione modifica efficacemente le intestazioni della risposta di origine di CloudFront e aggiunge ogni intestazione di sicurezza con determinati valori alla risposta prima di restituire chiamando la funzione di callback fornita come mostrato di seguito.

Come testiamo questa funzione Lambda@Edge?

Prima di modificare ufficialmente la modalità di restituzione delle tue risorse con le intestazioni di sicurezza, devi verificare che la funzione funzioni dopo aver configurato tutto manualmente tramite la Console AWS. È fondamentale che le tue applicazioni web siano in grado di caricarsi e funzionare correttamente con le intestazioni di sicurezza aggiunte alle risposte di rete. L'ultima cosa che vuoi sentire è un'interruzione imprevista che si verifica a causa delle intestazioni di sicurezza, quindi testale a fondo nei tuoi ambienti di sviluppo.

È anche importante sapere cosa scriverai esattamente nel codice Terraform in seguito per salvare questa configurazione nella tua base di codice. Nel caso in cui non conosci Terraform, ti fornisce un modo per scrivere e gestire la tua infrastruttura cloud attraverso il codice.

Suggerimento: dai un'occhiata ai documenti Terraform per vedere se possono aiutarti a mantenere le tue configurazioni complesse senza dover ricordare tutti i passaggi che hai eseguito nelle console cloud.

Come iniziare nella Console AWS

Iniziamo con come impostare manualmente le cose tramite la Console AWS.

  1. Innanzitutto, devi creare la funzione Lambda@Edge nella regione "us-east-1". Andando alla pagina dei servizi Lambda, faremo clic su "Crea funzione" e la chiameremo qualcosa come "testSecurityHeaders1".

2. È possibile utilizzare un ruolo esistente con autorizzazioni per eseguire la funzione sui server perimetrali oppure è possibile utilizzare uno dei loro modelli di policy del ruolo, ad esempio "Autorizzazioni Lambda di base@Edge..." e denominarlo "lambdaedgeroletest".

3. Dopo aver creato la funzione e il ruolo Lambda di prova, dovresti vedere qualcosa di simile in cui noterai il pulsante "Aggiungi trigger" per la funzione. È qui che alla fine assocerai Lambda al comportamento della cache di una distribuzione CloudFront attivato sull'evento di risposta di origine.

4. Successivamente, è necessario modificare il codice della funzione con il codice delle intestazioni di sicurezza che abbiamo creato in precedenza e premere "Salva".

5. Dopo aver salvato il codice della funzione, testiamo se la tua funzione Lambda funziona anche scorrendo verso l'alto e premendo il pulsante "Test". Creerai un evento di test denominato "samplecloudfrontresponse" utilizzando il modello di evento "cloudfront-modify-response-header" per deridere un evento di risposta dell'origine CloudFront effettivo e per vedere come viene eseguita la tua funzione su di esso.

Noterai cose come l'oggetto header "cf.response" che il codice della tua funzione Lambda modificherà.

6. Dopo aver creato l'evento di test, fai nuovamente clic sul pulsante "Test" e dovresti vedere come è stata eseguita la funzione Lambda su di esso. Dovrebbe essere eseguito correttamente con i log che mostrano la risposta risultante con intestazioni di sicurezza aggiunte come questa.

Ottimo, la funzione Lambda sembra aver aggiunto correttamente le intestazioni di sicurezza alla risposta!

7. Torniamo all'area "Designer" e facciamo clic sul pulsante "Aggiungi trigger" in modo da poter associare la funzione Lambda ai comportamenti della cache della distribuzione CloudFront sull'evento di risposta di origine. Assicurati di selezionare un trigger "CloudFront" e fai clic sul pulsante "Deploy to Lambda@Edge".

8. Successivamente, seleziona la distribuzione CloudFront (nel nostro esempio, abbiamo cancellato l'input qui per motivi di sicurezza) e un comportamento della cache da associare ad essa.

Quindi scegli il comportamento della cache "*" e seleziona l'evento "Risposta origine" in modo che corrisponda a tutti i percorsi di richiesta alla tua distribuzione CloudFront e per assicurarti che la funzione Lambda venga sempre eseguita per tutte le risposte di origine.

Quindi spunta il riconoscimento prima di fare clic su "Distribuisci" per distribuire ufficialmente la tua funzione Lambda.

9. Dopo aver associato correttamente la tua funzione Lambda a tutti i comportamenti della cache della tua distribuzione CloudFront rilevanti, dovresti vedere qualcosa di simile nell'area "Designer" del dashboard Lambda dove puoi vedere i trigger di CloudFront e avere la possibilità di visualizzarli o eliminarli.

Apportare modifiche al codice Lambda

Ogni volta che potrebbe essere necessario apportare modifiche al codice Lambda, ti consigliamo di:

    1. Pubblica una nuova versione tramite il pulsante a discesa "Azioni".
    2. Elimina i trigger sulla versione precedente (puoi fare clic sul menu a discesa "Qualificatori" per vedere tutte le versioni della tua Lambda)
    3. Associa i trigger al numero di versione più recente pubblicato di recente

Dopo aver distribuito Lambda per la prima volta o dopo aver pubblicato una nuova versione di Lambda e aver associato i trigger alla versione Lambda più recente, potresti non vedere subito le intestazioni di sicurezza nelle risposte per la tua applicazione web. Ciò è dovuto al modo in cui i server perimetrali in CloudFront memorizzano nella cache le risposte. A seconda di quanto tempo imposti il ​​tempo di vita nei comportamenti della cache, potrebbe essere necessario attendere un po' per vedere le nuove intestazioni di sicurezza a meno che tu non esegua un invalidamento della cache nella distribuzione CloudFront interessata.

Dopo aver ridistribuito le modifiche alla tua funzione Lambda, spesso ci vuole tempo per svuotare la cache (a seconda delle impostazioni della cache di CloudFront) prima che le tue risposte abbiano le ultime modifiche alle intestazioni di sicurezza.

Suggerimento: per evitare di aggiornare molto la pagina o di rimanere incerto se le modifiche hanno funzionato, avvia un'invalidazione della cache di CloudFront per accelerare il processo di svuotamento della cache in modo da poter vedere le intestazioni di sicurezza aggiornate.

Vai alla pagina dei tuoi servizi CloudFront, attendi che lo stato della tua distribuzione CloudFront venga distribuito, il che significa che tutte le associazioni lambda sono complete e distribuite, e vai alla scheda "Invalidazioni". Fai clic su "Crea invalidazione" e metti "/*" come percorso dell'oggetto per invalidare tutte le cose nella cache e premi "Invalida". L'operazione non dovrebbe richiedere troppo tempo e, una volta completata, l'aggiornamento dell'applicazione Web dovrebbe visualizzare le ultime modifiche all'intestazione di sicurezza.

Mentre ripeti le intestazioni di sicurezza in base a ciò che trovi come violazioni o errori nella tua applicazione web, puoi ripetere questo processo:

    • Pubblicazione di una nuova versione della funzione Lambda
    • Eliminazione dei trigger sulla vecchia versione Lambda
    • Associazione dei trigger alla nuova versione
    • Cache che invalida la tua distribuzione CloudFront
    • Testare la tua applicazione web
    • Ripetendo fino a quando non ti senti sicuro e al sicuro, le cose funzionano come previsto senza pagine vuote, richieste API non riuscite o errori di sicurezza della console

Una volta che le cose sono stabili, puoi facoltativamente passare a Terraforming come hai appena fatto manualmente nelle configurazioni del codice, supponendo che Terraform sia integrato con i tuoi account AWS. Non tratteremo come impostare Terraform dall'inizio, ma ti mostreremo frammenti di come apparirà il codice Terraform.

Terraforming di Lambda@Edge attivato dalla nostra distribuzione CloudFront

Dopo aver eseguito l'iterazione sulla funzione Lambda@Edge per le intestazioni di sicurezza nella regione "us-east-1", abbiamo voluto aggiungerla alla nostra base di codice Terraform per la manutenibilità del codice e il controllo della versione in futuro.

Per tutti i comportamenti della cache che abbiamo già implementato, abbiamo dovuto associare il comportamento della cache alla funzione Lambda@Edge, che l'evento di risposta dell'origine ha attivato.

I passaggi seguenti presuppongono che tu abbia già la maggior parte delle distribuzioni CloudFront e dei bucket S3 configurati tramite Terraform. Ci concentreremo sui moduli e sulle proprietà principali relativi a Lambda@Edge e aggiungeremo il trigger ai comportamenti della cache della distribuzione CloudFront. Non illustreremo come configurare i tuoi bucket S3 e altre impostazioni di distribuzione di CloudFront da zero tramite Terraform, ma speriamo che tu possa vedere il livello di impegno per raggiungere questo obiettivo da solo.

Attualmente suddividiamo le nostre risorse AWS in cartelle di moduli separate e passiamo le variabili in quei moduli per flessibilità nella nostra configurazione. Abbiamo una cartella apply con una sottocartella di development e production e ognuna ha il proprio file main.tf in cui chiamiamo questi moduli con determinate variabili di input per creare un'istanza o modificare le nostre risorse AWS.

Queste sottocartelle hanno anche ciascuna una cartella lambdas in cui conserviamo il nostro codice Lambda, ad esempio un file security_headers_lambda.js . security_headers_lambda.js ha lo stesso codice che abbiamo utilizzato nella nostra funzione Lambda durante il test manuale, tranne per il fatto che lo stiamo anche salvando nella nostra base di codice per poterlo comprimere e caricare tramite Terraform.

1. Innanzitutto, abbiamo bisogno di un modulo riutilizzabile per comprimere il nostro file Lambda prima che venga caricato e pubblicato come un'altra versione della nostra funzione Lambda@Edge. Questo prende in un percorso alla nostra cartella Lambda che contiene l'eventuale funzione Lambda Node.js.

2. Successivamente, aggiungiamo al nostro modulo CloudFront esistente, che racchiude i bucket S3, le policy e le risorse di distribuzione CloudFront creando anche una risorsa Lambda creata dal file Lambda compresso. Poiché gli output del modulo zip Lambda passano come variabili nel modulo CloudFront per configurare la risorsa Lambda, è necessario specificare la regione del provider AWS come "us-east-1" e con una policy del ruolo funzionante come questa.

3. All'interno del modulo CloudFront, associamo quindi questa funzione Lambda@Edge ai comportamenti della cache della distribuzione CloudFront, come illustrato di seguito.

4. Infine, mettendo tutto insieme nel file main.tf della nostra cartella di apply/development o apply/production , chiamiamo tutti questi moduli e inseriamo gli output appropriati come variabili nel nostro modulo CloudFront come mostrato qui.

Queste modifiche alla configurazione si occupano essenzialmente dei passaggi manuali che abbiamo eseguito nella sezione precedente per aggiornare il codice Lambda e associare la versione più recente ai comportamenti della cache di CloudFront e ai trigger per gli eventi di risposta dell'origine. Woohoo! Non è necessario eseguire o ricordare i passaggi della Console AWS purché applichiamo queste modifiche alle nostre risorse.

Come possiamo implementarlo in sicurezza in ambienti diversi?

Quando abbiamo associato per la prima volta la nostra funzione Lambda@Edge alla nostra distribuzione CloudFront di test, abbiamo notato rapidamente come la nostra applicazione Web non si caricasse più correttamente. Ciò è dovuto principalmente al modo in cui abbiamo implementato la nostra intestazione Content-Security-Policy e al modo in cui non copriva tutte le risorse che abbiamo caricato nella nostra applicazione. Le altre intestazioni di sicurezza rappresentavano un rischio minore in termini di blocco del caricamento della nostra applicazione. In futuro, ci concentreremo sull'implementazione delle intestazioni di sicurezza con ulteriori iterazioni in mente per ottimizzare l'intestazione Content-Security-Policy.

Come accennato in precedenza, abbiamo scoperto come possiamo sfruttare l' intestazione Content-Security-Policy-Report-Only invece per ridurre al minimo il rischio mentre raccogliamo più domini di risorse da aggiungere in ciascuna delle nostre politiche.

In questa modalità di solo rapporto, i criteri continueranno a essere eseguiti nel browser e nella console di output messaggi di errore relativi a eventuali violazioni dei criteri. Tuttavia, non bloccherà completamente quegli script e fonti, quindi la nostra applicazione Web può ancora funzionare come al solito. Spetta a noi continuare a esaminare l'intera applicazione Web per essere sicuri di non perdere nessuna fonte importante nelle nostre politiche, altrimenti influenzerà negativamente i nostri clienti e il team di supporto.

Per ogni ambiente, puoi implementare le intestazioni di sicurezza Lambda come segue:

    • Pubblica le modifiche al tuo Lambda manualmente o tramite un piano Terraform e applica prima le modifiche all'ambiente con altre intestazioni di sicurezza e l'intestazione Content-Security-Policy-Report-Only.
    • Attendi che lo stato della tua distribuzione CloudFront sia completamente distribuito con la Lambda associata ai comportamenti della cache.
    • Esegui un'invalidazione della cache sulla tua distribuzione CloudFront se le precedenti intestazioni di sicurezza vengono ancora visualizzate o se le modifiche correnti vengono visualizzate nel browser impiega troppo tempo.
    • Visita e visita le pagine della tua applicazione web con gli strumenti per sviluppatori aperti, scansionando la console per eventuali messaggi di errore della console "Solo report..." per migliorare la tua intestazione Content-Security-Policy.
    • Apporta modifiche al codice Lambda per tenere conto delle violazioni segnalate.
    • Ripeti dal primo passaggio finché non ti senti abbastanza sicuro da modificare l'intestazione da Content-Security-Policy-Report-Only a Content-Security-Policy, il che significa che l'ambiente lo applicherà.

Miglioramento del punteggio delle nostre intestazioni di sicurezza

Dopo aver applicato con successo le modifiche di Terraform ai nostri ambienti e aver invalidato le cache di CloudFront, abbiamo aggiornato le pagine nella nostra applicazione web. Abbiamo tenuto aperti gli strumenti di sviluppo per vedere le intestazioni di sicurezza, come HSTS, CSP e altre nelle nostre risposte di rete, come le intestazioni di sicurezza mostrate di seguito.

Abbiamo anche eseguito la nostra applicazione web attraverso un rapporto di scansione delle intestazioni di sicurezza come quello su questo sito . Di conseguenza, abbiamo assistito a grandi miglioramenti (una valutazione A!) rispetto a un voto precedentemente negativo e puoi ottenere miglioramenti simili dopo aver modificato le tue configurazioni S3/CloudFront per avere le intestazioni di sicurezza in atto.

Andando avanti con le intestazioni di sicurezza

Dopo aver impostato manualmente le intestazioni di sicurezza tramite la Console AWS o aver effettuato correttamente il Terraforming della soluzione e aver applicato le modifiche a ciascuno dei tuoi ambienti, ora hai un'ottima base per eseguire ulteriori iterazioni e migliorare le intestazioni di sicurezza esistenti in futuro.

A seconda dell'evoluzione dell'applicazione Web, potrebbe essere necessario rendere l'intestazione Content-Security-Policy più specifica in termini di risorse consentite per una maggiore sicurezza. In alternativa, potrebbe essere necessario aggiungere una nuova intestazione interamente per uno scopo separato o per riempire un'altra falla nella sicurezza.

Con queste modifiche future alle intestazioni di sicurezza nelle funzioni Lambda@Edge, puoi seguire strategie di rilascio simili per ambiente per assicurarti che le tue applicazioni Web siano protette da attacchi dannosi sul Web e funzionino ancora senza che gli utenti si accorgano della differenza.

Per altri articoli scritti da Alfred Lucero, vai alla pagina dell'autore del suo blog: https://sendgrid.com/blog/author/alfred/