La CSP que solo se rompía en producción
Agregar encabezados de seguridad debía ser el issue aburrido. No existía ningún encabezado, así que ya tenía una política sugerida resuelta desde un spike anterior, y la tarea parecía ser copiar y pegar una cadena de Content-Security-Policy en un archivo de configuración. No se quedó aburrido.
La primera sorpresa fue estructural. Este sitio es Astro sobre Cloudflare Workers, y el plan pedía dos mecanismos: middleware para las páginas renderizadas en el servidor, un archivo _headers para las estáticas. Fui a escribir el middleware y antes revisé algo: cada una de las rutas de este sitio está prerenderizada. No la mayoría, todas. El middleware en Astro solo corre para páginas renderizadas bajo demanda, y no hay ninguna. Lo escribí de todos modos, para cuando aparezca una ruta dinámica real, pero lo etiqueté con honestidad: hoy no hace nada. El archivo _headers es lo único que toca una sola solicitud real.
La segunda sorpresa vino de leer de verdad la documentación de Cloudflare en vez de asumir que ya entendía la sintaxis de _headers. Quería una política estricta en todos lados, y una más permisiva solo para la carpeta que contiene un juego de Unity WebGL, ya que WebAssembly necesita permisos que una página normal no necesita. Mi instinto fue agregar una segunda regla para esa carpeta con los permisos extra añadidos. La documentación de Cloudflare me detuvo: las reglas que coinciden con la misma solicitud no se sobrescriben entre sí, se combinan, y los encabezados duplicados se unen con comas. Un navegador que lee dos CSP unidas por comas no elige una, aplica la intersección, la versión más estricta de cada regla. Mi política “más permisiva” para el juego habría quedado silenciosamente reducida de vuelta a la estricta. Existe una sintaxis real para anular esto, un signo de exclamación antes del nombre del encabezado, que borra lo anterior para poder escribir algo genuinamente distinto. Sin haberlo revisado, habría publicado una política que se veía permisiva en el archivo y se comportaba exactamente tan restrictiva como la que supuestamente estaba aflojando.
Incluso con eso arreglado, que el encabezado se viera bien en un archivo de texto no decía nada sobre si el juego iba a funcionar. Así que lo cargué. Chrome sin interfaz, apuntado directo al documento HTML propio del juego, leyendo lo que la consola realmente decía. La primera corrida falló de inmediato: el juego trae su propio script pequeño en línea, y mi política no permitía scripts en línea. Lo volví a abrir y lo permití, solo para esa ruta. La segunda corrida, más tarde, falló de nuevo, esta vez más adentro, Unity levanta un web worker desde una URL blob, y resulta que el permiso para crear el worker no es el mismo permiso que el worker necesita para cargar su propio código. Ese segundo caso vive bajo script-src, no worker-src, y nada de la primera falla lo habría indicado; solo lo encontré porque dejé que la página corriera lo suficiente para llegar a ese paso.
La última falla fue la que ninguna prueba local podría haber detectado. Todo pasaba contra el runtime real de Cloudflare corriendo en mi máquina. Fusioné, desplegué, y apunté un navegador sin interfaz al sitio real en producción para cerrar el círculo, y falló en algo nuevo: Cloudflare mismo inyecta un script de analítica en cada respuesta real, un paso que solo ocurre en su edge, nunca en la vista previa local. Mi política no sabía que ese script existía porque no había forma de saberlo, salvo llegando hasta producción.
Cuatro fallas distintas, cuatro razones distintas por las que un encabezado que “se ve bien” no lo estaba, y cada una de ellas solo salió a la luz porque insistí en observar la carga real de la página en vez de confiar en que el archivo estaba correcto. El último arreglo salió como un push de seguimiento rápido una vez que el sitio ya estaba en producción, permitió el script extra, y confirmó todo limpio.
Lecturas relacionadas
El error de CSP que no me correspondía arreglar
Una lista de lanzamiento, una violación de Content Security Policy que señalaba el script exacto que acababa de editar, y un comentario en el repositorio que habría evitado todo el desvío.
Convertir un hash de CSP en una salida del build en vez de mantenerlo a mano
Un botón de lanzamiento del juego se rompió en silencio dos días antes de que alguien lo notara. El arreglo no fue recalcular un hash, fue asegurarse de que nadie tuviera que volver a hacerlo.
Cerrar el círculo de la sección Open Source, y el 404 que no podía correr código de servidor
Localizar una página de error sin ningún servidor involucrado, una verificación de ruta que se habría roto en /estimates, e infraestructura publicada a sabiendas de que todavía no hace nada.