Arte digital de uma nuvem colorida e brilhante com um grande cadeado colorido no centro

URL pré-assinada: manipule arquivos no AWS S3 de forma segura e eficiente.

Glauco Villas Boas
Geekie Educação
Published in
13 min readJun 3, 2024

--

Por padrão, os buckets no AWS S3 (Simple Storage Service) são privados, acessíveis apenas por usuários autorizados. No entanto, frequentemente precisamos que pessoas externas façam downloads ou uploads nesses repositórios. Um caminho fácil é tornar esse bucket público, para que o frontend possa exibir esses arquivos sem a necessidade de autenticações. Mas se esses arquivos forem sensíveis, deixá-los públicos é uma brecha na segurança da sua aplicação. Então como fazer isso de forma segura e eficiente? A resposta está no uso de URLs pré-assinadas (pre-signed URLs). Neste artigo, exploramos como elas funcionam e como podem otimizar o acesso seguro e eficiente aos seus arquivos no S3.

O que são URLs pré-assinadas?

URLs pré-assinadas no AWS S3 são urls temporárias que permitem acesso controlado a um bucket privado sem a exposição direta das credenciais da AWS. Essas URLs são geradas com uma chave privada e podem ser configuradas para permitir operações específicas, como upload ou download de arquivos diretamente do AWS S3, por um período de tempo limitado, sem a necessidade de um servidor intermediário para fazer a transferência dos arquivos.

Também é possível determinar exatamente qual arquivo estará acessível, ou definir o nome, tipo, caminho, tamanho máximo, etc, do arquivo que será enviado. Adicionalmente, você pode impor restrições mais amplas, como quais IPs estão autorizados a usar a URL e a duração da sua validade. O AWS S3 recusa as requests que não respeitam as restrições configuradas na URL pré-assinada (Ex.: A url foi gerada para envio de um arquivo mp3, mas o usuário tenta enviar um arquivo mp4 através dela).

As URLs pré-assinadas podem ser geradas utilizando o AWS SDK com uma chave privada, permitindo que seu backend crie essas URLs de forma autônoma, sem a necessidade de comunicação com os servidores da AWS para que elas sejam geradas.

Casos de uso

URL pré-assinada de download

Suponha que você seja uma pessoa desenvolvedora de um site de recursos humanos, onde empresas publicam vagas de emprego e candidatos submetem seus currículos em PDF para as vagas de interesse.

Esses currículos contêm informações sensíveis, como dados pessoais e talvez até fotos e documentos. A segurança desses dados é primordial não apenas para manter a confiança dos usuários e proteger suas informações pessoais, mas também para evitar processos judiciais sob a Lei Geral de Proteção de Dados Pessoais (LGPD).

Esses arquivos são armazenados em um bucket no AWS S3 (Simple Storage Service), que, por padrão, é privado e acessível apenas a usuários na AWS com permissões específicas. Então, como permitir que empresas através do frontend de sua aplicação acessem esses currículos?

❌ Tornar o bucket público ❌

Uma abordagem simples seria tornar o bucket público, permitindo que qualquer pessoa acesse os arquivos sem autenticação, bastando o frontend ter acesso a URL do caminho do arquivo no bucket do AWS S3.

Front end primeiramente solicita endereço do arquivo no S3 para o back end, e então acessa arquivos através de seu endereço no S3.

O endereço do currículo no S3 pode ser algo parecido com isso:

No entanto, tornar o bucket público e expô-lo apresenta vários riscos:

  1. URLs Previsíveis: atacantes poderiam adivinhar o endereço dos arquivos se eles forem previsíveis, como alterar ‘curriculo_2.pdf’ para ‘curriculo_3.pdf’.
  2. Listagem de Arquivos: se o bucket estiver configurado para permitir listagem de arquivos, atacantes poderiam facilmente acessar todos os arquivos.
  3. Ataques de Força Bruta: experimentar diferentes combinações de URLs até encontrar arquivos válidos com o uso de bots.
  4. Comprometimento de Contas da AWS: se as credenciais de acesso à AWS forem comprometidas, isso pode expor todos os dados do bucket.
  5. Vulnerabilidades na Segurança da AWS: embora raras, brechas na própria AWS que permitam atacantes identificarem as URLs dos arquivos, colocariam todos eles em risco.

