Acceso denegado en CloudFront tras un despliegue limpio en S3: revisa la raíz del bucket
Un 403 tras sincronizar con S3 suele parecer un problema de permisos, pero a veces es solo un desajuste de rutas. Este es el caso y cómo resolverlo.

Has desplegado un build limpio en S3, lo has conectado a CloudFront con OAC y al abrir la URL te encuentras con un 403 Access Denied. Lo primero que piensas es que algo falla con los permisos, y te tiras horas repasando políticas IAM que están bien. Pero el problema puede estar en algo mucho más tonto: la estructura de carpetas dentro del bucket.
Eso es lo que le pasó recientemente a un compañero montando un pipeline de Bamboo para desplegar un frontend de Angular en S3 y CloudFront. El pipeline hacía el build, sincronizaba los artefactos con S3 y el bucket estaba configurado como sitio estático servido por CloudFront con Origin Access Control. El build y la sincronización funcionaban, los archivos estaban en el bucket... pero la URL del CDN devolvía exactamente ese 403.
Lo primero que revisó fue todo lo relacionado con IAM: la bucket policy con s3:GetObject para el principal de OAC, que el OAC estuviera correctamente asociado al origin, que el origin path apuntara al bucket. Todo correcto. Y ahí está la trampa: cuando los permisos están bien, puedes perder horas verificando políticas que nunca fueron el problema.
La causa real no estaba en los permisos, sino en dónde habían acabado los archivos dentro del bucket. El build de Angular, usando un builder basado en esbuild, había sacado el bundle de producción en una subcarpeta, no en la raíz del bucket. CloudFront, con su origin path configurado para servir desde la raíz, buscaba index.html en un sitio donde no existía.
Aquí está el detalle que confunde: S3 y CloudFront no distinguen esa situación de un fallo real de permisos. Un objeto ausente bajo un origin protegido con OAC devuelve exactamente el mismo Access Denied que un objeto al que no tienes acceso. No te da un 404. Por eso es tan fácil diagnosticarlo mal.
Cómo confirmar que es un problema de estructura y no de permisos: antes de tocar nada de IAM, ejecuta aws s3 ls s3://tu-bucket/ --recursive y mira dónde está tu index.html y tus assets. Si el origin path de CloudFront apunta a la raíz (campo vacío o /) pero los archivos están en una subcarpeta como /browser/ o /dist/, ya tienes la respuesta. También puedes comparar el campo Origin Path de la distribución con las claves reales de los objetos en el bucket.
La solución tiene dos caminos, según lo que controles: o ajustas la configuración de build para que los archivos acaben exactamente donde el origin espera (normalmente la raíz), o cambias el origin path de CloudFront para que apunte a la subcarpeta correcta: por ejemplo, /browser. El objetivo es que la clave de objeto que solicita CloudFront exista de verdad.
La conclusión es que un Access Denied en un sitio estático detrás de CloudFront y OAC no siempre es un bug de permisos: un objeto ausente bajo un origin protegido devuelve el mismo error que uno realmente bloqueado. Antes de tocar IAM, verifica con aws s3 ls --recursive que los archivos están donde tu origin path espera.
Y ojo: los cambios en las herramientas de build (un bundler nuevo, una versión actualizada de framework, un ajuste en la configuración) pueden cambiar silenciosamente las rutas de salida. Merece la pena revisar esto primero si el error aparece justo después de un cambio de este tipo.

