DEV Community

Laura Bolaños for AWS Community Builders

Posted on • Originally published at builder.aws.com

Patrones de acceso seguro a S3 🌐

Este análisis se propone clarificar algunos casos de uso donde se requiere servir contenido de S3 de manera segura, identificando las brechas de seguridad a las que debemos enfrentarnos y explorar posibles mejoras. El desafío real no está en analizar cada servicio por separado, sino en combinarlos correctamente.

1. CloudFront + OAC con presigned url

Caso de uso frecuente: Un sitio web que muestra imágenes/vídeos de una temática determinada. Una parte de los objetos multimedia puede restringirse a que solo lo vean usuarios que iniciaron sesión vía Cognito. Aquí la solución más sencilla:

Upload de Multimedia (escritura)
El frontend solicita una Presigned URL a un endpoint protegido por Cognito (una api). La api se integra con una función Lambda que genera una URL temporal (válida por x minutos) firmada con permisos de "s3:PutObject" para el bucket privado de imágenes.

Visualización de Multimedia (lectura)
El bucket de S3 tiene el bloqueo de acceso público activado.
Los elementos multimedia se sirven a través de CloudFront, utilizando OAC (Origin Access Control) para firmar internamente la petición (usando el protocolo SigV4) y solicitar el objeto a S3 de forma segura.
La política (Bucket Policy) del bucket S3 solo permite la acción "s3:GetObject" si la petición proviene específicamente del ARN de la distribución de CloudFront ("Condition: AWS:SourceArn").

CloudFront + OAC con presigned url

Conceptualmente una Presigned URL se puede utilizar tanto para cargar como para leer objetos, en el ejemplo anterior notar que solo se usa para cargar los objetos a S3*.* Una presigned URL es simplemente una URL con la firma embebida ➡ cualquiera que la tenga puede usarla hasta que expire, sin autenticación. Por eso debemos aplicar diferentes técnicas de seguridad.

  • La url puede tener una duración desde 1 minuto hasta 12 horas, si alguien no autorizado consigue la url y ya caduco ➡ NO la puede utilizar.
  • Con el uso de Cognito, el token JWT tiene una duración y se puede aplicar este control con Cognito JWT Authorizer (nativo de API Gateway). La otra posibilidad es tener Lambda Authorizer en API Gateway. Esta última tiene lógica de autorización custom.
  • Cuando generamos una presigned url para upload, se puede controlar que tipos de objetos cargar y su peso máximo. Algunos ejemplos:
# Generate presigned URL for PUT
# ContentType specifies the types of objects and controls their size on the frontend
upload_url = s3_client.generate_presigned_url(
            "put_object",
            Params={
                "Bucket":      IMAGES_BUCKET,
                "Key":         imagen_key,
                "ContentType": expected_content_type,
            },
            ExpiresIn=PRESIGNED_URL_TTL,
)
Enter fullscreen mode Exit fullscreen mode
# the frontend makes a multipart POST request
response = s3_client.generate_presigned_post(
    Bucket=IMAGES_BUCKET,
    Key=imagen_key,
    Fields={
        "Content-Type": expected_content_type,
    },
    Conditions=[
        {"Content-Type": expected_content_type},
        ["content-length-range", 1, 5242880],  # máx 5MB
    ],
    ExpiresIn=PRESIGNED_URL_TTL,
)
Enter fullscreen mode Exit fullscreen mode

1.2 Caso de uso complementario - Validación de acceso en el edge (Lambda@Edge)

Podemos enfrentarnos a dos requerimientos no funcionales: minimizar la latencia y asegurar quién puede hacer request a Cloudfront.
El caso anterior asegura que solo CloudFront puede acceder a S3, pero no controla quién puede hacer requests a CloudFront. Con Lambda@Edge es posible agregar lógica de autorización en los nodos de la CDN, antes de que el request llegue al origen. La secuencia es la siguiente:

  • Cuando un usuario solicita un objeto de un bucket, CloudFront invoca una función Lambda@Edge en el nodo más cercano geográficamente.
  • La función valida el JWT de Cognito (o cualquier token de sesión) sin necesidad de redirigir el request al backend.
  • Si el token es válido, el request continúa hacia S3 a través de OAC normalmente.
  • Si no es válido, Lambda@Edge retorna un 401 o redirige al login desde el edge.