A exposição desses currículos a terceiros não autorizados não apenas compromete a integridade dos dados, mas também pode resultar em perda de confiança e potenciais ações legais. Portanto, enquanto a tentação de simplificar o acesso pode ser grande, os riscos associados tornam essa opção inaceitável.

✅ Usar URLs pré-assinada de download com bucket privado ✅

Optar por manter o bucket privado e gerar URLs pré-assinadas temporárias para visualização de currículos no frontend é uma abordagem mais segura. Dessa forma, nós seguimos o princípio do menor privilégio, que consiste em autorizar somente o absoluto necessário, criando URLs que permitem acesso restrito aos arquivos específicos que precisam ser visualizados, sem expor todo o conteúdo do bucket.

Essa metodologia minimiza significativamente os riscos. Mesmo que uma URL específica seja comprometida, apenas o arquivo correspondente é afetado e somente durante o período de validade da URL. Os riscos associados a URLs previsíveis, listagem de arquivos e ataques de força bruta são mitigados, pois o acesso não é mais determinado apenas pelo conhecimento do caminho do arquivo, mas também exige privilégios apropriados.

Front end primeiramente solicita url pré-assinada do arquivo no S3 para o back end, e então acessa arquivos através da url pré-assinada.

O processo é similar à abordagem anterior, mas com uma diferença crucial: o bucket permanece privado, e em vez de o backend fornecer diretamente o endereço do arquivo no S3 ao frontend, ele gera uma URL pré-assinada temporária. Dessa forma, o frontend ainda pode acessar o currículo, mesmo com o bucket privado.

A geração da URL não requer comunicação direta com a AWS. Basta utilizar o SDK da AWS para definir as restrições e, com uma chave privada, gerar a URL pré-assinada. A URL vai ser parecida com essa:

https://luppy.s3.sa-west-2.amazonaws.com/caminho/do/curriculo.pdf?X-Amz-Security-Token=xxx&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240511T211720Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=yyy&X-Amz-Signature=zzz

Qualquer tentativa de modificar a URL resultará em um erro, impedindo o acesso não autorizado:

The request signature we calculated does not match the signature you provided. Check your key and signing method

A conta da AWS referente a chave privada utilizada para gerar a URL pré-assinada, precisa ter permissões de acesso ao bucket privado, sem essas permissões, as URLs geradas não poderão acessar os arquivos. Se a conta perde essas permissões, qualquer URL pré-assinada criada anteriormente também perderá a validade.

Seguindo o princípio do menor privilégio, recomenda-se criar um usuário específico com apenas as permissões estritamente necessárias para acessar o bucket. Assim, em caso de qualquer comprometimento de segurança, é possível revogar rapidamente as permissões deste usuário, minimizando os riscos e limitando o impacto de uma possível violação.

Bônus: CDN

O uso de uma CDN (Content Delivery Network — Rede de Distribuição de Conteúdo) é uma estratégia eficaz para otimizar o acesso a arquivos armazenados no AWS S3, especialmente quando se trata de usuários geograficamente distantes dos servidores da AWS. As CDNs funcionam armazenando cópias dos arquivos em vários servidores ao redor do mundo, o que permite que o conteúdo seja entregue ao usuário final com menor latência e maior velocidade de carregamento.

Ao usar uma CDN, os usuários não acessam diretamente o bucket do S3, mas sim uma URL da CDN que entrega o arquivo em cache. Se o arquivo já estiver em cache, não há necessidade de buscá-los no AWS S3. Como o custo associado ao acesso de arquivos via CDN é geralmente inferior ao custo de acesso direto no S3, isso também pode resultar em uma economia de custos considerável.

