7

Cabeceras HTTP seguras para Self-Hosted

Vistas: 0
Cabeceras HTTP seguras para Self-Hosted

Has configurado Traefik como puerta de entrada. Tienes rate limiting, whitelist de IP, circuit breaker. Tus servicios están protegidos contra abusos y accesos no autorizados. Pero hay un problema. Cuando un navegador se conecta a tu servidor, el diálogo entre ambos incluye unas pequeñas líneas de texto llamadas cabeceras HTTP. El servidor envía información sobre la respuesta: el tipo de contenido, cuánto dura la caché, qué servidor hay detrás. Y también puede enviar instrucciones de seguridad.

Instrucciones que le dicen al navegador: «no cargues este contenido en un iframe de otro sitio», «solo ejecuta scripts de este origen», «nunca uses HTTP para este dominio». Si no envías esas instrucciones, el navegador asume que todo está permitido. Y eso es peligroso. Un atacante puede incrustar tu web en un iframe y engañar a un usuario para que haga clic sin saberlo (clickjacking). Puede inyectar un script malicioso que robe cookies (XSS). Puede interceptar una conexión HTTP porque el usuario escribió http:// en lugar de https://.

Las cabeceras de seguridad HTTP son la primera línea de defensa a nivel de navegador. Y son gratis de implementar. Solo necesitas configurarlas una vez en Traefik y todos tus servicios las heredan automáticamente.

En este capítulo vas a aprender a configurar cada una de ellas, en qué consiste cada una, cómo combinarlas para obtener una calificación A+ en securityheaders.com, y cómo depurar cuando algo se rompe (porque la CSP siempre encuentra la forma de romper algo).

¿Qué son las cabeceras de seguridad HTTP?

Cada vez que un navegador pide una página a tu servidor, la respuesta HTTP incluye cabeceras. Son pares clave-valor que viajan antes del contenido:

HTTP/2 200 OK
content-type: text/html
content-length: 4523
server: nginx

Tu backend genera sus propias cabeceras, pero no siempre son las más seguras. De hecho, muchos backends envían cabeceras que revelan información sensible (X-Powered-By: PHP/8.2) o directamente no envían cabeceras de protección.

Aquí entra Traefik. Con el middleware headers puedes:

  • Añadir cabeceras de seguridad que tu backend no envía.
  • Sobrescribir cabeceras existentes con valores más seguros.
  • Eliminar cabeceras que revelan información del backend.

Todo ello sin tocar una línea del código de tu aplicación. El backend ni siquiera sabe que las cabeceras han sido modificadas.

¿Por qué debería importarte?

Piénsalo así. Tu servidor tiene una IP pública. Cualquiera con un navegador puede conectarse a él. Cuando lo hace, el navegador confía en las instrucciones que recibe.

Si no le dices «no te metas en iframes», cualquier sitio web puede incrustar tu página en un iframe. Un atacante crea un sitio que parece legítimo, pone tu página de login en un iframe transparente, y cuando el usuario hace clic en lo que cree que es un botón, está haciendo clic en el botón de login de tu servicio. Adiós credenciales.

Si no le dices «solo ejecuta scripts de este origen», cualquier script inyectado a través de un comentario, un campo de formulario o un parámetro URL se ejecutará sin cuestionarse. Eso es XSS en estado puro.

Si no le dices «usa siempre HTTPS», un atacante en la misma red WiFi puede interceptar la primera conexión HTTP y redirigir al usuario a una copia falsa de tu sitio. Eso es ataque man-in-the-middle.

Cada cabecera de seguridad cierra una puerta concreta. Y juntas, forman una barrera que hace que tu sitio sea mucho más difícil de atacar.

El middleware headers de Traefik v3

Traefik tiene un middleware específico para gestionar cabeceras HTTP. Se llama, adecuadamente, headers. Puedes definirlo tanto en Docker labels como en el archivo dinámico.

La estructura básica en el archivo dinámico es:

http:
  middlewares:
    headers-seguridad:
      headers:
        # Configuración aquí

