#aws#presigned#S3

Presigned URLs en S3: GET vs PUT

Si trabajas con AWS S3 desde una aplicación backend, tarde o temprano te vas a topar con las **presigned URLs** (URLs prefirmadas). Son una de esas herramientas que parecen simples por fuera, pero que esconden varios detalles importantes que conviene entender bien antes de usarlas

¿Qué es una presigned URL?

Una presigned URL es una URL temporal que le da a alguien (un usuario, un frontend, otro servicio) permiso para realizar una operación específica sobre un objeto de S3, sin necesidad de tener credenciales de AWS propias.

La idea central es esta: tu backend, que sí tiene credenciales con permisos sobre el bucket, genera una URL firmada digitalmente. Esa firma incluye información como el bucket, el objeto, el método HTTP permitido, y una fecha de expiración. Quien reciba esa URL puede usarla directamente, pero solo para lo que la firma autoriza, y solo durante el tiempo que dure la validez.

Esto evita dos problemas comunes:

  • Exponer credenciales de AWS en el cliente (frontend, apps móviles, etc.)
  • Tener que enrutar archivos pesados a través de tu propio servidor

Presigned URL para GET: descargar/leer objetos

Cuando generas una presigned URL para un GET, le estás dando a alguien acceso temporal para leer un objeto que normalmente sería privado.

Casos de uso típicos

  • Compartir un archivo privado (como un Excel generado dinámicamente) sin hacer el bucket público
  • Dar acceso temporal a documentos sensibles (facturas, reportes, comprobantes)
  • Servir contenido a usuarios autenticados sin exponer el bucket completo

Ejemplo con AWS SDK v2 (Java)

S3Presigner presigner = S3Presigner.create();

GetObjectRequest getObjectRequest = GetObjectRequest.builder()
    .bucket("mi-bucket")
    .key("reportes/reporte-2026-08.xlsx")
    .build();

GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
    .signatureDuration(Duration.ofMinutes(15))
    .getObjectRequest(getObjectRequest)
    .build();

PresignedGetObjectRequest presignedRequest = presigner.presignGetObject(presignRequest);

String url = presignedRequest.url().toString();

Con esto, quien reciba url puede descargar el archivo directamente desde S3, sin pasar por tu servidor, durante los próximos 15 minutos.

Presigned URL para PUT: subir objetos

Para un PUT, el flujo es distinto pero el principio es el mismo: le das permiso a alguien para subir un archivo directamente a un key específico de tu bucket.

Casos de uso típicos

  • Subida de archivos desde el frontend directamente a S3 (evitando que tu backend reciba el archivo completo)
  • Formularios de carga de documentos, imágenes de perfil, comprobantes
  • Integraciones donde un servicio externo necesita depositar un archivo en tu bucket

Ejemplo con AWS SDK v2 (Java)

PutObjectRequest putObjectRequest = PutObjectRequest.builder()
    .bucket("mi-bucket")
    .key("uploads/comprobante-123.pdf")
    .contentType("application/pdf")
    .build();

PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
    .signatureDuration(Duration.ofMinutes(10))
    .putObjectRequest(putObjectRequest)
    .build();

PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);

String url = presignedRequest.url().toString();

El cliente hace un PUT HTTP directo a esa URL con el archivo como body. Tu backend nunca ve el contenido del archivo, solo genera el permiso.

Diferencias clave entre GET y PUT

Aspecto Presigned GET Presigned PUT
Dirección del flujo S3 → cliente Cliente → S3
Propósito Leer/descargar un objeto existente Crear/sobrescribir un objeto
Riesgo principal Exponer datos sensibles si se filtra la URL Que alguien suba contenido no deseado o de tamaño excesivo
Validaciones recomendadas Expiración corta, key específica Content-Type, límite de tamaño, expiración corta
Uso típico Descargar reportes, servir imágenes privadas Formularios de carga, uploads desde frontend

Errores comunes al trabajar con presigned URLs

Uno de los más frecuentes es el temido SignatureDoesNotMatch. Generalmente ocurre por:

  • Desajuste de zona horaria o reloj del sistema entre el cliente y el servidor que firma
  • El cliente modifica headers que formaron parte de la firma (por ejemplo, cambia el Content-Type al hacer el PUT, distinto al que se usó al generar la URL)
  • Usar la región incorrecta al construir el cliente de S3
  • Codificación incorrecta de caracteres especiales en el key del objeto

La regla de oro: todo lo que se firma debe coincidir exactamente con lo que se envía. Si firmaste con un Content-Type específico, el cliente debe enviar ese mismo header, ni más ni menos.

Buenas prácticas

  1. Expiración corta: entre 5 y 15 minutos suele ser suficiente para la mayoría de los casos de uso.
  2. Keys específicos, no genéricos: evita permitir subidas a rutas amplias; usa keys únicos (por ejemplo, con UUID) para prevenir sobrescrituras accidentales.
  3. Restringe el Content-Type en los PUT: así evitas que alguien suba un archivo ejecutable disfrazado de imagen.
  4. No reutilices URLs firmadas: genera una nueva por cada operación cuando el caso lo permita.
  5. Registra el uso: aunque S3 no requiere que pase por tu backend, es buena práctica loguear cuándo se genera cada URL para trazabilidad.

Conclusión

Las presigned URLs son una herramienta poderosa para desacoplar la transferencia de archivos de tu backend, reduciendo carga en el servidor y mejorando la experiencia del usuario. La clave está en entender que GET y PUT resuelven problemas distintos —uno es para compartir, el otro para recibir— y que cada uno requiere sus propias consideraciones de seguridad.