No que diz respeito à segurança, as CDNs oferecem proteção robusta contra ataques DDoS (Ataques de Negação de Serviço Distribuído) e ajudam a mascarar o endereço original dos arquivos armazenados no bucket do S3.

Front end primeiramente solicita endereço do arquivo na CDN para o back end, e então acessa arquivos através de seu endereço na CDN. Caso o arquivo não esteja no cache da CDN, ela fará uma requisição para o AWS S3 para copiar o arquivo original, nesse caso o bucket é público.

Exemplo de url do CDN:

https://z2g1lha3urco.cloudfront.net/caminho/do/curriculo.pdf

Por mais que o endereço real do arquivo esteja melhor protegido, ainda assim os arquivos no bucket público estão suscetíveis aos ataques citados anteriormente.

Integrar URLs pré-assinadas com CDNs é possível, mas necessita de alguns passos adicionais. Isso ocorre porque a CDN precisa acessar o arquivo do S3 para armazená-lo em cache, mas como o bucket será privado, a CDN precisa de permissões para acessar o arquivo original.

Temos algumas alternativas, exemplo delas são:

Front end primeiramente solicita uma URL assinada da CDN para o back end, e então acessa arquivos através da URL assinada. Caso o arquivo não esteja no cache da CDN, ela fará uma requisição para o AWS S3 para copiar o arquivo original, nesse caso o bucket é privado.

URL pré-assinada de upload

Agora vamos dizer que seu website recebe milhares de currículos diariamente, e além dos currículos, os usuários também podem enviar vídeos e outros arquivos referentes aos seus portfólios.

Tradicionalmente, muitos desenvolvedores optam por uma arquitetura em que o frontend envia esses arquivos para o backend, que então os encaminha ao AWS S3:

Front end envia arquivo para o back end, que então envia esse arquivo para o AWS S3.

Esta abordagem pode sobrecarregar o servidor backend, exigindo uma infraestrutura mais robusta para manejar o alto volume de dados, o que pode afetar negativamente tanto a performance e custos, quanto a escalabilidade do sistema.

A utilização de URLs pré-assinadas simplifica esse processo, permitindo que os usuários enviem seus arquivos diretamente ao S3. Isso elimina a necessidade do backend agir como um intermediário, reduzindo a carga sobre os servidores e melhorando a eficiência geral da transferência de dados.

Front end solicita uma URL pré-assinada de upload para o back end, em seguida envia o arquivo para o AWS S3 através dessa URL, por último faz uma nova requisição para o back end informando que o upload foi bem sucedido.

Ao gerar uma URL pré-assinada de upload, é possível definir restrições específicas, como o diretório e nome exato do arquivo no bucket, tempo de validade da URL, os tipos de arquivos permitidos, tamanho máximo do arquivo, entre outros. Se o arquivo não for compatível com as restrições ou caso a URL expire, o upload não será autorizado pelo AWS S3 e um status de erro será retornado pela requisição.

É importante destacar que, se a mesma URL for utilizada mais de uma vez para upload antes de expirar, o arquivo original será substituído pelo mais recente enviado.

Também é possível integrar o bucket do S3 com serviços complementares como AWS SQS (Simple Queue Service) e AWS Lambda, por exemplo. Após cada upload bem-sucedido, o usuário recebe imediatamente um status HTTP de sucesso, indicando que a requisição foi concluída, porém essa integração cria um sistema onde, em seguida, de maneira assíncrona, uma mensagem é enviada ao SQS, que por sua vez ativa uma função Lambda. Esta função pode, então, realizar uma variedade de processamentos nos dados, como conversão de formatos, edição ou extração de informações, notificando o backend assim que estes processamentos forem concluídos (se for necessário).