⭐ La ventaja es la baja latencia, la validación ocurre en el edge node más cercano al usuario, sin necesidad de viajar hasta la región de AWS donde vive el backend. Cada request de contenido protegido se resuelve en milisegundos en lugar de sumar el tiempo de ida y vuelta a tu servidor.

2. Descarga temporal de un archivo privado - S3 Presigned URLs

Para esta opción el casos de uso típico: Un usuario necesita descargar un reporte generado, una factura o un documento legal guardado en bucket S3. No se utiliza Cloudfront porque no necesitamos cachear contenido de la petición.

S3 Presigned URLs

  • Se produce la petición del usuario, el backend valida que ese usuario tiene permiso sobre ese archivo específico.
  • Lambda genera una presigned URL con GetObject que expire entre 5-15 min. La lógica de validación de lambda es idéntica del caso de uso 1 de Upload.
  • El browser descarga directamente desde S3 usando esa URL.

🔐 Brechas de seguridad detectadas

Riesgos de seguridad Mitigación posible
El usuario la copia y la comparte intencionalmente o no. TTL muy corto (1-2 minutos si es descarga inmediata)
Queda en logs del browser, proxies o herramientas de red Generar la URL y redirigir directamente (302) en lugar de devolverla al frontend (nunca queda expuesta en el browser)
Se filtra por un Referer header en un redirect Incluir el userId en la key del objeto (uploads/{userId}/{filename}) para que aunque alguien obtenga la URL, solo pueda acceder al archivo de ese usuario específico.

3. Distribución segura con cookies por sesión - CloudFront Signed Cookies

Caso de uso típico: Una plataforma web con contenido que se accede por suscripción, los usuarios se loguean pero cada sesión debe tener acceso acotado en el tiempo. En lugar de firmar URL por URL, el backend emite una Signed Cookie de CloudFront al momento del login, que el browser envía automáticamente en cada request de objetos multimedia.

CloudFront Signed Cookies

  • El backend verifica la sesión y envía tres headers Set-Cookie al browser (CloudFront requiere exactamente tres pares clave-valor para una Signed Cookie). Estos headers deben enviarse antes de que el usuario solicite el contenido privado.
  • Si se configura un tiempo de expiración corto, el backend puede reenviar tres nuevos headers Set-Cookie en requests posteriores para mantener el acceso activo.
  • CloudFront valida las cookies en cada request antes de servir los objetos multimedia. La distribución debe tener al menos dos comportamientos de cache: uno público y uno que requiere autenticación. La sección privada redirige al login si no hay cookie válida.
  • Al cerrar sesión o expirar las cookies, el acceso se revoca sin cambiar las URLs.

⭐ Esta opción es Ideal cuando un usuario accede a múltiples objetos multimedia de una misma sección sin generar una firma por cada archivo.
⭐ Si CloudFront se configura para cachear basándose en cookies, NO generará entradas de caché separadas por los atributos de las Signed Cookies. Es decir, todos los usuarios autorizados compartirán la misma entrada de caché para un objeto determinado— CloudFront valida la cookie para permitir o denegar el acceso, pero la ignora al construir la cache key.


Aspectos generales a tener en cuenta

Diseño
Considerar Patrón BFF (Backend for Frontend). Para el caso de descarga privada, se puede interponer una capa BFF entre el frontend y API Gateway. Esta capa maneja la sesión del usuario y puede ejecutar un redirect 302 directo hacia S3, evitando que la presigned URL quede expuesta en el browser.

Performance/Costo
Invalidaciones de CloudFront (incurre costo después de 1000/mes). Además del costo analizar tiempo de cacheo de datos.

S3 request rates (3500 PUT/5500 GET por segundo por prefix, si el volumen es alto distribuir con prefijos).

Costo de Lambda por invocación para generar presigned URLs a escala.

Seguridad
Rotación de claves KMS si el bucket usa SSE-KMS.

Versionado de objetos en S3 para evitar sobreescritura accidental.

Operacional
TTL de presigned URLs vs tiempo de expiración del JWT — si el JWT expira antes que la URL, tenemos un problema!

Estrategia de nombres de objetos (userId/filename) para evitar colisiones y facilitar políticas IAM por prefijo.

Top comments (0)