Dentro del bloque headers, tienes varias secciones según lo que quieras hacer:

  • Cabeceras de seguridad nativas: opciones como strictTransportSecurity, contentTypeNosniff, browserXssFilter, customFrameOptionsValue, referrerPolicy. Traefik tiene atajos específicos para las cabeceras más comunes.
  • customResponseHeaders: para añadir cabeceras personalizadas a la respuesta. Aquí metes CSP, Permissions-Policy, y cualquier otra que no tenga atajo específico.
  • customRequestHeaders: para modificar cabeceras de la petición antes de que llegue al backend. Útil para añadir X-Forwarded-*.
  • accessControl*: para configurar CORS (Access-Control-Allow-Origin, etc.).

En este capítulo nos centramos en las cabeceras de seguridad. CORS lo veremos en un capítulo aparte cuando hablemos de APIs expuestas.

Cabecera por cabecera

Vamos a ver cada cabecera de seguridad, qué protege, cómo configurarla en Traefik, y qué valor usar.

Strict-Transport-Security (HSTS)

Esta es la cabecera más importante de todas. Le dice al navegador: «a partir de ahora, solo te conectes a este dominio por HTTPS, aunque el usuario escriba HTTP. Si intentas hacerlo por HTTP, bloquea la conexión».

Sin HSTS, un usuario que escriba tudominio.com en la barra del navegador hará primero una petición HTTP. Un atacante en la misma red puede interceptar esa petición y responder con un sitio falso antes de que el navegador reciba la redirección a HTTPS.

Con HSTS, el navegador sabe de antemano que solo debe usar HTTPS. Ni siquiera intenta la conexión HTTP.

headers:
  strictTransportSecurity:
    maxAge: 31536000
    includeSubDomains: true
    preload: true

Desglose de los parámetros:

  • maxAge: tiempo en segundos durante el cual el navegador recuerda esta política. 31536000 es un año. Puedes poner 63072000 (2 años) si quieres ser más agresivo.
  • includeSubDomains: aplica la política también a todos los subdominios. Si tudominio.com tiene HSTS, app.tudominio.com y api.tudominio.com también lo heredan.
  • preload: autoriza a que tu dominio sea incluido en la lista de precarga de HSTS que llevan integrada Chrome, Firefox, Safari y Edge. Una vez incluido, el navegador nunca hará una conexión HTTP a tu dominio, ni siquiera la primera vez.

Cuidado con preload. Una vez que tu dominio está en la lista de precarga, sacarlo es un proceso lento y tedioso. Asegúrate de que todos tus subdominios soportan HTTPS antes de activarlo. Si tienes un subdominio que solo funciona con HTTP, includeSubDomains lo romperá.

También puedes configurar HSTS como cabecera personalizada si prefieres el control manual:

headers:
  customResponseHeaders:
    Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"

Pero el atajo nativo de Traefik es más limpio y menos propenso a errores de sintaxis.

X-Frame-Options

Protege contra clickjacking. Este ataque consiste en incrustar tu página dentro de un iframe en otro sitio. El usuario cree que está haciendo clic en el sitio del atacante, pero en realidad está haciendo clic en tu página (transparente) dentro del iframe.

Con X-Frame-Options: DENY, le dices al navegador: «no permitas que esta página se cargue en ningún iframe, venga de donde venga».

headers:
  customFrameOptionsValue: DENY

Otra opción es SAMEORIGIN, que permite iframes solo desde el mismo origen. Útil si tu propia aplicación usa iframes para cargar contenido propio.

headers:
  customFrameOptionsValue: SAMEORIGIN

¿Cuándo usarías SAMEORIGIN? Si tienes un panel de administración que carga paneles de Grafana en iframes, o una aplicación que usa iframes para mostrar documentos. En ese caso, DENY rompería tu propia interfaz.

Para la mayoría de servicios self-hosted, DENY es la opción correcta. Si no estás usando iframes explícitamente, no los necesitas.

X-Content-Type-Options

Esta cabecera evita el MIME sniffing. Los navegadores antiguos tenían la mala costumbre de «adivinar» el tipo de un recurso en lugar de confiar en la cabecera Content-Type. Si el servidor decía que un archivo era text/plain pero el navegador detectaba que parecía HTML, lo renderizaba como HTML.

Un atacante podía subir un archivo con extensión .txt pero con contenido HTML y JavaScript. El servidor lo servía como text/plain, pero el navegador lo interpretaba como HTML y ejecutaba el script. XSS.

Con X-Content-Type-Options: nosniff, el navegador se obliga a respetar el Content-Type que envía el servidor. Si el servidor dice text/plain, el navegador lo trata como texto plano, aunque parezca HTML.