Front end solicita URL pré-assinada de upload para o back end, e então envia o arquivo para o AWS S3 através da URL. De forma assíncrona, o AWS S3 envia um evento de upload bem sucedido para o AWS SQS que então envia esse evento para uma Lambda Function que por último faz os processamentos e avisa ao back end que o processo foi concluído.

Um caso prático dessa aplicação é uma função Lambda configurada para extrair texto do currículo e classificá-lo usando inteligência artificial. Ao final do processamento, a Lambda aciona uma API, enviando os dados relevantes diretamente para o backend da aplicação.

Esse é apenas um exemplo, mas as possibilidades são diversas. A escolha da arquitetura, das ferramentas e processos integrados dependerá das necessidades específicas do seu projeto.

Mão na massa

Criei um projeto bem simples no GitHub que demonstra o uso de URLs pré-assinadas para upload e download de arquivos no AWS S3. Através dele é possível ter uma ideia de como aplicar essa abordagem na prática:

O backend foi feito em Node.js (Javascript), mas é possível utilizar o AWS SDK em diversas linguagens (.NET, Go, Java, Python, Kotlin, Ruby, Rust…).

Na página do repositório, há instruções de como usá-lo, mas vamos dar uma resumida por aqui.

Configuração de credenciais

A forma mais assertiva de se dar acesso programático a aplicações que vão executar dentro da infraestrutura AWS (EC2, Lambda, ECS, etc.) é através dos AWS IAM Roles, associando um Role diretamente ao recurso que executará o serviço, eliminando a necessidade de criação e utilização de credenciais explícitas que precisam ser recicladas com alguma frequência. Isso não apenas simplifica a gestão de acessos, mas também reforça a segurança.

Mas, para o nosso exemplo, vamos criar uma aplicação bem simples que rodará localmente, em um ambiente fora da AWS. Nesse contexto, é necessário o uso de credenciais explicitamente fornecidas para possibilitar o acesso aos serviços da AWS. Vamos demonstrar como configurar e utilizar essas credenciais:

Primeiramente, é necessário ter uma conta na AWS e um usuário administrador. Se ainda não possui, crie uma seguindo este guia:

Em seguida, crie bucket privado no S3 para armazenar os arquivos.

Após isso, teremos todas as informações necessárias para preencher as variáveis no arquivo.env do projeto.

AWS_ACCESS_KEY_ID= // Chave pública do usuário
AWS_SECRET_ACCESS_KEY= // Chave privada do usuário
AWS_REGION= // Região da AWS em que o bucket está hospedado (ex.: us-east-2)
AWS_BUCKET_NAME= //Nome do bucket

Para obter AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY, acesse a página de usuários na IAM da sua conta AWS:

https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-2#/users

Selecione o usuário , e clique em “Create access key”.
Para o nosso exemplo, usei o usuário administrador, mas o ideal é criar um novo user com as permissões absolutamente necessárias para gerenciar os arquivos do bucket em específico.

Página IAM da AWS exibindo o painel de um usuário, destcando o texto "Create access key"

O caso de uso para o nosso exemplo será o “Application running outside AWS”. Após selecionar esta opção, clique em “Next”, e em seguida clique em “Create access key”.

Dashboard da AWS exibindo dois campos "Access key" e "Secret Access Key".

Após isso, você verá dois valores: “Access key” e “Secret access key”. O primeiro é o valor que deve ser utilizado na variável AWS_ACCESS_KEY_ID, e o segundo é o valor de AWS_SECRET_ACCESS_KEY.

Código

Após configurar as variáveis no arquivo .env, você pode iniciar o projeto com o comando npm install & npm start.

Vamos detalhar os componentes fundamentais do backend e frontend:

Configuração do client s3

Primeiro, configuramos o cliente S3 no backend, definindo as credenciais e a região.

