Lo que Caddy debería saber sobre tu sitio Astro
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
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.
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.
Ingeniero 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í