headers:
  contentTypeNosniff: true

Sencillo, pequeño, pero cierra una vulnerabilidad concreta. Actívalo siempre.

¿Te pongo un ejemplo real? En 2016, investigadores descubrieron que Safari en iOS y macOS tenía un comportamiento peculiar con los archivos CSV. Si un atacante subía un archivo con extensión .csv pero con contenido HTML y JavaScript incrustado, Safari lo renderizaba como HTML en lugar de tratarlo como texto plano. El MIME sniffing de Safari detectaba que el contenido parecía HTML y lo mostraba como tal, ejecutando cualquier script incrustado. Esto permitía ataques XSS persistentes en aplicaciones que permitían subir archivos CSV.

Otro caso histórico: Internet Explorer era especialmente agresivo con el MIME sniffing. Microsoft documentó vulnerabilidades donde un atacante podía servir un archivo de imagen (con Content-Type: image/png) que en realidad contenía HTML con JavaScript. IE lo detectaba como HTML y lo ejecutaba. Esto llevó a que Microsoft introdujera X-Content-Type-Options como respuesta, y con el tiempo todos los navegadores lo adoptaron.

Hoy, los navegadores modernos respetan mayoritariamente el Content-Type, pero la cabecera sigue siendo necesaria para navegadores antiguos y para cubrir casos extremos. Además, herramientas de auditoría como securityheaders.com la penalizan si no está presente. No hay razón para no incluirla.

Content-Security-Policy (CSP)

Esta es la cabecera más potente y, al mismo tiempo, la más compleja de configurar. La CSP (Content-Security-Policy) te permite controlar con gran detalle qué recursos puede cargar y ejecutar el navegador en tu página.

¿Quieres que solo se ejecuten scripts de tu propio dominio? CSP. ¿Quieres que las imágenes solo se carguen desde tu servidor y desde un CDN concreto? CSP. ¿Quieres evitar que alguien inyecte un script inline aunque encuentre un hueco XSS? CSP.

La CSP se define mediante directivas. Cada directiva controla un tipo de recurso:

  • default-src: valor por defecto para todas las directivas que no estén explicitadas. Si pones default-src 'self', todas las directivas (script, style, img, font, etc.) solo permiten el mismo origen.
  • script-src: controla qué orígenes pueden cargar scripts. La más importante para evitar XSS.
  • style-src: controla qué orígenes pueden cargar hojas de estilo.
  • img-src: controla qué orígenes pueden cargar imágenes.
  • font-src: controla qué orígenes pueden cargar fuentes.
  • connect-src: controla a qué orígenes puede conectarse el JavaScript (fetch, XHR, WebSockets).
  • frame-src: controla qué orígenes pueden cargarse en iframes.
  • object-src: controla la carga de plugins (Flash, Java). Poner 'none' es buena idea.
  • base-uri: controla el valor de la etiqueta <base>. Poner 'self' evita que un atacante cambie la base de las URLs relativas.

Una configuración CSP mínima para un servicio self-hosted sería:

headers:
  customResponseHeaders:
    Content-Security-Policy: "default-src 'self'; object-src 'none'; base-uri 'self'"

Pero esta configuración probablemente va a romper algo. ¿Por qué? Porque muchos servicios cargan recursos de terceros. Google Fonts, scripts de analytics, imágenes de CDNs, fuentes externas.

CSP para servicios típicos

Grafana: carga fuentes de Google, scripts propios, y hace conexiones WebSocket.

Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; connect-src 'self' ws://grafana.tudominio.com; img-src 'self' data:"

Nextcloud: carga scripts inline, imágenes de varios orígenes, y necesita conectarse a su propio Talk.

Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' https://nextcloud.tudominio.com; media-src 'self'"

WordPress: probablemente el más complejo, porque carga scripts y estilos de plugins, temas, CDNs, Google Fonts, etc.

Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://secure.gravatar.com; frame-src 'self' https://www.youtube.com"

No te obsesiones con que la CSP sea perfecta desde el primer día. Es mejor una CSP que cubre el 80% de los casos que ninguna CSP en absoluto.

Permissions-Policy (antes Feature-Policy)

Controla qué APIs del navegador puede utilizar tu página. Cámara, micrófono, geolocalización, acelerómetro, sensor de luz, etc.

