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-Typeal 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
- Expiración corta: entre 5 y 15 minutos suele ser suficiente para la mayoría de los casos de uso.
- Keys específicos, no genéricos: evita permitir subidas a rutas amplias; usa keys únicos (por ejemplo, con UUID) para prevenir sobrescrituras accidentales.
- Restringe el Content-Type en los PUT: así evitas que alguien suba un archivo ejecutable disfrazado de imagen.
- No reutilices URLs firmadas: genera una nueva por cada operación cuando el caso lo permita.
- 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.