Strict-Transport-Security (HSTS) le dice al browser que tu sitio se accede SIEMPRE por HTTPS. La primera vez que un visitante llega por HTTPS y recibe el header, lo guarda; cualquier intento futuro de cargar el sitio por http:// se reescribe a https:// antes de salir del browser.
Es el control que cierra el último hueco que dejó la migración a HTTPS: el primer http:// que un usuario tipea o que un link viejo apunta. Sin HSTS, ese primer request va en claro y puede ser interceptado para hacer downgrade (TLS stripping).
El problema: HSTS es pegajoso. Una vez que un browser lo memoriza, no hay forma práctica de bajarse rápido si te equivocaste. Por eso vale la pena entender qué activás antes de pegarlo.
Anatomía del header
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Tres piezas, cada una con su trade-off:
max-age=63072000— segundos que el browser memoriza la política. 63072000 = 2 años. Lo mínimo razonable para producción es 6 meses (15552000). Menos de eso es práctica de “estoy probando”.includeSubDomains— extiende la regla a TODOS los subdominios del host actual. Es lo que sube la postura de “tu apex está protegido” a “tu zona DNS entera está protegida”.preload— declara intención de aparecer en la HSTS preload list. Los browsers traen esa lista hardcoded; tu sitio queda protegido antes del primer request. Es la única forma de cerrar el “primer-request gap” del 100%.
La trampa de includeSubDomains
Este es el campo que más se equivoca. Implica que cualquier subdominio bajo tu zona también va a forzarse a HTTPS.
Caso real que vemos a menudo:
cliente.clestá en HTTPS con cert válido. Se le activa HSTS conincludeSubDomains.cliente.cltiene unlegacy.cliente.clviejo, sin cert, sirviendo un sitio interno por HTTP plano (intranet, monitoreo Grafana, lo que sea).- A partir del primer visit a
cliente.cl, ese mismo browser ya no puede entrar alegacy.cliente.clpor HTTP. Y como no tiene cert TLS, tampoco por HTTPS. El subdominio queda inalcanzable hasta que el max-age expire.
La lógica defensiva es la correcta — un atacante que controle DNS del subdominio podría exfiltrar cookies del apex si HSTS no aplicara a subs. Pero en práctica te tomás el tiempo de inventariar todos tus subs antes de prender la regla.
El camino seguro: aumentar gradualmente
La receta que usamos en clientes:
Paso 1 — max-age corto, sin includeSubDomains, sin preload
Strict-Transport-Security: max-age=300
Cinco minutos. Si rompés algo, en 5 minutos los browsers olvidan. Lo dejás 24-48 horas en producción para verificar que el apex realmente sirve TLS bien (no hay request mixto, no hay redirect-loop, todo funciona).
Paso 2 — max-age 6 meses, sin includeSubDomains todavía
Strict-Transport-Security: max-age=15552000
Lo dejás 1-2 semanas. Esto ya te da la protección real para el apex.
Paso 3 — Inventario de subdominios + decisión sobre includeSubDomains
Antes de agregarlo, listá todos tus subs:
dig +short NS cliente.cl
dig +short ANY cliente.cl
# o consulta los CT logs:
curl -s 'https://crt.sh/?q=%25.cliente.cl&output=json' | jq -r '.[].name_value' | sort -u
Para cada uno, verifica:
- ¿Sirve HTTPS con cert válido?
- ¿Es un subdominio “vivo” o queda DNS huérfano? (los huérfanos no importan para HSTS, pero deberías limpiarlos por otras razones).
- ¿Hay alguno que SÓLO sirva HTTP por diseño? (intranets viejas, dashboards internos sin TLS).
Si todos sirven HTTPS bien, agregás includeSubDomains. Si hay alguno HTTP-only, primero lo migrás a HTTPS o lo retirás del DNS.
Strict-Transport-Security: max-age=15552000; includeSubDomains
Una semana más en este estado.
Paso 4 — max-age 2 años + preload
Cuando ya estás seguro de que todo el ecosistema soporta TLS y querés cerrar el primer-request gap:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Submitís el dominio en hstspreload.org. Toma 1-3 meses entrar en la próxima release de Chromium, después Firefox/Safari/Edge la heredan.
Importante: una vez en la preload list, salir es lento. Solicitar la remoción puede tomar meses y los browsers viejos pueden quedarte preloaded por años. No actives preload mientras dudes.
Config en Apache
Vía .htaccess o vhost
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</IfModule>
El flag always es importante: sin él, Apache aplica el header solo en respuestas 200; con always también lo manda en 30x/40x/50x, donde tradicionalmente se “olvida” — y es justamente en un 301 HTTP→HTTPS donde más útil resulta.
Redirect 301 obligatorio antes
HSTS asume que tu sitio responde por HTTPS. El redirect HTTP→HTTPS debe estar antes:
<VirtualHost *:80>
ServerName cliente.cl
ServerAlias www.cliente.cl
Redirect permanent / https://cliente.cl/
</VirtualHost>
O en .htaccess:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
El header HSTS debe servirse desde HTTPS (los browsers ignoran HSTS recibido por HTTP), así que el vhost :443 es el que lleva el Header always set.
Detrás de Cloudflare
Si estás detrás de CF (o cualquier CDN), tenés dos opciones para servir HSTS:
- Del origen: Apache pone el header, CF lo deja pasar. Pro: una sola fuente de verdad. Contra: si tu origen alguna vez se sirve directo (CF bypass), el header sigue saliendo.
- Del edge (CF dashboard): CF inyecta el header. Pro: aunque tu origen se sirva en HTTP por algún motivo, CF te cubre. Contra: si migrás de CDN o quitás CF, perdés el header sin enterarte.
En la práctica, ambos: el origen lo manda Y CF lo refuerza. Cloudflare detecta duplicados y deja uno solo.
Cómo verificás
Antes de pegarlo en producción
# verifica que el sitio sirve HTTPS bien
curl -sI https://cliente.cl/ | grep -i "strict-transport"
# verifica que ningún subdominio queda HTTP-only:
for sub in www app api blog dashboard mail; do
echo "--- $sub.cliente.cl ---"
curl -sI --max-time 5 https://$sub.cliente.cl/ 2>&1 | head -1
done
Después de pegarlo
# debería verse el header completo
curl -sI https://cliente.cl/ | grep -i "strict-transport"
# Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# verifica que el redirect 301 desde HTTP funciona
curl -sI http://cliente.cl/ | head -5
# debería devolver 301 hacia https://...
Antes de submitir a preload
hstspreload.org hace los checks por vos: header presente, max-age suficiente, includeSubDomains, preload, redirect 301, cert válido, subdominios accesibles por HTTPS. Si fallás algún check, te dice cuál — y mejor arreglarlo antes que después.
Salida de emergencia
Si pegaste HSTS y querés revertir:
- Antes de preload: bajá
max-agea 0 (no quites el header). Los browsers que recibanmax-age=0borran la entrada. Tardásmax-ageoriginal en cubrir a todos los visitantes que tuvieron la versión anterior. - Después de preload: hay que solicitar remoción en la HSTS preload list + esperar la siguiente release de Chrome (~6 semanas) + heredan los demás browsers (~3 meses) + los browsers viejos pueden quedarte protegidos por años. No hay rollback rápido.
Cómo lo detectamos en el FreeScan
El Asentic FreeScan reporta tres patrones distintos:
- HSTS ausente — Medium. El sitio funciona en HTTPS pero no protege el primer request.
- HSTS con max-age corto — Low. El header está, pero
max-age < 6 meseste deja una ventana de exposición innecesaria. - HSTS sin
includeSubDomainso sinpreload— Info. Eligible para subir la postura, no es un riesgo per se.
La distinción importa porque “no implementado” y “implementado pobre” se ven igual en una auditoría superficial, pero requieren acciones distintas.
Cierre
HSTS es de los headers con mejor ratio efecto/esfuerzo. La parte difícil no es el header en sí — son tres líneas de Apache — sino el inventario de subdominios que hay que hacer antes para no romper el ecosistema. Dedicarle un día a eso y migrar gradualmente vale el costo de la postura final.
Si tu sitio ya sirve HTTPS hace tiempo, lo más probable es que el camino al preload esté a una semana de distancia.