Si tu servicio self-hosted no usa la cámara (y no debería, a menos que sea un servicio de videollamadas), ¿para qué permitir que cualquier script intente acceder a ella?

headers:
  customResponseHeaders:
    Permissions-Policy: "camera=(), microphone=(), geolocation=(), interest-cohort=()"
  • camera=(): deniega el acceso a la cámara.
  • microphone=(): deniega el acceso al micrófono.
  • geolocation=(): deniega el acceso a la geolocalización.
  • interest-cohort=(): deniega la inclusión en FLoC (Federated Learning of Cohorts), el sistema de tracking de Google. Por privacidad.

Si tienes un servicio que sí necesita alguna de estas APIs (por ejemplo, una aplicación de videoconferencias como Jitsi), puedes permitirlas solo para ese servicio:

Permissions-Policy: "camera=(self), microphone=(self)"

Esto permite cámara y micrófono solo al mismo origen. Cualquier script externo que intente acceder a ellas recibirá un error.

Referrer-Policy

Controla cuánta información envía el navegador en la cabecera Referer cuando un usuario hace clic en un enlace de tu sitio a otro sitio.

Sin esta cabecera, cuando un usuario hace clic en un enlace de tudominio.com a otro-sitio.com, el navegador envía la URL completa de tu página como referencia. Eso incluye rutas, parámetros de query, y cualquier información que pueda estar en la URL.

Si tu servicio tiene URLs del tipo app.tudominio.com/dashboard?token=abc123, ese token viajaría al sitio externo en la cabecera Referer. Un desastre de privacidad.

headers:
  referrerPolicy: strict-origin-when-cross-origin

¿Qué significa este valor?

  • strict-origin-when-cross-origin: cuando el usuario navega de tu sitio a otro sitio diferente (cross-origin), solo envía el origen (tudominio.com), no la URL completa. Cuando navega dentro de tu mismo sitio, envía la URL completa. Y cuando baja de HTTPS a HTTP, no envía nada.

Este es el valor recomendado por defecto. Cubre la mayoría de casos sin romper funcionalidades.

Otras opciones útiles:

  • no-referrer: nunca envía la cabecera Referer. La opción más restrictiva. Útil para paneles de administración o páginas sensibles.
  • same-origin: solo envía el referer dentro del mismo origen. Para sitios donde no quieres filtrar información a externos.
  • strict-origin: como el anterior, pero siempre envía solo el origen, incluso dentro del mismo sitio. Más restrictivo.

X-XSS-Protection

Esta cabecera es un caso curioso. Históricamente, los navegadores tenían un filtro integrado contra XSS reflejado. Cuando detectaban un intento de XSS, bloqueaban la página.

El filtro tenía más problemas que beneficios. Podía ser explotado por atacantes para causar denegación de servicio en páginas legítimas. Por eso, los navegadores modernos lo han ido eliminando.

Chrome lo eliminó en 2020. Edge lo mismo. Firefox nunca lo implementó. Hoy, la cabecera está obsoleta.

¿Por qué lo eliminó Chrome? El filtro XSS de Chrome tenía vulnerabilidades conocidas. Investigadores de seguridad documentaron múltiples formas de bypass: un atacante podía inyectar código que el filtro interpretaba erróneamente como un ataque XSS, provocando que Chrome bloqueara una página legítima entera. Era un vector de denegación de servicio. También había casos donde el propio filtro filtraba información sensible al intentar «sanitizar» la respuesta. Google llegó a la conclusión de que el filtro causaba más problemas de los que resolvía, y con una CSP bien configurada, era redundante. Así que lo eliminaron del todo.

Pero muchos scanners de seguridad y herramientas como securityheaders.com todavía la comprueban. Y si tienes usuarios con navegadores antiguos (Internet Explorer, Safari antiguo), aún puede tener sentido.

headers:
  browserXssFilter: true

El valor true en Traefik equivale a X-XSS-Protection: 1; mode=block. Le dice al navegador: «activa el filtro XSS y si detectas un ataque, bloquea la página completamente en lugar de intentar sanitizarla».

Si prefieres no enviar esta cabecera obsoleta, simplemente no la configures. Pero si quieres contentar a los scanners automáticos, actívala. No hace daño, aunque en navegadores modernos no haga nada.

Eliminar cabeceras que filtran información

Además de añadir cabeceras de seguridad, puedes eliminar cabeceras que tu backend envía y que revelan información sobre tu stack.

