Autenticación S3 en el Origen del CDN — Servir Desde Buckets Privados
Si tu origin es un object store (AWS S3, Cloudflare R2, Wasabi, Backblaze B2, MinIO, etc.) y el bucket es privado, el CDN necesita autenticar cada petición de descarga con AWS Signature Version 4 (SigV4). Con la Autenticación S3 en el Origen le das al CDN las credenciales del bucket una sola vez, y a partir de ahí cada cache miss se firma de forma transparente — los visitantes nunca ven las credenciales, tu bucket sigue siendo privado, y no tienes que hacerlo público para que funcione.
Esta guía explica cuándo usarlo, cómo configurarlo, y cómo evitar los 403 / 404 / 421 más típicos al primer intento.
Cuándo lo necesitas
| Tu origin | ¿Necesitas esto? |
|---|---|
| Tu propio servidor web (Nginx, Apache, API custom) | No — el origin sirve por HTTPS normal, sin firma |
| Bucket S3-compatible público (has activado public access) | No — cualquiera lo descarga con un GET plano |
| Bucket S3-compatible privado (requiere firma AWS para leer) | Sí — sin esto cada petición se va con 403 |
Proveedores soportados
Cualquier object store que hable AWS SigV4 sobre una API S3 funciona out-of-the-box:
- AWS S3 — endpoint
s3.<region>.amazonaws.com - Cloudflare R2 — endpoint
<account_id>.r2.cloudflarestorage.com(o<account_id>.eu.r2.cloudflarestorage.compara jurisdicción EU) - Wasabi — endpoint
s3.<region>.wasabisys.com - Backblaze B2 (API S3-compatible) — endpoint
s3.<region>.backblazeb2.com - MinIO (self-hosted) — tu propio endpoint
- Cualquier otro S3-compatible — basta con que aceptes pasar el endpoint URL
Configurar el origin
Añade o edita un origin en tu zona CDN: panel → pestaña Origins → Añadir Origen (o icono de lápiz para editar).
La primera parte es igual que cualquier otro origin:
| Campo | Valor |
|---|---|
| Nombre | Texto libre. Cualquier cosa que te ayude a reconocer este origin. |
| Origin URL | https://<tu-endpoint> (sin path final). Ejemplo: https://s3.eu-central-1.amazonaws.com |
| Cabecera Host | Normalmente déjalo en blanco — se auto-rellena con el hostname del endpoint. No pongas tu dominio del CDN. |
| Base path | /<nombre-del-bucket> si las URLs que ven tus clientes no incluyen el nombre del bucket. Mira la sección de abajo. |
Después expande la sección Bucket privado (S3 SigV4) al final y rellena:
| Campo | Valor |
|---|---|
| Activar firma SigV4 | Toggle on |
| Access key ID | La access key estilo S3 (parece un identificador — seguro mostrarlo) |
| Secret access key | El secret estilo S3. Solo escritura — una vez guardado, no lo volvemos a mostrar |
| Región | Depende del proveedor. Para R2 déjalo en blanco (defaultea a auto). Para AWS / Wasabi / Backblaze usa la región del bucket (us-east-1, eu-central-1, etc.) |
| Servicio | s3 (default — funciona para todos los proveedores soportados) |
Guarda. En unos segundos el CDN propaga las nuevas credenciales a todos los PoPs y empieza a firmar peticiones de forma transparente.
Path mapping (el error más común)
El primer segmento del path de la URL es lo que S3 considera el nombre del bucket. Así que si tu URL del CDN es:
https://tu-zona.cubecdn.io/videos/clip.mp4
El CDN reenvía /videos/clip.mp4 a tu origin. S3 lee:
- bucket =
videos - key (path del objeto dentro del bucket) =
clip.mp4
Si en realidad tienes los ficheros bajo el bucket mi-media, la petición busca el bucket videos que no existe → 403 / 404 / "InvalidBucketName".
Tienes dos formas de arreglarlo:
Opción A — Usar el nombre del bucket como primer segmento de tus URLs
Lo más limpio. Si tu bucket es mi-media, los clientes piden:
https://tu-zona.cubecdn.io/mi-media/path/to/file.mp4
Deja Base path vacío en el origin. Ficheros como path/to/file.mp4 se leen del bucket mi-media. Funciona si no te importa exponer el nombre del bucket en las URLs.
Opción B — Ocultar el nombre del bucket con Base path
Si quieres URLs más cortas (sin nombre del bucket visible para clientes), pon:
Base path=/mi-media
Entonces los clientes piden:
https://tu-zona.cubecdn.io/path/to/file.mp4
Y el CDN reescribe el path a /mi-media/path/to/file.mp4 antes de reenviarlo a S3. El nombre del bucket nunca aparece en las URLs de tus clientes.
Elegir los permisos correctos del token / API key
Cuando creas las credenciales en tu proveedor S3, dales los permisos mínimos imprescindibles:
- Object Read — suficiente si el CDN solo descarga objetos existentes (este es el caso común)
- Object Read & Write — solo si tu aplicación usa también las mismas credenciales para uploads desde otro sitio
- Admin — evítalo
Y limita el scope de las credenciales a solo el bucket (o buckets) que vas a servir desde esta zona CDN. En Cloudflare R2 usa "Specify bucket" y elige el bucket; en AWS usa una política IAM con Resource: "arn:aws:s3:::mi-media/*". No le des acceso a toda tu cuenta.
Actualizar credenciales (rotación)
Cuando regeneres las credenciales S3 en el proveedor (buena práctica de seguridad cada pocos meses), actualízalas en el panel igual que las pusiste por primera vez:
- Abre el origin → editar → expandir la sección Bucket privado (S3 SigV4)
- Sustituye el Access key ID por el nuevo
- Pega el nuevo Secret access key. Dejar este campo en blanco mantiene el actual — solo rellénalo si estás rotando
- Guarda
Propagamos las nuevas credenciales en ~30 segundos. Las respuestas cacheadas de antes de la rotación se siguen sirviendo desde caché mientras su TTL siga vivo — solo las descargas frescas usan las nuevas credenciales.
Jurisdicciones de Cloudflare R2
R2 tiene dos jurisdicciones: default y EU. Los buckets se crean en una u otra, y el endpoint URL es distinto:
- Default:
<account_id>.r2.cloudflarestorage.com - EU:
<account_id>.eu.r2.cloudflarestorage.com
Si apuntas el CDN a la jurisdicción equivocada, R2 devuelve 421 Misdirected Request incluso con credenciales correctas. Comprueba la jurisdicción de tu bucket en el dashboard de Cloudflare (R2 → tu bucket → Settings → Location) y usa el endpoint que corresponda.
Además: los tokens API están scoped a una jurisdicción. Un token creado desde la vista R2 default no funciona contra buckets EU y viceversa. Si migras un bucket entre jurisdicciones, regenera también el token.
Errores típicos y qué significan
| Status / Code | Significado | Fix |
|---|---|---|
403 AccessDenied (cuerpo XML) | S3 recibió una firma válida pero las credenciales no tienen permiso sobre este bucket/objeto | Comprueba el scope del token y los permisos sobre el recurso |
403 SignatureDoesNotMatch | El cálculo de la firma está mal | Vuelve a introducir el secret access key; comprueba que la región sea la correcta para el bucket |
400 InvalidArgument: Authorization | La petición llegó a S3 sin auth — faltaban credenciales | Guarda de nuevo el origin con credenciales; verifica que el toggle SigV4 está activo |
404 NoSuchBucket | El primer segmento del path no coincide con ningún bucket de tu cuenta | Mira "Path mapping" arriba |
404 NoSuchKey | El bucket está bien pero el objeto no existe en ese path | Verifica la key exacta en tu bucket — las mayúsculas cuentan |
421 Misdirected Request (Cloudflare R2) | Endpoint de jurisdicción equivocada, o account ID erróneo en la URL | Mira "Jurisdicciones de Cloudflare R2" arriba |
503 Service Unavailable | Origin inalcanzable desde el edge | Mira la página de status de tu proveedor; verifica el endpoint URL |
El cuerpo XML de la respuesta suele decirte el <Code> y <Message> exactos. Si solo recibes una respuesta HTML, eso significa que el error vino de una capa por delante de S3 (típicamente mismatch de routing / SNI — mira el caso del 421).
Limitaciones
- Todos los origins con autenticación S3 en la misma zona deben compartir las mismas credenciales. El CDN firma por backend, no por server, así que si listas dos origins ambos con SigV4 activo necesitan el mismo access key + región + endpoint. Para buckets separados con credenciales separadas, crea zonas CDN separadas.
- El secret access key es solo escritura. Después de guardar el origin no lo volvemos a mostrar. Si lo olvidas, regenéralo en el proveedor y vuelve a introducirlo.
- Solo peticiones GET / HEAD. El CDN no firma PUT/POST/DELETE — es una red de entrega, no un pipeline de upload.
Referencia de API
POST /cdn/zones/{uuid}/origins crear origin
PATCH /cdn/zones/{uuid}/origins/{origin_uuid} actualizar origin
Campos del body para ambos:
{
"s3_auth_enabled": true|false,
"s3_access_key": "AKIAXXXXXXXXXXXXX",
"s3_secret_key": "<solo-escritura, vacío en PATCH para mantener actual>",
"s3_region": "us-east-1" // o vacío para R2 (defaultea a "auto")
}
s3_secret_key vacío en PATCH = mantiene el valor persistido. Nunca se devuelve en ninguna respuesta GET.
