Publicar una web estática parece sencillo: generar los archivos, subirlos a Amazon S3 y servirlos mediante CloudFront. Sin embargo, basta una diferencia entre el nombre de un archivo y el enlace que lo apunta para que una página deje de abrirse.
En CloudCompute.es encontramos un caso concreto: el archivo de un artículo tenía un espacio al principio del nombre. El enlace de la portada apuntaba al nombre sin ese espacio. La página devolvía AccessDenied, mientras el resto de la web seguía accesible.
1. Un error 403 no identifica por sí solo la causa
CloudFront puede devolver un 403 por problemas de acceso al origen, rutas incorrectas, objetos ausentes o bloqueos de seguridad. El mensaje es el punto de partida; todavía no indica qué configuración debemos cambiar.
En S3, una petición a un objeto que no existe puede devolver 403 Forbidden si el solicitante no dispone de s3:ListBucket. Con ese permiso, la respuesta para el objeto ausente es 404 Not Found. No hace falta conceder acceso público al listado del bucket para diagnosticarlo: utiliza una identidad administrativa autorizada.
2. Comprueba el nombre del archivo local
Estos ejemplos usan nombres ficticios. Sustituye el dominio, el bucket, el identificador de distribución y mi-articulo.html por los de tu proyecto. Ejecuta los comandos locales desde la carpeta de la web.
find . -type f -iname '*mi-articulo*'
ls -lb ./mi-articulo.html
Un resultado como ./ mi-articulo.html contiene un espacio justo después de la barra. Es un archivo distinto de ./mi-articulo.html. También debes comprobar mayúsculas, extensión y directorio.
Si has confirmado ese espacio inicial, puedes corregirlo con este comando. La opción -n evita sobrescribir otro archivo que ya tenga el nombre de destino.
mv -n './ mi-articulo.html' './mi-articulo.html'
ls -lb './mi-articulo.html'
Revisa después el enlace generado en la portada:
grep -n -C 3 'mi-articulo' index.html
Si utilizas un generador, corrige sus datos de entrada y vuelve a ejecutarlo. Una edición manual del HTML generado puede desaparecer en la siguiente generación.
3. Verifica el objeto exacto en Amazon S3
Antes de cambiar políticas, consulta la clave que debería existir:
aws s3api head-object \
--bucket MI_BUCKET \
--key 'mi-articulo.html' \
--query '{Bytes:ContentLength,Tipo:ContentType,Modificado:LastModified}' \
--no-cli-pager
Una respuesta correcta confirma que esa identidad de AWS CLI puede consultar el objeto. No demuestra todavía que CloudFront tenga acceso. Si devuelve 403, el resultado es ambiguo: comprueba tus permisos y la existencia del objeto con una identidad autorizada.
Para un bucket pequeño, este listado permite ver las claves que contienen el nombre del artículo. La salida JSON ayuda a detectar espacios:
aws s3api list-objects-v2 \
--bucket MI_BUCKET \
--query "Contents[?contains(Key, 'mi-articulo')].Key" \
--output json \
--no-cli-pager
Si el archivo corregido existe localmente, súbelo a la clave exacta:
aws s3 cp './mi-articulo.html' \
's3://MI_BUCKET/mi-articulo.html' \
--content-type 'text/html; charset=utf-8' \
--no-cli-pager
4. Si el objeto existe, revisa el acceso de CloudFront
Para un bucket privado con origen REST de S3, revisa el Origin Access Control (OAC), la firma de solicitudes y la política del bucket. Esta debe autorizar s3:GetObject al servicio CloudFront para la distribución correspondiente. Si el objeto utiliza SSE-KMS, revisa también la política de la clave KMS.
Un origen de tipo S3 website endpoint tiene otro modelo: se configura como origen personalizado y no admite OAC. No mezcles instrucciones de ambos modelos ni conviertas un bucket privado en público para probar.
aws cloudfront get-distribution-config \
--id MI_DISTRIBUCION \
--query 'DistributionConfig.{Origins:Origins,DefaultBehavior:DefaultCacheBehavior,OtherBehaviors:CacheBehaviors,Restrictions:Restrictions,WebACLId:WebACLId}' \
--output json \
--no-cli-pager
Comprueba qué origen selecciona el comportamiento que coincide con la ruta del artículo, y si hay un OriginPath que añada un prefijo. Por ejemplo, con /public, una petición a /mi-articulo.html buscará el objeto bajo ese prefijo.
Si hay AWS WAF, revisa las solicitudes bloqueadas y sus reglas. Comprueba también las restricciones geográficas y si el comportamiento exige URL o cookies firmadas. No desactives controles sin identificar cuál rechaza la petición.
5. Invalida la ruta después de corregir el origen
Una invalidación permite que CloudFront vuelva a consultar el origen en lugar de seguir sirviendo una respuesta almacenada. No sube archivos ni corrige permisos.
aws cloudfront create-invalidation \
--distribution-id MI_DISTRIBUCION \
--paths '/mi-articulo.html' \
--no-cli-pager
El comando devuelve un identificador de invalidación. Puedes esperar a que termine con:
aws cloudfront wait invalidation-completed \
--distribution-id MI_DISTRIBUCION \
--id ID_INVALIDACION \
--no-cli-pager
Después, realiza una petición GET y revisa tanto el estado como el contenido recibido:
curl -sS -D /tmp/cloud-articulo-cabeceras.txt \
-o /tmp/cloud-articulo-respuesta.html \
'https://TU_DOMINIO/mi-articulo.html'
cat /tmp/cloud-articulo-cabeceras.txt
head -n 12 /tmp/cloud-articulo-respuesta.html
Comprueba que aparece el HTML del artículo esperado. Un estado 200 por sí solo no basta si una configuración de errores está sustituyendo la respuesta por otra página. Si el navegador conserva una versión anterior, recuerda que la invalidación de CloudFront no elimina su caché local.
6. Detecta nombres problemáticos antes de publicar
Este control revisa los archivos HTML y termina con un error si encuentra espacios al principio o al final del nombre. Guárdalo como automatizacion/validar_nombres.py:
from pathlib import Path
import sys
raiz = Path(__file__).resolve().parents[1]
errores = []
for archivo in raiz.rglob('*'):
if not archivo.is_file():
continue
nombre = archivo.name
if nombre.strip().lower().endswith('.html'):
if nombre != nombre.strip():
errores.append(str(archivo.relative_to(raiz)))
if errores:
for ruta in errores:
print(f'Nombre HTML con espacios en los extremos: {ruta!r}')
sys.exit(1)
print('Nombres HTML comprobados.')
En tu script de publicación, ejecuta esta comprobación antes de generar o subir archivos. El script debe detenerse si falla:
python3 automatizacion/validar_nombres.py || exit 1
Esta validación detecta el error concreto de los espacios, pero no sustituye una comprobación de enlaces, imágenes y metadatos. El objetivo es incorporar al proceso una prueba útil cada vez que un incidente revela un fallo repetible.
Qué nos enseña este caso
La secuencia de diagnóstico es sencilla: URL solicitada, archivo local, clave en S3, acceso de CloudFront y respuesta pública. Seguir ese orden permite comprobar hipótesis concretas y evita modificar permisos por un problema de nombres.
Una publicación termina cuando el contenido correcto se puede abrir desde su URL pública. Haber ejecutado la subida es solo una parte del proceso.