headers:
  customResponseHeaders:
    Server: ""
    X-Powered-By: ""
    X-AspNet-Version: ""

Poniendo el valor vacío, Traefik elimina la cabecera de la respuesta. Esto impide que un atacante sepa qué versión de PHP, Node o .NET estás usando. Si no sabe la tecnología, no puede buscar vulnerabilidades específicas.

Hay otras cabeceras que también filtran información y conviene eliminar. Por ejemplo, X-Cache revela si tu contenido se está sirviendo desde caché (valores como HIT, MISS, EXPIRED), lo que da pistas sobre la infraestructura de tu sitio. X-Request-Id expone identificadores internos de petición que un atacante podría usar para correlacionar solicitudes o rastrear el comportamiento del sistema. X-Drupal-Cache y X-Varnish delatan que estás usando Drupal o Varnish respectivamente, información valiosa para un atacante que busca vulnerabilidades específicas de esas plataformas.

Cuantas menos cabeceras informativas envíes, más difícil será para un atacante hacer reconocimiento de tu stack. Añádelas todas a la lista de eliminación:

headers:
  customResponseHeaders:
    Server: ""
    X-Powered-By: ""
    X-AspNet-Version: ""
    X-Cache: ""
    X-Request-Id: ""
    X-Drupal-Cache: ""
    X-Varnish: ""

Middleware de cabeceras por defecto para todos los routers

La mejor forma de aplicar cabeceras de seguridad es tener un middleware que se aplique a todas las rutas por defecto. Así te aseguras de que ningún servicio nuevo se quede sin protección.

Hay dos formas de hacerlo:

Opción 1: Middleware en el entrypoint

Configuras el middleware directamente en el entrypoint websecure. Todas las peticiones HTTPS pasan por él.

# traefik.yml
entryPoints:
  websecure:
    address: ":443"
    http:
      middlewares:
        - headers-seguridad@file

Así, cualquier router que use el entrypoint websecure tendrá automáticamente las cabeceras de seguridad. No necesitas añadir middlewares en cada router.

Opción 2: Chain de seguridad que incluyes en todos los routers

Defines una chain que incluya headers, rate limit, compress, etc., y la referencias en cada router.

http:
  middlewares:
    chain-seguridad:
      chain:
        middlewares:
          - headers-seguridad
          - ratelimit-global
          - compress-gzip

Luego en cada router:

labels:
  - "traefik.http.routers.mi-servicio.middlewares=chain-seguridad"

La opción 1 es más cómoda porque no requiere tocar cada router. La opción 2 te da más control: puedes decidir qué servicios tienen la chain completa y cuáles no.

Mi recomendación: usa la opción 1 para las cabeceras de seguridad (quieres que estén siempre presentes) y la opción 2 para middlewares adicionales como rate limit o compress.

CSP avanzada: depuración y modo report-only

La CSP es la cabecera que más dolores de cabeza va a darte. Es muy probable que la primera configuración que pruebes rompa algo: un botón que no funciona, un gráfico que no se renderiza, un formulario que no envía datos.

¿La razón? La CSP bloquea recursos que no están en la lista de permitidos. Si tu aplicación carga un script desde un CDN que no habías incluido en script-src, el script se bloquea silenciosamente. Sin errores visibles, solo funcionalidad rota.

Modo report-only

Antes de aplicar una CSP restrictiva, usa el modo report-only. Con Content-Security-Policy-Report-Only, el navegador informa de las violaciones pero no bloquea los recursos. Puedes ver qué se rompería sin romperlo realmente.

headers:
  customResponseHeaders:
    Content-Security-Policy-Report-Only: "default-src 'self'; report-uri /csp-report"

Además, con report-uri (o report-to en versiones modernas) puedes especificar un endpoint donde el navegador envía informes de violación en formato JSON.

{
  "csp-report": {
    "document-uri": "https://tudominio.com/dashboard",
    "violated-directive": "script-src-elem",
    "blocked-uri": "https://cdn.jsdelivr.net/npm/chart.js",
    "script-sample": "console.log('test')"
  }
}

Con esa información sabes exactamente qué recurso está siendo bloqueado y desde qué página.

