Adicione cabeçalhos de segurança com Lambda@Edge e Terraform no AWS S3/CloudFront
Publicados: 2021-04-24Ao fazer login em https://app.sendgrid.com para verificar os detalhes da sua conta ou visitar https://mc.sendgrid.com para campanhas de marketing, você está visitando nossos aplicativos web de front-end hospedados em buckets do AWS S3 com Distribuições do CloudFront em cima deles.
Nossos aplicativos da web consistem essencialmente em arquivos JavaScript e CSS reduzidos e divididos por código, arquivos HTML e arquivos de imagem carregados em buckets do S3 como caches do CloudFront e os exibe para nossos usuários. Cada um dos ambientes de nosso aplicativo da web, desde teste, preparação e produção, tem um bucket S3 separado e uma distribuição do CloudFront.
Essa infraestrutura AWS S3 e CloudFront funciona bem para nossos aplicativos da web em escala na hospedagem de arquivos em uma rede de entrega de conteúdo, mas nossas configurações iniciais careciam de proteções mais rígidas na forma de cabeçalhos de segurança.
A adição desses cabeçalhos de segurança impediria os usuários de ataques, como scripts entre sites, detecção de MIME, clickjacking, injeção de código e ataques man-in-the-middle relacionados a protocolos inseguros. Se não forem atendidos, isso teria graves consequências para os dados de nossos clientes e para a confiança de nossa empresa em oferecer uma experiência segura na web.
Antes de pesquisarmos como adicionar esses cabeçalhos, primeiro demos um passo para trás para ver onde estávamos. Depois de executar o URL do nosso aplicativo da Web por meio de um site de verificação de cabeçalhos de segurança , sem surpresa recebemos uma nota reprovada, mas vimos uma lista útil de cabeçalhos a serem examinados, conforme mostrado abaixo.
Como você pode ver, havia muito espaço para melhorias. Pesquisamos como configurar nossos recursos AWS S3 e CloudFront para responder com cabeçalhos de segurança para mitigar os riscos e vulnerabilidades mencionados.
Em alto nível, podemos fazer isso criando uma função Lambda@Edge que altera os cabeçalhos de resposta de origem para anexar os cabeçalhos de segurança desejados antes que os arquivos do aplicativo Web retornem ao navegador do usuário.
A estratégia é primeiro testar a conexão manual por meio do Console AWS. Em seguida, colocaremos essas configurações no Terraform para salvar essa parte da infraestrutura em código para referência futura e compartilhamento entre outras equipes e aplicativos.
Que tipo de cabeçalhos de segurança gostaríamos de adicionar?
Como parte das recomendações de nossa equipe de segurança do produto, fomos encarregados de adicionar cabeçalhos de segurança como “Strict-Transport-Security” e “X-Frame-Options”. Recomendamos que você também confira recursos como o MDN Web Security Cheatsheet para se atualizar. Aqui está um breve resumo dos cabeçalhos de segurança que você pode aplicar aos seus aplicativos da web.
Segurança Estrita de Transporte (HSTS)
Isso é para fornecer dicas ao navegador para acessar seu aplicativo da Web por meio de HTTPS em vez de HTTP.
Política de segurança de conteúdo (CSP)
Isso é para definir listas de permissões explícitas sobre que tipo de recursos você carrega ou se conecta em seu aplicativo da Web, como scripts, imagens, estilos, fontes, solicitações de rede e iframes. Este foi o mais difícil de configurar, pois tínhamos scripts, imagens, estilos e endpoints de API de terceiros para registrar explicitamente nessas políticas.
Dica: Use o cabeçalho Content-Security-Policy-Report-Only para ajudá-lo com testes em determinados ambientes. Se determinados recursos violassem as políticas, observamos uma saída útil do console dos recursos que precisávamos permitir em nossas políticas.
Se você quiser evitar um contratempo engraçado com telas em branco e aplicativos da Web não sendo carregados, recomendamos que você experimente suas políticas no modo somente relatório primeiro e faça testes completos antes de se sentir confiante o suficiente para implantar essas políticas de segurança em produção.
X-Content-Type-Options
Isso é para manter e carregar ativos com tipos MIME corretos em sua página da web.
X-Frame-Options
Isso é para fornecer regras sobre como seu aplicativo da Web pode ser carregado em um iframe.
Proteção X-XSS
Isso impede que as páginas sejam carregadas se um ataque de script entre sites for detectável em determinados navegadores.
Política de referência
Isso gerencia como o cabeçalho "Referenciador" com informações sobre a origem da solicitação é transmitido ao seguir links para sites ou recursos externos.
Com esses cabeçalhos de segurança em mente, vamos voltar a como configuramos nossas distribuições do CloudFront hoje e como as funções do Lambda@Edge nos ajudarão a atingir nosso objetivo.
Como usar o Lambda@Edge com nossas distribuições do CloudFront
Para nossas distribuições do CloudFront, configuramos coisas como:
- Os certificados SSL para os domínios a serem anexados no URL do CloudFront, como https://app.sendgrid.com
- Origens do bucket do S3
- Grupos de origem com buckets primários e de réplica para failover automático
- Comportamentos de cache
Esses comportamentos de cache, em particular, nos permitem controlar por quanto tempo queremos que as respostas para certos tipos de caminhos e arquivos sejam armazenadas em cache nos servidores de borda ao redor do mundo. Além disso, os comportamentos de cache também nos fornecem uma maneira de acionar funções do AWS Lambda em resposta a vários eventos, como solicitações de origem e respostas de origem. Você pode pensar nas funções do AWS Lambda como um código específico que você define que será executado em resposta a um determinado evento.
No nosso caso, podemos alterar os cabeçalhos de resposta de origem antes que sejam armazenados em cache pelos servidores de borda. Nossa função Lambda@Edge adicionará cabeçalhos de segurança personalizados à resposta de origem antes que ela retorne ao servidor de borda e antes que o usuário final receba os arquivos JavaScript, CSS e HTML com esses cabeçalhos.
Modelamos nossa abordagem após esta postagem no blog da AWS e a estendemos para facilitar a realização de alterações em uma política de segurança de conteúdo específica. Você pode modelar sua função Lambda@Edge da maneira como configuramos as coisas na geração de listas para o script, estilo e fontes de conexão. Essa função modifica efetivamente os cabeçalhos de resposta de origem do CloudFront e anexa cada cabeçalho de segurança com determinados valores à resposta antes de retornar chamando a função de retorno de chamada fornecida conforme mostrado abaixo.
Como testamos essa função do Lambda@Edge?
Antes de alterar oficialmente como seus ativos retornam com cabeçalhos de segurança, você deve verificar se a função funciona depois de configurar tudo manualmente por meio do Console AWS. É crucial que seus aplicativos da Web sejam capazes de carregar e funcionar corretamente com os cabeçalhos de segurança adicionados às suas respostas de rede. A última coisa que você quer ouvir é uma interrupção inesperada que ocorre devido aos cabeçalhos de segurança, portanto, teste-os completamente em seus ambientes de desenvolvimento.
Também é importante saber exatamente o que você escreverá no código do Terraform posteriormente para salvar essa configuração em sua base de código. Caso você não conheça o Terraform, ele fornece uma maneira de escrever e gerenciar sua infraestrutura de nuvem por meio de código.
Dica: dê uma olhada nos documentos do Terraform para ver se eles podem ajudá-lo a manter suas configurações complexas sem precisar lembrar de todas as etapas que você executou nos consoles de nuvem.
Como começar no Console AWS
Vamos começar com como configurar as coisas manualmente por meio do Console AWS.
- Primeiro, você precisa criar a função Lambda@Edge na região “us-east-1”. Indo para a página de serviços do Lambda, clicaremos em “Create Function” e nomearemos algo como “testSecurityHeaders1”.
2. Você pode usar uma função existente com permissões para executar a função em servidores de borda ou pode usar um de seus modelos de política de função, como "Permissões básicas do Lambda@Edge…" e nomeá-lo "lambdaedgeroletest".
3. Depois de criar sua função e função de teste do Lambda, você deverá ver algo como isto, onde você notará o botão “Add Trigger” para a função. É aqui que você eventualmente associará o Lambda ao comportamento de cache de uma distribuição do CloudFront acionado no evento de resposta de origem.
4. Em seguida, você precisa editar o código de função com o código de cabeçalhos de segurança que criamos antes e clicar em "Salvar".
5. Depois de salvar o código da função, vamos testar se sua função Lambda ainda funciona rolando até o topo e pressionando o botão “Test”. Você criará um evento de teste chamado "samplecloudfrontresponse" usando o modelo de evento "cloudfront-modify-response-header" para simular um evento de resposta de origem real do CloudFront e ver como sua função é executada nele.
Você notará coisas como o objeto de cabeçalho “cf.response” que seu código de função do Lambda modificará.
6. Após criar o evento de teste, você clicará no botão “Test” novamente e deverá ver como a função Lambda foi executada nele. Ele deve ser executado com sucesso com logs exibindo a resposta resultante com cabeçalhos de segurança adicionados como este.
Ótimo, parece que a função Lambda anexou os cabeçalhos de segurança à resposta corretamente!
7. Vamos voltar para a área “Designer” e clicar no botão “Add Trigger” para que você possa associar a função Lambda aos comportamentos de cache da sua distribuição do CloudFront no evento de resposta de origem. Certifique-se de selecionar um gatilho “CloudFront” e clique no botão “Deploy to Lambda@Edge”.
8. Em seguida, selecione a distribuição do CloudFront (em nosso exemplo, limpamos a entrada aqui por motivos de segurança) e um comportamento de cache para associar a ela.
Em seguida, você escolhe o comportamento de cache “*” e seleciona o evento “Resposta de origem” para corresponder em todos os caminhos de solicitação à sua distribuição do CloudFront e para garantir que a função Lambda sempre seja executada para todas as respostas de origem.
Em seguida, marque a confirmação antes de clicar em “Implantar” para implantar oficialmente sua função Lambda.
9. Após associar com sucesso sua função Lambda a todos os comportamentos de cache relevantes da distribuição do CloudFront, você deverá ver algo semelhante a isso na área "Designer" do painel Lambda, onde você pode ver os gatilhos do CloudFront e ter a opção de visualizá-los ou excluí-los.
Fazendo alterações no seu código Lambda
Sempre que você precisar fazer alterações em seu código Lambda, recomendamos:
- Publique uma nova versão no menu suspenso do botão "Ações"
- Exclua os gatilhos na versão mais antiga (você pode clicar no menu suspenso "Qualificadores" para ver todas as versões do seu Lambda)
- Associe os acionadores ao número da versão mais recente que você publicou recentemente
Ao implantar o Lambda pela primeira vez ou após publicar uma nova versão do Lambda e associar os gatilhos à versão mais recente do Lambda, talvez você não veja os cabeçalhos de segurança imediatamente nas respostas para seu aplicativo Web. Isso se deve ao modo como os servidores de borda no CloudFront armazenam em cache as respostas. Dependendo de quanto tempo você define o tempo de vida em seus comportamentos de cache, talvez seja necessário esperar um pouco para ver os novos cabeçalhos de segurança, a menos que você faça uma invalidação de cache na distribuição afetada do CloudFront.
Depois de reimplantar suas alterações na função do Lambda, geralmente leva algum tempo para que o cache seja limpo (dependendo das configurações de cache do CloudFront) antes que suas respostas tenham os ajustes mais recentes nos cabeçalhos de segurança.
Dica: para evitar atualizar muito a página ou ficar sem saber se suas alterações funcionaram, inicie uma invalidação de cache do CloudFront para acelerar o processo de limpeza do cache para que você possa ver seus cabeçalhos de segurança atualizados.
Acesse a página do CloudFront Services, aguarde o status da distribuição do CloudFront ser implantado, o que significa que todas as associações lambda estão concluídas e implantadas, e vá para a guia “Invalidations”. Clique em “Criar Invalidação” e coloque “/*” como o caminho do objeto para invalidar todas as coisas no cache e clique em “Invalidar”. Isso não deve demorar muito e, depois de concluído, atualizar seu aplicativo da Web deve ver as alterações mais recentes do cabeçalho de segurança.
Conforme você itera em seus cabeçalhos de segurança com base no que você encontra como violações ou erros em seu aplicativo da web, você pode repetir este processo:
- Publicando uma nova versão da função Lambda
- Excluindo os gatilhos na versão antiga do Lambda
- Associando os gatilhos na nova versão
- Cache invalidando sua distribuição do CloudFront
- Testando seu aplicativo da web
- Repetindo até que você se sinta confiante e seguro, as coisas funcionam conforme o esperado, sem páginas em branco, solicitações de API com falha ou erros de segurança do console
Quando as coisas estiverem estáveis, você pode, opcionalmente, passar para o Terraforming o que acabou de fazer manualmente nas configurações de código, supondo que o Terraform esteja integrado às suas contas da AWS. Não abordaremos como configurar o Terraform desde o início, mas mostraremos trechos de como será o código do Terraform.
Terraformando o Lambda@Edge acionado por nossa distribuição CloudFront
Após iterar na função Lambda@Edge para cabeçalhos de segurança na região “us-east-1”, queríamos adicioná-la à nossa base de código do Terraform para manutenção de código e controle de versão no futuro.
Para todos os comportamentos de cache que já implementamos, tivemos que associar o comportamento de cache à função Lambda@Edge, que o evento de resposta de origem acionou.
As etapas a seguir pressupõem que você já tenha a maioria das distribuições do CloudFront e buckets do S3 configurados por meio do Terraform. Vamos nos concentrar nos principais módulos e propriedades relacionados ao Lambda@Edge e adicionar o gatilho aos comportamentos de cache da distribuição do CloudFront. Não explicaremos como configurar seus buckets do S3 e outras configurações de distribuição do CloudFront do zero por meio do Terraform, mas esperamos que você possa ver o nível de esforço para fazer isso por conta própria.
Atualmente, dividimos nossos recursos da AWS em pastas de módulos separadas e passamos variáveis para esses módulos para flexibilidade em nossa configuração. Temos uma pasta apply
com uma subpasta de development
e production
e cada uma tem seu próprio arquivo main.tf
onde chamamos esses módulos com determinadas variáveis de entrada para instanciar ou modificar nossos recursos da AWS.
Essas subpastas também têm uma pasta lambdas
onde mantemos nosso código Lambda, como um arquivo security_headers_lambda.js
. O security_headers_lambda.js
tem o mesmo código que usamos em nossa função Lambda quando testamos manualmente, exceto que também o estamos salvando em nossa base de código para compactar e fazer upload por meio do Terraform.
1. Primeiro, precisamos de um módulo reutilizável para compactar nosso arquivo Lambda antes que ele seja carregado e publicado como outra versão de nossa função Lambda@Edge. Isso leva um caminho para nossa pasta Lambda que contém a eventual função Lambda do Node.js.
2. Em seguida, adicionamos ao nosso módulo CloudFront existente, que envolve os buckets do S3, as políticas e os recursos de distribuição do CloudFront criando também um recurso Lambda criado a partir do arquivo Lambda compactado. Como as saídas do módulo zip do Lambda passam como variáveis no módulo do CloudFront para configurar o recurso do Lambda, precisamos especificar a região do provedor da AWS como “us-east-1” e com uma política de função de trabalho como esta.
3. No módulo do CloudFront, associamos essa função do Lambda@Edge aos comportamentos de cache da distribuição do CloudFront, conforme demonstrado abaixo.
4. Por fim, juntando tudo no arquivo main.tf
da nossa pasta de apply/development
ou apply/production
, chamamos todos esses módulos e colocamos as saídas apropriadas como variáveis em nosso módulo CloudFront, conforme mostrado aqui.
Esses ajustes de configuração basicamente cuidam das etapas manuais que fizemos na seção anterior para atualizar o código do Lambda e associar a versão mais recente aos comportamentos de cache e gatilhos do CloudFront para eventos de resposta de origem. Woohoo! Não há necessidade de seguir ou lembrar as etapas do Console AWS, desde que apliquemos essas alterações aos nossos recursos.
Como implementamos isso com segurança em diferentes ambientes?
Quando associamos pela primeira vez nossa função Lambda@Edge à nossa distribuição de teste do CloudFront, percebemos rapidamente como nosso aplicativo da web não carregava mais corretamente. Isso se deve principalmente à forma como implementamos nosso cabeçalho Content-Security-Policy e como ele não cobria todos os recursos que carregamos em nosso aplicativo. Os outros cabeçalhos de segurança representavam menos risco em termos de bloquear o carregamento de nosso aplicativo. No futuro, vamos nos concentrar em lançar os cabeçalhos de segurança com mais iterações em mente para ajustar o cabeçalho Content-Security-Policy.
Conforme mencionado anteriormente, descobrimos como podemos aproveitar o cabeçalho Content-Security-Policy-Report-Only para minimizar o risco à medida que reunimos mais domínios de recursos para adicionar em cada uma de nossas políticas.
Nesse modo somente relatório, as políticas ainda serão executadas no navegador e enviarão mensagens de erro do console de qualquer violação das políticas. No entanto, ele não bloqueará totalmente esses scripts e fontes, portanto, nosso aplicativo da Web ainda poderá ser executado normalmente. Cabe a nós continuar analisando todo o aplicativo da Web para garantir que não percamos nenhuma fonte importante em nossas políticas, caso contrário, isso afetará negativamente nossos clientes e equipe de suporte.
Para cada ambiente, você pode implementar os cabeçalhos de segurança do Lambda da seguinte forma:
- Publique as alterações no Lambda manualmente ou por meio de um plano do Terraform e aplique primeiro as alterações no ambiente com outros cabeçalhos de segurança e o cabeçalho Content-Security-Policy-Report-Only.
- Aguarde o status de distribuição do CloudFront ser totalmente implantado com o Lambda associado aos comportamentos de cache.
- Faça uma invalidação de cache em sua distribuição do CloudFront se os cabeçalhos de segurança anteriores ainda forem exibidos ou se demorar muito para que as alterações atuais apareçam no navegador.
- Visite e faça um tour pelas páginas do seu aplicativo da Web com as ferramentas do desenvolvedor abertas, verificando o console em busca de mensagens de erro do console “Report Only…” para melhorar seu cabeçalho Content-Security-Policy.
- Faça alterações em seu código Lambda para levar em consideração as violações relatadas.
- Repita a partir da primeira etapa até se sentir confiante o suficiente para alterar seu cabeçalho de Content-Security-Policy-Report-Only para Content-Security-Policy, o que significa que o ambiente irá aplicá-lo.
Melhorando nossa pontuação de cabeçalhos de segurança
Depois de aplicar com sucesso as alterações do Terraform em nossos ambientes e invalidar os caches do CloudFront, atualizamos as páginas em nosso aplicativo da web. Mantivemos as ferramentas do desenvolvedor abertas para ver os cabeçalhos de segurança, como HSTS, CSP e outros em nossas respostas de rede, como os cabeçalhos de segurança mostrados abaixo.
Também executamos nosso aplicativo da Web por meio de um relatório de verificação de cabeçalhos de segurança, como o deste site . Como resultado, testemunhamos grandes melhorias (uma classificação A!) de uma nota anteriormente reprovada, e você pode obter melhorias semelhantes depois de alterar suas configurações do S3/CloudFront para ter cabeçalhos de segurança em vigor.
Avançando com cabeçalhos de segurança
Depois de configurar manualmente os cabeçalhos de segurança por meio do Console AWS ou Terraformar com sucesso a solução e aplicar as alterações a cada um de seus ambientes, agora você tem uma ótima base para iterar ainda mais e melhorar seus cabeçalhos de segurança existentes no futuro.
Dependendo da evolução do seu aplicativo da Web, talvez seja necessário tornar seu cabeçalho Content-Security-Policy mais específico em termos dos recursos permitidos para uma segurança mais rígida. Ou talvez seja necessário adicionar um novo cabeçalho inteiramente para uma finalidade separada ou para preencher outra falha de segurança.
Com essas alterações futuras em seus cabeçalhos de segurança nas funções do Lambda@Edge, você pode seguir estratégias de lançamento semelhantes por ambiente para garantir que seus aplicativos da Web estejam protegidos contra ataques maliciosos na Web e ainda funcionem sem que seus usuários percebam a diferença.
Para mais artigos escritos por Alfred Lucero, acesse a página do autor do blog: https://sendgrid.com/blog/author/alfred/