Como mencionado anteriormente, se este serviço estivesse operando dentro da infraestrutura da AWS, poderíamos utilizar um AWS IAM Role associado ao recurso (como EC2, Lambda ou ECS Service). Nesse caso, não seria necessário fornecer explicitamente as credenciais em credentials, pois o SDK da AWS detectaria e utilizaria automaticamente o Role associado ao recurso.

Endpoint para gerar URLs pré-assinadas de download

Configuramos uma API para listar e criar URLs pré-assinadas de download. Este endpoint busca todos os arquivos do bucket e retorna um array com URLs pré-assinada de download para cada um dos arquivos:

Endpoint para gerar URLs pré-assinadas de upload

Essa rota é responsável por gerar uma URL pré-assinada de upload e retorná-la para que o frontend possa fazer a submissão do arquivo a partir dela.

Como destacado nos comentários, também é possível especificar condições no campo Conditions. Essas condições servem para definir restrições sobre os arquivos a serem enviados, tais como o tamanho máximo e o tipo do arquivo, garantindo que apenas uploads que atendam a esses critérios sejam aceitos.

Ao gerar uma URL pré-assinada de POST, é criado, além da URL, um conjunto de campos que devem ser incluídos na requisição. Caso contrário, a submissão do arquivo não será aceita pelo AWS S3. A URL e campos tem esse formato:

{
"url": "https://nome-do-bucket.s3.us-east-2.amazonaws.com/",
"fields": {
"bucket": "nome-do-bucket",
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
"X-Amz-Credential": "xxx",
"X-Amz-Date": "20240513T004923Z",
"key": "uploads/17155233594-arquivo",
"Policy": "yyy",
"X-Amz-Signature": "zzz"
}
}

Esses campos incluem detalhes como o algoritmo de assinatura, credenciais e a política de segurança, que garantem que o upload seja feito de forma segura e de acordo com as permissões estabelecidas. Também é necessário especificar o campo file na sua requisição, que representa o arquivo a ser enviado.

Além da URL pré-assinada de POST, existe a URL pré-assinada de PUT, que também serve para enviar arquivos para o bucket, mas é mais compacta, pois incorpora diretamente na própria URL todos os parâmetros necessários para o envio do arquivo (parecido com a URL pré-assinada de GET). Então, ao invés de gerar a URL e os campos, é gerada apenas uma longa URL com todos os parâmetros de seguranças. O lado negativo é que ela é bem mais limitada por não poder definir Policies, então não é possível fazer algumas restrições como tamanho e tipo do arquivo. Exemplo de URL pré-assinada de PUT:

https://nome-do-bucket.s3.us-east-2.amazonaws.com/uploads/arquivo.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=xxx&X-Amz-Date=20240513T004923Z&X-Amz-Signature=zzz

Para enviar os arquivos para as URLs pré-assinadas de POST, temos a seguinte lógica no frontend:

O que fazemos é criar um objeto FormData, inserimos os campos obrigatórios nele, e em seguida adicionamos o arquivo no campo file. Depois disso, basta fazer uma requisição de POST para a url recebida pelo backend, passando o FormData no corpo da requisição, e o arquivo será enviado para o bucket no AWS S3.

Caso a operação seja realizada com sucesso, o status 204 No Content é retornado pela API.

Conclusão

Em resumo, utilizar URLs pré-assinadas para gerenciar acessos ao AWS S3 é relativamente simples e apresenta vantagens significativas tanto para uploads quanto para downloads. Para uploads, ao permitir que usuários enviem arquivos diretamente ao S3, eliminamos a necessidade de intermediários, o que simplifica o fluxo de dados e reduz os custos operacionais, além de escalar facilmente com o aumento da demanda sem sobrecarregar a infraestrutura do servidor. Para downloads, a estratégia fortalece a segurança ao aderir ao princípio do menor privilégio, mantendo informações sensíveis num bucket privado, garantindo que apenas operações autorizadas sejam realizadas, de acordo com permissões temporárias e controladas.

Referências

--

--