Cómo depurar violaciones CSP

  1. Abre la consola del navegador (F12). Las violaciones CSP aparecen como warnings en la consola. Chrome las muestra en rojo con el mensaje «Refused to load… because it violates the following Content Security Policy directive».
  2. Usa el report-uri. Configura un endpoint que reciba los informes. Puedes usar servicios como https://report-uri.com o montar tu propio receptor con csp-report middleware.
  3. Ve iterando. Cada violación te dice qué origen necesitas añadir a tu CSP. Añádelo, prueba de nuevo, espera nuevas violaciones. Repite hasta que no haya más.
  4. No uses ‘unsafe-inline’ a menos que sea necesario. 'unsafe-inline' permite scripts inline, lo cual reduce la protección contra XSS. Si tu aplicación no necesita scripts inline directamente en el HTML, no lo pongas. Si los necesita (muchos frameworks modernos los usan), al menos combínalo con un nonce o un hash.

Nonces y hashes para scripts inline

Si tu aplicación necesita ejecutar scripts inline (etiquetas <script> en el HTML), no tienes que recurrir a 'unsafe-inline'. Puedes usar nonces o hashes.

Un nonce es un número aleatorio que se genera para cada petición. El servidor genera un nonce, lo incluye en la CSP y en la etiqueta <script>. El navegador solo ejecuta el script si el nonce coincide.

<script nonce="abc123">
  console.log("Este script se ejecuta");
</script>
Content-Security-Policy: script-src 'nonce-abc123'

Traefik no genera nonces automáticamente. Necesitas un plugin o un middleware externo para hacerlo. O puedes configurarlo a nivel de aplicación.

Un hash es una huella digital del contenido del script. Calculas el hash del script y lo incluyes en la CSP.

Content-Security-Policy: script-src 'sha256-xyz...'

Ventaja: no necesitas generar valores aleatorios. Desventaja: si el script cambia, cambia el hash y tienes que actualizar la CSP.

Para servicios self-hosted donde controlas el código, los hashes son más prácticos que los nonces. Pero para empezar, no te compliques: usa 'unsafe-inline' si es necesario y ve refinando más adelante.

Ejemplo completo en traefik-dynamic.yml

Aquí tienes el archivo completo con todos los middlewares de cabeceras de seguridad, listo para usar.

Nota importante sobre YAML: las claves duplicadas no están permitidas en YAML. Si necesitas añadir varias cabeceras personalizadas, agrupa todas bajo un único bloque customResponseHeaders. Así:

    headers-seguridad:
      headers:
        strictTransportSecurity:
          maxAge: 31536000
          includeSubDomains: true
          preload: true
        customFrameOptionsValue: DENY
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: strict-origin-when-cross-origin
        customResponseHeaders:
          Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
          Permissions-Policy: "camera=(), microphone=(), geolocation=(), interest-cohort=()"
          Server: ""
          X-Powered-By: ""

Nota: en navegadores modernos, la directiva CSP frame-ancestors prevalece sobre X-Frame-Options. Si usas CSP con frame-ancestors, la cabecera X-Frame-Options es redundante, pero mantener ambas no hace daño.

Y en los entrypoints de tu traefik.yml:

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          to: websecure
          scheme: https

  websecure:
    address: ":443"
    http:
      middlewares:
        - headers-seguridad@file
      tls:
        certResolver: letsencrypt

Con esto, todas las peticiones HTTPS que pasen por Traefik recibirán las cabeceras de seguridad. No importa si son 5 servicios o 50. Una configuración, todos protegidos.

Verificación: ¿está funcionando?

Has configurado las cabeceras. Pero, ¿cómo sabes que realmente se están enviando? No te fíes. Verifica.

Con curl

La forma más rápida de comprobar las cabeceras:

curl -sI https://tudominio.com | grep -i -E "(strict-transport|content-security|x-frame|x-content|x-xss|referrer|permissions|server|x-powered)"

Si todo está correcto, verás algo como:

strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; ...
x-frame-options: DENY
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), ...

Si ves la cabecera Server o X-Powered-By, es que no las has eliminado correctamente.

Puedes hacer una verificación más completa con Python si quieres ver todas las cabeceras:

python3 -c "import requests; r = requests.get('https://tudominio.com'); [print(f'{k}: {v}') for k, v in r.headers.items()]"

Con securityheaders.com

La herramienta de referencia. Ve a securityheaders.com e introduce tu dominio.

