Del Build al Navegador
Lo que Caddy debería saber sobre tu sitio Astro
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Tamaño
Espaciado
Fuente
La brecha entre el build y la entrega
Tienes una página web. Carga, funciona, muestra lo que querías mostrar. Pero ¿sabes si los visitantes que vuelven ven tus páginas cargar al instante o descargan todo otra vez en cada clic? ¿Si la conexión se puede degradar a HTTP? ¿Si tu página de inicio aparece como un dominio o como dos en Google? Para la mayoría de sitios, las respuestas son: no, sí, y sí. Esa última capa del stack es la configuración del servidor web, y la mayoría de los negocios nunca la tocan.
Astro hace mucho trabajo en el build. Compila las páginas a HTML estático, pone un content-hash en el nombre de cada asset dentro del directorio _astro/, y produce archivos listos para cualquier servidor estático. La pipeline de CI añade otra capa: precomprime todo en archivos sidecar .br, .gz y .zst para que el servidor nunca tenga que comprimir on the fly.
Todo ese esfuerzo lo puede deshacer en silencio la última capa de la cadena: la configuración del servidor web. Caddy sirve los archivos, pero no sabe qué está sirviendo. No sabe que los archivos de _astro/ llevan content-hashes en el nombre. No sabe que el HTML cambia en cada deploy y el CSS no. No sabe que el sitio es solo HTTPS y que no hay ninguna razón para que aparezca dentro de un iframe.
Para esa brecha de conocimiento existe la config. Y la mía, durante un tiempo, se veía así:
(jav_static) { header /favicon.svg Cache-Control "max-age=3600, must-revalidate" file_server { precompressed br gzip zstd } handle_errors { rewrite * /404.html file_server }}Funcionaba. Los archivos se servían, los assets precomprimidos se recogían, la página 404 aparecía cuando debía. Pero poco más. Sin cabeceras de seguridad, sin estrategia de caché más allá del favicon, sin redirección www. El navegador tomaba decisiones que deberían haber sido mías. Y cualquiera que pasara un escáner de seguridad o midiera la velocidad de la página vería exactamente cuánto se estaba perdiendo.
Definiciones rápidas
¿Qué es una cabecera de seguridad?
Una cabecera de seguridad es una instrucción que tu servidor web envía con cada respuesta, y le dice al navegador cómo tratar la página. No tiene que ver con lo que la página muestra, sino con lo que los navegadores pueden hacer con ella: si puede aparecer en un iframe, si la conexión debe usar HTTPS, si solo se confía en ciertas fuentes. El navegador es quien lo aplica; tú lo configuras una sola vez en tu servidor.
¿Qué es la caché del navegador?
La caché del navegador consiste en que el navegador guarda una copia local de un archivo (una hoja de estilos, una fuente, una imagen) para no tener que volver a descargarlo en la siguiente visita. El servidor web controla cuánto tiempo esa copia sigue siendo válida y si el navegador debe preguntar al servidor por una versión más nueva antes de usarla.
¿Qué es un content-hash?
Un content-hash es una cadena corta incrustada en el nombre del archivo, derivada directamente del contenido del archivo. arc.FDYzXBdN.js lleva esa cadena porque el archivo contiene exactamente eso en este momento. Cambia el archivo, cambia el hash, y con él el nombre. El navegador lo ve como un archivo nuevo y baja la nueva versión, mientras que la copia cacheada con el nombre antiguo sigue siendo válida. Eso es lo que hace seguro cachear esos archivos para siempre.
Qué importa realmente en un sitio estático
Revisé las directivas de Caddy que estaba evaluando: headers, caching, compresión, logging, opciones TLS, timeouts del servidor, métricas de Prometheus. Logging, TLS, timeouts y métricas no pedían ninguna decisión para un sitio estático de este tamaño. Los valores por defecto de Caddy bastan. Las decisiones interesantes están en headers, caching y compresión. La pregunta no era qué puede hacer Caddy, sino qué necesita realmente mi sitio.
Un sitio estático sin login, sin formularios, sin datos de usuario y sin backend tiene un modelo de amenazas muy distinto al de una aplicación web. La mayoría de las guías de seguridad están escritas para la segunda. Aplicarlas a ciegas a un sitio estático significa añadir complejidad que no protege de nada.
Así que evalué cada opción contra una prueba sencilla: ¿esto aborda una amenaza real, o solo contenta a un escáner?
Tres cabeceras de seguridad, no seis
En internet hay listas de sobra que te dicen que añadas cada cabecera de seguridad que exista. Lo que entró, y lo que no.
Strict-Transport-Security aborda una amenaza real. Sin ella, un visitante en un WiFi público podría ver su conexión degradada a HTTP e interceptada. El sitio ya es solo HTTPS, así que esta cabecera solo le dice al navegador que nunca intente HTTP. Un año, incluyendo subdominios, con preload para entrar en la lista HSTS preload que los navegadores llevan integrada de fábrica. El coste es real: cada subdominio, actual y futuro, debe servir HTTPS o quedarse inaccesible. Salir de la lista preload no está en tus manos: ocurre en los ciclos de release de los navegadores, no en tu config. Aceptado aquí porque este sitio es solo HTTPS por intención, y Caddy provisiona certificados para cada subdominio de forma automática vía Let’s Encrypt.
X-Content-Type-Options: nosniff impide que los navegadores adivinen los tipos de contenido. Sin ella, un navegador podría interpretar un archivo de texto como código ejecutable. Un solo valor, sin configuración, sin concesiones.
X-Frame-Options: DENY impide que cualquiera incruste el sitio en un iframe. Evita el clickjacking, donde un atacante superpone una UI invisible encima de tu página para robar clics. Este sitio no tiene ninguna razón para aparecer dentro de un iframe, así que DENY es la respuesta correcta.
header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY"}Eso es todo. Tres cabeceras que cubren tres vectores de ataque reales. Las opciones descartadas y por qué:
Referrer-Policy ya trae por defecto strict-origin-when-cross-origin en los principales navegadores. Ponerla de forma explícita no cambia nada. Los sitios externos ven tu dominio como referrer (bueno para SEO), pero no la ruta completa. El comportamiento correcto ya está ahí, sin tocar la config.
Permissions-Policy desactiva APIs del navegador como cámara, micrófono, geolocalización. Pero un sitio estático no usa esas APIs. Bloquear algo que no usas no te protege de nada. Si un atacante puede inyectar JavaScript para acceder a la cámara, tienes problemas mucho mayores que una cabecera ausente. Y si incrustas vídeos de YouTube con pantalla completa activada, como hago yo, tendrías que ir definiendo excepciones con cuidado. Más complejidad para cero ganancia de seguridad.
Cross-Origin-Opener-Policy impide que otros sitios obtengan una referencia a tu ventana del navegador. Esto importa en sitios con flujos de autenticación donde un popup podría robar tokens. Un sitio estático no tiene auth, ni tokens, ni popups.
Content-Security-Policy es una allowlist en tiempo de ejecución para scripts, estilos, fuentes e imágenes. Contiene el cross-site scripting, bloquea la exfiltración de datos y atrapa código de terceros comprometido, independientemente de cómo haya llegado a la página. En sitios que renderizan datos no confiables (formularios, contenido de usuario, APIs externas) mitiga una superficie de ataque activa; en sitios sin esas vías de entrada, protege frente a eventos menos probables pero de mayor impacto: un script de analítica comprometido, un build manipulado, una CDN envenenada, un cambio futuro que añade en silencio una vía de entrada. El coste es real: cada recurso inventariado, ruptura silenciosa ante cualquier error. Astro 6 añadió una integración que cubre lo que Astro emite; los iframes, las fuentes externas y los servicios de terceros en runtime siguen siendo manuales. Inshallah CSP llegará cuando la auditoría encaje en el calendario, o antes si el modelo de amenazas cambia.
El patrón: cada cabecera descartada, o bien duplica un default del navegador, o bloquea algo que no es una amenaza, o requiere trabajo que aún no se ha hecho. Ninguna se rechazó porque la seguridad no importe. Se rechazaron porque no hacen lo que la gente cree que hacen. Al menos no en un sitio estático.
Estrategia de caché: deja que el build te guíe
Aquí es donde conocer el output de tu framework importa más.
Astro pone cada asset procesado en el directorio _astro/ con un content-hash en el nombre: JS, CSS, fuentes, SVGs, imágenes. arc.FDYzXBdN.js. Si el contenido cambia, el hash cambia, y el nombre también. La URL antigua no se reutiliza nunca.
Esto significa que el navegador puede cachear estos archivos para siempre. No «mucho tiempo». Literalmente para siempre. El hash garantiza que, si el contenido cambia, la URL también cambia, así que el navegador pedirá siempre la versión nueva. No hay riesgo de servir contenido caducado.
Sin esta regla, cada visitante que vuelve re-descarga tu CSS, JS y fuentes en cada carga. En una conexión móvil, esa es la diferencia entre un sitio que se siente instantáneo y uno que se siente lento. El contenido no ha cambiado desde su última visita, pero el navegador no lo sabe.
header /_astro/* Cache-Control "public, max-age=31536000, immutable"max-age=31536000 es un año. immutable le dice al navegador que ni siquiera se moleste en revalidar. Nada de enviar un request condicional, nada de comprobar si cambió, solo usar la copia cacheada. Esa es la política habitual para assets con content-hash.
Los archivos HTML son distintos. No tienen hash en el nombre. /en/blog/some-post/index.html es la misma URL a través de los deploys. Cuando publico una versión nueva, el HTML cambia, pero la URL no. Así que el navegador tiene que consultar al servidor antes de usar una copia cacheada.
header ?Cache-Control "no-cache"no-cache no significa «no cachees». Significa «cachéalo, pero pregúntale primero al servidor». Si el archivo no ha cambiado, Caddy devuelve un 304 Not Modified, una respuesta minúscula, prácticamente gratis. Si ha cambiado, el navegador recibe la versión nueva. El prefijo ? significa que esta regla se aplica solo cuando no se ha puesto otro Cache-Control, así que no sobrescribe las reglas de _astro/* ni la del favicon.
Los favicons están en medio. Sin hash, pero rara vez cambian:
header /favicon.svg Cache-Control "max-age=3600, must-revalidate"header /favicon.ico Cache-Control "max-age=3600, must-revalidate"Tres reglas, y cada una sale directamente de lo que el build de Astro produce. Los archivos con hash se cachean para siempre. Los archivos sin hash se revalidan. Esa es toda la estrategia.
Los detalles
Sin una redirección www, www.javedab.com y javedab.com son sitios separados a ojos de Google. El link equity se reparte entre dos dominios. Una redirección 301 permanente consolida todo en una URL canónica y preserva la ruta completa.
www.javedab.com { redir https://javedab.com{uri} permanent}La pipeline de CI precomprime todos los assets, y Caddy sirve los archivos sidecar directamente. Pero si un archivo, por lo que sea, no está precomprimido, un fallback de compresión debería comprimirlo on the fly en lugar de servirlo sin comprimir. La directiva encode es una red de seguridad. Debería activarse en contadas ocasiones, y si lo hace, significa que algo en la pipeline de build necesita revisión.
encode zstd gzipEl email de ACME es fácil de pasar por alto. Caddy gestiona los certificados TLS de forma automática vía Let’s Encrypt. Si la renovación falla, el sitio se cae con un error TLS. Los visitantes ven un aviso del navegador, y algunos no vuelven. Sin email en la cuenta de ACME, ese fallo es silencioso. Te enteras cuando un cliente te dice que tu sitio se ve roto. Una línea en la config global lo convierte en un aviso que te llega antes de que eso pase.
{ email javed@javedab.com}Cómo queda la config ahora
(jav_static) { header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" }
header /_astro/* Cache-Control "public, max-age=31536000, immutable" header /favicon.svg Cache-Control "max-age=3600, must-revalidate" header /favicon.ico Cache-Control "max-age=3600, must-revalidate" header ?Cache-Control "no-cache"
encode zstd gzip file_server { precompressed br gzip zstd } handle_errors { rewrite * /404.html; file_server }}Cada línea tiene una razón. Nada está ahí porque un post de blog haya dicho que lo pusieras. Las cabeceras de seguridad cubren amenazas reales. Las reglas de caché cuadran con el output del build. El fallback de compresión es una red de seguridad, no la vía principal.
Una llamada de curl confirma que las políticas están activas al momento de escribir esto:
$ curl -sI https://javedab.com/ | grep -iE '^(cache-control|strict-transport|x-frame)'cache-control: no-cachestrict-transport-security: max-age=31536000; includeSubDomains; preloadx-frame-options: DENYLa idea
La configuración de tu servidor web debería reflejar lo que produce tu pipeline de build. Los defaults genéricos funcionan, pero dejan al navegador decisiones que deberían ser tuyas. Los defaults de Caddy cubren lo difícil: HTTP/3 está activo, el TLS es automático, los archivos sidecar precomprimidos se soportan de forma nativa. Pero el caching, las cabeceras de seguridad y las redirecciones son cosas que solo tú puedes configurar, porque solo tú sabes qué produce tu framework y qué necesita tu sitio.
Leer la documentación, evaluar cada opción, implementar las que importan, verificar con curl. La config ha pasado de unas cuantas líneas funcionales a algo que refleja este sitio en concreto, no una plantilla genérica.
Nada de esto es visible para un visitante casual. El sitio se veía igual antes y después. Pero es visible para cualquiera que revise. Un posible socio que pasa tu dominio por securityheaders.com. Un cliente que abre DevTools y ve immutable en tus assets. El tipo de persona que nota si la infraestructura detrás de la página está tan pensada como la página misma.
Eso es lo que significa tener tu infraestructura en tus manos. No complicado. Simplemente intencional.
¿Funciona esta estrategia de caché con frameworks distintos a Astro?
Sí, siempre que tu framework genere assets con content-hash. Next.js, SvelteKit, Nuxt y la mayoría de las herramientas basadas en Vite lo hacen por defecto. Busca en el output del build un directorio como `_next/static/`, `_app/immutable/` o similar con nombres de archivo hasheados. Las reglas de Caddy son idénticas; solo cambia el patrón de ruta. Los archivos HTML no se hashean en ningún framework, así que la regla `no-cache` se aplica universalmente.
¿Cómo verifico que mis cabeceras y reglas de caché están realmente activas?
Lo más rápido es curl: `curl -sI https://tudominio.com/ | grep -iE '^(cache-control|strict-transport|x-frame)'`. Cada cabecera que hayas configurado debería aparecer en la salida con el valor que pusiste.
Para una imagen más completa, pega tu dominio en [securityheaders.com](https://securityheaders.com). Califica cada cabecera y señala lo que falta. Las DevTools del navegador (pestaña Red, cualquier petición de asset) muestran el valor de Cache-Control que Caddy devolvió para ese archivo concreto. Eso sirve para confirmar que las reglas `_astro/*` y `no-cache` están funcionando bien.
¿Cuáles son los riesgos de activar HSTS preload?
El mayor riesgo es la irreversibilidad a corto plazo. Una vez que entras en la lista preload y los navegadores publican la actualización, cualquier subdominio sin HTTPS válido se vuelve inaccesible: no solo se redirige, sino que queda bloqueado a nivel del navegador antes incluso de intentar la conexión. Salir de la lista requiere una solicitud y luego esperar a los ciclos de release de los navegadores, lo que puede llevar meses.
Acéptalo solo si cada subdominio actual y futuro va a servir HTTPS. Si tienes subdominios que no controlas del todo, o herramientas internas que solo corren por HTTP, deja fuera `includeSubDomains` y `preload`, y usa la cabecera sin esos flags.
Web 2 de 3
Volver a WebIngeniero aeroespacial
Emprendedor ético en público
Tú te ocupas de tu negocio
Yo me encargo del lado digital
Trabaja conmigo
- IA con honestidad
- Infraestructura privada
- Sitios web que rinden
Cuéntame sobre tu situación:
javed@javedab.com Más sobre mí