Te dará una calificación de la A a la F. Con las cabeceras que hemos configurado, deberías obtener una A+:

  • A: tienes las cabeceras principales (HSTS, X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy, Permissions-Policy).
  • A+: además de todas las anteriores, tienes HSTS con preload y includeSubDomains.

Si sacas menos de una A, la propia herramienta te dice qué cabeceras te faltan. Corrige una a una.

Con herramientas locales

Puedes usar curl con un script más completo para hacer una auditoría local:

#!/bin/bash
DOMAIN=$1
HEADERS=$(curl -sI "https://$DOMAIN")

check_header() {
  if echo "$HEADERS" | grep -qi "$1"; then
    echo "OK: $1"
  else
    echo "FALTA: $1"
  fi
}

echo "=== Auditoría de cabeceras de seguridad para $DOMAIN ==="
echo ""
check_header "strict-transport-security"
check_header "content-security-policy"
check_header "x-frame-options"
check_header "x-content-type-options"
check_header "x-xss-protection"
check_header "referrer-policy"
check_header "permissions-policy"

Guárdalo como audit-headers.sh, hazlo ejecutable y pásale tu dominio:

chmod +x audit-headers.sh
./audit-headers.sh tudominio.com

Desde Firefox Developer Tools

Si prefieres una comprobación visual sin salir del navegador, Firefox Developer Tools te lo pone fácil. Abre las herramientas de desarrollador (F12), ve a la pestaña Red (Network), recarga la página y selecciona cualquier petición. En el panel de la derecha, busca la sección Cabeceras de respuesta (Response Headers). Ahí verás todas las cabeceras que el servidor está enviando, incluyendo las de seguridad. Puedes confirmar que strict-transport-security, content-security-policy, x-frame-options y el resto están presentes con los valores correctos. Es una forma rápida de depurar sin instalar nada adicional.

Con la extensión Mozilla Observatory

Mozilla tiene una extensión para Firefox llamada Mozilla Observatory que analiza tu sitio y te da una puntuación detallada. También tiene versión web en observatory.mozilla.org.

Te evalúa no solo cabeceras, sino también TLS, cookies, y otras prácticas de seguridad. Es más completo que securityheaders.com.

Artículos relacionados

Si te ha interesado este artículo, te recomiendo echar un vistazo a estos otros artículos del blog:

Referencias y enlaces

Si quieres profundizar en la configuración de Traefik, te recomiendo el tutorial de Traefik v3 de atareao.es. Allí cubro en detalle la configuración de middlewares, incluyendo cabeceras de seguridad, desde el primer capítulo hasta el hardening final. Para la documentación oficial del middleware headers de Traefik v3, tienes la referencia de Traefik sobre el middleware headers. Y para la guía definitiva de CSP, el Content Security Policy Reference de Mozilla.

Conclusión

Las cabeceras de seguridad HTTP son gratis, fáciles de configurar en Traefik, y tienen un impacto enorme en la seguridad de tus servicios.

En este capítulo has visto:

  • HSTS: fuerza HTTPS. Con includeSubDomains y preload alcanzas el nivel máximo de protección.
  • X-Frame-Options: evita clickjacking. DENY para la mayoría de servicios, SAMEORIGIN solo si usas iframes propios.
  • X-Content-Type-Options: evita MIME sniffing. Siempre nosniff.
  • CSP: la más potente y compleja. Controla qué recursos puede cargar el navegador. Empieza con default-src 'self' y ve añadiendo orígenes según necesites.
  • Permissions-Policy: desactiva APIs del navegador que no uses. Cámara, micrófono, geolocalización.
  • Referrer-Policy: controla la información que se envía al hacer clic en enlaces. strict-origin-when-cross-origin es el valor recomendado.
  • X-XSS-Protection: obsoleta pero aún presente en scanners. Actívala si quieres contentar a las herramientas automáticas.
  • Eliminar cabeceras del backend: Server, X-Powered-By. Menos información, menos superficie de ataque.

Y has aprendido a configurarlo todo en el middleware headers de Traefik, aplicarlo globalmente desde el entrypoint, y verificar que funciona con curl, securityheaders.com y Mozilla Observatory. Las cabeceras de seguridad no son un lujo. Son un requisito mínimo para cualquier servicio expuesto a internet. Y con Traefik, configurarlas una vez y que se apliquen a todos tus servicios es cuestión de minutos.

Deja una respuesta