5

Middlewares básicos

Vistas: 0

Hasta ahora has visto cómo Traefik enruta el tráfico desde los EntryPoints hasta tus servicios a través de los Routers. Pero, ¿qué pasa si quieres modificar la petición antes de que llegue a su destino? ¿O si quieres limitar el número de peticiones para que no te tumben el servicio? ¿O añadir cabeceras de seguridad sin tocar ni una línea de tu backend?

Para todo eso están los Middlewares, la capa intermedia entre el router y el servicio. Como te comenté en el capítulo anterior, Traefik es capaz de modificar o incluso enriquecer una determinada petición antes de que llegue al servicio destino. Pues bien, los middlewares son el mecanismo que hace esto posible.

En este capítulo vas a ver los middlewares que te van a salvar el pellejo en el día a día. Y te adelanto que, una vez que los uses, no vas a querer volver atrás. Son la guinda del pastel de Traefik.

¿Qué es un middleware?

Un middleware es un componente que se sitúa entre el router y el servicio, y que puede transformar la petición o la respuesta. Piensa en ello como una cadena de montaje: la petición va pasando por diferentes estaciones, y en cada una se le aplica una transformación.

Pero, ¿qué tipo de transformaciones puedes hacer? Pues de todo un poco: desde limitar el tráfico para que no te saturen el servidor, hasta modificar cabeceras HTTP, pasando por comprimir las respuestas, redirigir rutas antiguas a nuevas, o incluso cortar el acceso por circuito si el servicio va mal.

Lo interesante de los middlewares en Traefik es que se definen independientemente del router. Esto te permite crearlos una vez y reutilizarlos en todos los servicios que quieras. Y luego se asocian a uno o varios routers mediante la propiedad middlewares. Vamos, que evitas repetir código como un desesperado.

Cómo se definen los middlewares

En Docker, los middlewares se definen también mediante labels. La estructura general es,

labels:
  - "traefik.http.middlewares.<nombre>.<tipo>.<configuracion>=<valor>"

Y luego se asocian a un router,

  - "traefik.http.routers.mi-app.middlewares=mid1,mid2"

Los middlewares se separan por comas y se aplican en el orden en que los listas. Esto es importante porque el orden puede cambiar el resultado drásticamente. Por ejemplo, si pones el compresor antes de añadir cabeceras de seguridad, las cabeceras se añadirán al contenido comprimido. Pero si pones el rate limit después de un middleware que modifica la ruta, el límite se aplicará sobre la ruta modificada. Más adelante profundizo en esto.

Middlewares disponibles

Traefik v3 incluye un montón de middlewares. Vamos a ver los más útiles para el día a día. Y ojo, que no son todos: hay middlewares de autenticación que veremos en el próximo capítulo, y luego tienes los plugins para cosas más exóticas.

RateLimit (limitar peticiones)

Este middleware te permite limitar el número de peticiones que puede recibir un servicio. Esto te puede ahorrar más de un disgusto si tienes un servicio expuesto a internet y quieres evitar abusos, ataques de fuerza bruta o simplemente que un usuario malintencionado te deje el servicio en el suelo.

labels:
  - "traefik.http.middlewares.limit-10.ratelimit.average=10"
  - "traefik.http.middlewares.limit-10.ratelimit.burst=20"
  - "traefik.http.middlewares.limit-10.ratelimit.period=1m"

Los parámetros que puedes configurar son:

  • average: el número medio de peticiones permitidas en el periodo. Si pones 10, significa que de media solo se permiten 10 peticiones cada minuto (o el periodo que hayas configurado).
  • burst: el máximo de peticiones permitidas de golpe antes de empezar a limitar. Esto permite ráfagas cortas de tráfico. Por ejemplo, si un usuario abre varias pestañas a la vez, las primeras 20 pasan, pero luego se empieza a aplicar el límite.
  • period: el periodo de tiempo. Puede ser 1s (1 segundo), 1m (1 minuto), 1h (1 hora). Úsalo según el tipo de tráfico que esperes.

Pero hay más. Traefik v3 también te permite definir el SourceCriterion, que es cómo identifica a cada cliente para aplicar el límite. Por defecto usa la IP de origen, pero puedes cambiarlo,

labels:
  - "traefik.http.middlewares.limit-login.ratelimit.average=5"
  - "traefik.http.middlewares.limit-login.ratelimit.burst=10"
  - "traefik.http.middlewares.limit-login.ratelimit.period=1m"
  - "traefik.http.middlewares.limit-login.ratelimit.sourcecriterion.ipstrategy.depth=2"

El depth indica cuántos octetos de la IP usar para agrupar. Con depth=2 en una IPv4, agrupa por los dos primeros octetos (por ejemplo, 192.168.x.x), lo que es útil si detrás de una NAT todos los usuarios comparten la misma IP pública.

También puedes definir el SourceCriterion por cabecera o por cookie,

  - "traefik.http.middlewares.limit-api.ratelimit.sourcecriterion.requestheadername=X-API-Key"

Esto te permite limitar por clave de API en lugar de por IP. Muy útil si tienes usuarios autenticados que pueden venir desde distintas IPs.

Para usarlo en un router,

  - "traefik.http.routers.mi-app.middlewares=limit-10"

Advertencia de seguridad: el rate limiting no es un sustituto de un firewall o un WAF. Si alguien te lanza un ataque DDoS con suficientes IPs distintas, el rate limit por IP no te va a salvar. Para eso necesitas soluciones específicas. Pero para proteger endpoints concretos —como un formulario de login, una API pública o un endpoint de subida de archivos— el rate limit es tu mejor aliado.

Caso de uso real: imagina que tienes una API de terceros con límite de 100 peticiones por minuto. Pues pones un rate limit en Traefik antes de que llegue a tu backend para no superar ese límite,

labels:
  - "traefik.http.middlewares.limit-externo.ratelimit.average=80"
  - "traefik.http.middlewares.limit-externo.ratelimit.burst=100"
  - "traefik.http.middlewares.limit-externo.ratelimit.period=1m"

Poniendo 80 de media dejas un margen de seguridad. Esto te puede ahorrar más de un disgusto con proveedores externos que cortan el acceso si te pasas.

Headers (cabeceras)

Uno de los más utilizados, y con razón. Te permite añadir, modificar o eliminar cabeceras HTTP tanto de la petición como de la respuesta. Esto es clave para seguridad, CORS, y para comunicarte con servicios upstream que esperan cabeceras concretas.

Cabeceras de seguridad básicas:

labels:
  - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.X-Content-Type-Options=nosniff"
  - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.X-Frame-Options=DENY"
  - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.X-XSS-Protection=1; mode=block"
  - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.Referrer-Policy=strict-origin-when-cross-origin"

Pero no te quedes solo con esas. Si quieres una protección más completa, mete también Content-Security-Policy y Permissions-Policy,

labels:
  - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.Content-Security-Policy=default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
  - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.Permissions-Policy=camera=(), microphone=(), geolocation=()"
  - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.Strict-Transport-Security=max-age=31536000; includeSubDomains; preload"

Eso del HSTS (Strict-Transport-Security) es importante: le dice al navegador que siempre use HTTPS para tu dominio durante un año. Pero ojo, si no tienes bien configurado el HTTPS, no actives esto o te quedarás fuera.

Eliminar cabeceras sensibles:

A veces tu backend envía cabeceras que no quieres exponer al exterior. Por ejemplo, la cabecera Server que revela qué tecnología usas,

  - "traefik.http.middlewares.sec-headers.headers.customrequestheaders.Server="

Poniendo el valor vacío eliminas la cabecera. Esto es una práctica de seguridad básica: cuanta menos información des sobre tu stack, más difícil se lo pones a posibles atacantes.

Cabeceras CORS para APIs:

Si tienes una API a la que se conecta un frontend desde otro dominio, necesitas configurar CORS,

  - "traefik.http.middlewares.cors.headers.customresponseheaders.Access-Control-Allow-Origin=https://midominio.com"
  - "traefik.http.middlewares.cors.headers.customresponseheaders.Access-Control-Allow-Methods=GET, POST, PUT, DELETE, OPTIONS"
  - "traefik.http.middlewares.cors.headers.customresponseheaders.Access-Control-Allow-Headers=Content-Type, Authorization"
  - "traefik.http.middlewares.cors.headers.customresponseheaders.Access-Control-Max-Age=86400"

Y para que funcione bien con las peticiones preflight OPTIONS, necesitas un router específico,

  - "traefik.http.routers.mi-api.middlewares=cors"
  - "traefik.http.routers.mi-api.rule=Host(`api.dominio.com`) && Method(`OPTIONS`)"
  - "traefik.http.routers.mi-api.service=noop@internal"

Usas el servicio interno noop de Traefik para que las OPTIONS no lleguen a tu backend. Esto te ahorra tener que gestionar preflight en tu aplicación.

Cabeceras de proxy inverso:

Cuando Traefik actúa como proxy inverso, es buena práctica añadir cabeceras para que tu backend sepa quién hace la petición original,

  - "traefik.http.middlewares.proxy-headers.headers.customrequestheaders.X-Forwarded-For="
  - "traefik.http.middlewares.proxy-headers.headers.customrequestheaders.X-Forwarded-Proto="
  - "traefik.http.middlewares.proxy-headers.headers.customrequestheaders.X-Forwarded-Host="

Pero espera, que esto tiene truco. Si dejas las cabeceras vacías, las eliminas. Si quieres que Traefik las añada automáticamente mejor usar customrequestheaders con los valores que quieras,

  - "traefik.http.middlewares.proxy-headers.headers.customrequestheaders.X-Forwarded-Proto=https"

Aunque te recomiendo que confíes en el comportamiento por defecto de Traefik y solo toques estas cabeceras si sabes exactamente lo que haces.

Redirect (redirección)

No confundas esto con la redirección a nivel de entrypoint que viste en el capítulo de EntryPoints. El middleware Redirect te permite redirigir rutas concretas dentro de un mismo dominio o incluso a otro dominio diferente.

Traefik tiene dos tipos de redirect middleware:

  1. RedirectScheme: para redirigir de HTTP a HTTPS o viceversa.
  2. RedirectRegex: para redirigir usando expresiones regulares.

RedirectScheme:

labels:
  - "traefik.http.middlewares.redir-https.redirectscheme.scheme=https"
  - "traefik.http.middlewares.redir-https.redirectscheme.permanent=true"

Este es el más simple: todo lo que llegue a este middleware se redirige a HTTPS. El permanent=true significa que devuelve un 301 (Moved Permanently). Si pones false, devuelve un 302 (Found), que es una redirección temporal.

RedirectRegex con rutas:

labels:
  # Redirigir /old a /new
  - "traefik.http.middlewares.redir-old.redirectregex.regex=^(.*)/old(.*)"
  - "traefik.http.middlewares.redir-old.redirectregex.replacement=$$1/new$$2"
  - "traefik.http.middlewares.redir-old.redirectregex.permanent=true"

Fíjate en los $$. En los labels de Docker, el $ tiene un significado especial (es el marcador de variable de entorno), así que para poner un $ literal en la regex necesitas duplicarlo. Esto te va a dar más de un dolor de cabeza hasta que lo interiorices.

Redirigir a otro dominio:

  - "traefik.http.middlewares.redir-domain.redirectregex.regex=^https://antiguo.com/(.*)"
  - "traefik.http.middlewares.redir-domain.redirectregex.replacement=https://nuevo.com/$$1"
  - "traefik.http.middlewares.redir-domain.redirectregex.permanent=true"

Esto coge toda la URL de antiguo.com y la redirige a la misma ruta en nuevo.com. El $1 captura lo que hay después del dominio y lo reusa en el reemplazo.

Redirigir www al dominio principal:

  - "traefik.http.middlewares.redir-www.redirectregex.regex=^https://www\\.(.+)"
  - "traefik.http.middlewares.redir-www.redirectregex.replacement=https://$$1"
  - "traefik.http.middlewares.redir-www.redirectregex.permanent=true"

Advertencia: cuando hagas redirecciones 301 permanentes, asegúrate de que la configuración es definitiva. Los navegadores cachean las 301 de forma agresiva, y si luego cambias de opinión, los usuarios pueden seguir viendo la redirección antigua durante días o semanas. Para pruebas usa siempre 302.

Compress (compresión)

Comprime las respuestas antes de enviarlas al cliente usando gzip. Esto mejora el rendimiento y ahorra ancho de banda, especialmente en respuestas con mucho texto como HTML, JSON o CSS.

labels:
  - "traefik.http.middlewares.compresion.compress=true"

Pero no te quedes con la configuración por defecto. Puedes afinar qué tipos de contenido comprimir,

labels:
  - "traefik.http.middlewares.compresion.compress.excludedcontenttypes=text/event-stream"
  - "traefik.http.middlewares.compresion.compress.minresponsebodybytes=1024"
  • excludedcontenttypes: evita comprimir ciertos tipos de contenido. Por ejemplo, los text/event-stream (Server-Sent Events) no deberían comprimirse porque necesitan ser procesados en tiempo real.
  • minresponsebodybytes: solo comprime respuestas a partir de cierto tamaño. No tiene sentido comprimir respuestas de 200 bytes, el overhead no compensa.

En el router,

  - "traefik.http.routers.mi-app.middlewares=compresion"

Advertencia: la compresión consume CPU. En servidores con poco presupuesto de CPU, comprimir todo puede ser contraproducente. Mide antes y después para ver si realmente merece la pena. Para APIs JSON que devuelven respuestas pequeñas, a veces no compensa.

IPWhiteList (lista blanca de IPs)

Permite o deniega el acceso según la IP de origen. Esto es importantísimo para servicios de administración, paneles de control, o cualquier endpoint que solo deba ser accesible desde tu red interna.

labels:
  - "traefik.http.middlewares.solo-local.ipwhitelist.sourcerange=192.168.1.0/24, 10.0.0.0/8"

Puedes especificar tanto rangos CIDR como IPs concretas,

  - "traefik.http.middlewares.admin-only.ipwhitelist.sourcerange=192.168.1.100, 10.0.0.0/8, 172.16.0.0/12"

Pero hay un detalle importante: ¿cómo sabe Traefik cuál es la IP de origen real? Si tienes un proxy delante (Cloudflare, un CDN, o incluso otro proxy inverso), la IP que ve Traefik es la del proxy, no la del cliente real.

Para eso existe la configuración de ipStrategy,

  - "traefik.http.middlewares.admin-only.ipwhitelist.sourcerange=192.168.1.0/24"
  - "traefik.http.middlewares.admin-only.ipwhitelist.ipstrategy.depth=2"

El depth le dice a Traefik que mire en la cabecera X-Forwarded-For y coja la IP en la posición indicada. Si tienes un solo proxy delante (como Cloudflare), depth=1 funciona. Si tienes una cadena de proxies, necesitas ajustar el depth.

  # Si Cloudflare está delante, confía en la cabecera CF-Connecting-IP
  - "traefik.http.middlewares.admin-only.ipwhitelist.ipstrategy.depth=1"

O mejor aún, si usas Cloudflare,

  - "traefik.http.middlewares.admin-only.ipwhitelist.ipstrategy.depth=1"
  - "traefik.http.middlewares.admin-only.ipwhitelist.sourcerange=192.168.1.0/24, 10.0.0.0/8"

En el router,

  - "traefik.http.routers.admin.middlewares=solo-local"

Advertencia de seguridad: el IPWhiteList por sí solo no es suficiente si confías en X-Forwarded-For sin validar. Un atacante podría falsear esa cabecera si Traefik está mal configurado. Asegúrate de que Traefik solo confía en IPs de proxys conocidos. Y para servicios críticos, combínalo con autenticación. El IPWhiteList es una capa, no la solución completa.

Caso de uso real: proteger el panel de administración de WordPress

services:
  wordpress:
    image: wordpress:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.wp-admin.rule=Host(`blog.dominio.com`) && PathPrefix(`/wp-admin`)"
      - "traefik.http.routers.wp-admin.middlewares=solo-oficina"
      - "traefik.http.middlewares.solo-oficina.ipwhitelist.sourcerange=81.32.45.0/24"
      - "traefik.http.routers.wp-public.rule=Host(`blog.dominio.com`)"

Así, el /wp-admin solo es accesible desde la IP de tu oficina, mientras que el resto del blog es público. Esto solo ya te libra del 99% de los ataques automatizados a WordPress.

Retry (reintentos)

Reintenta la petición si el servicio responde con un error. Muy útil para servicios que pueden tener fallos intermitentes, como una base de datos que a veces tarda un poco más de la cuenta, o un microservicio que se reinicia.

labels:
  - "traefik.http.middlewares.reintentos.retry.attempts=3"

Puedes configurar cuántos intentos hacer,

  - "traefik.http.middlewares.reintentos.retry.attempts=5"
  - "traefik.http.middlewares.reintentos.retry.initialinterval=500ms"
  • attempts: número máximo de intentos (incluyendo el original). Con 3, hace la petición original y 2 reintentos.
  • initialinterval: tiempo de espera antes del primer reintento. Por defecto es 100ms, y se duplica en cada reintento (backoff exponencial).

Advertencia importante: no uses Retry con métodos HTTP no idempotentes como POST, PUT, PATCH o DELETE. Si el servidor recibe la petición, la procesa correctamente, pero la respuesta se pierde en la red, Traefik reintentará y el servidor procesará la misma petición dos veces. Esto puede causar duplicados en pagos, pedidos, etc. Si necesitas reintentar POSTs, asegúrate de que tu servicio es idempotente (por ejemplo, usando un idempotency key).

Retry es genial para:

  • APIs REST con GETs que a veces fallan por timeout.
  • Servicios que se reinician y tienen un breve periodo de indisponibilidad.
  • Conexiones a bases de datos a través de Traefik TCP.

No lo uses para:

  • Peticiones POST que crean recursos (pedidos, pagos, registros).
  • Servicios con tiempos de respuesta muy variables sin un timeout adecuado.

CircuitBreaker (cortacircuitos)

Si un servicio falla repetidamente, el circuit breaker lo deja de intentar y devuelve un error directamente. Esto evita que un servicio caído degrade el rendimiento de todo el sistema (efecto cascada).

labels:
  - "traefik.http.middlewares.cortacircuito.circuitbreaker.expression=NetworkErrorRatio() > 0.5"

La expresión puede combinar varios indicadores,

  # Se abre si más del 50% de errores de red O latencia superior a 500ms
  - "traefik.http.middlewares.cortacircuito.circuitbreaker.expression=NetworkErrorRatio() > 0.5 || LatencyAtQuantileMS(50.0) > 500"

Las funciones disponibles son:

  • NetworkErrorRatio(): ratio de errores de red (conexiones fallidas, timeouts, etc.). Devuelve un valor entre 0 y 1.
  • ResponseCodeRatio(<from>, <to>, <codeFrom>, <codeTo>): ratio de códigos de respuesta concretos. Por ejemplo, ResponseCodeRatio(0, 100, 500, 599) te da el ratio de errores 5xx sobre las últimas 100 peticiones.
  • LatencyAtQuantileMS(<quantile>): latencia en milisegundos en un percentil concreto. LatencyAtQuantileMS(50.0) es la mediana de latencia.
  • GoogleStackdriverWindow(): para integrar con métricas de Google Cloud.

Ejemplo más completo,

  # Se abre si >30% de errores 5xx en las últimas 100 peticiones
  - "traefik.http.middlewares.cortacircuito.circuitbreaker.expression=ResponseCodeRatio(0, 100, 500, 599) > 0.3"

El circuit breaker tiene tres estados:

  1. Cerrado (closed): funciona con normalidad, las peticiones pasan al servicio.
  2. Abierto (open): después de que la expresión se cumple, se abre el circuito y las peticiones fallan inmediatamente sin llegar al servicio. Esto da tiempo al servicio para recuperarse.
  3. Semi-abierto (half-open): después de un tiempo (por defecto 60 segundos), Traefik prueba con una petición para ver si el servicio se ha recuperado. Si funciona, vuelve a estado cerrado. Si falla, vuelve a abierto.

No puedes configurar el tiempo de espera en half-open con labels (tendrías que usar el file provider o la API), pero el comportamiento por defecto suele ser adecuado.

Advertencia: el circuit breaker funciona sobre un número limitado de peticiones (por defecto las últimas 100). Si tienes poco tráfico, puede tardar en activarse. Y si tienes mucho tráfico, puede abrirse por un pico puntual. Ajusta los umbrales según el tráfico de tu servicio.

ErrorPage (páginas de error)

Muy útil para mostrar páginas de error personalizadas cuando un servicio falla, en lugar de mostrar el feo mensaje por defecto de Traefik o, peor aún, el error del backend.

labels:
  - "traefik.http.middlewares.pagina-error.errors.status=500-599"
  - "traefik.http.middlewares.pagina-error.errors.service=error-service"
  - "traefik.http.middlewares.pagina-error.errors.query=/{status}.html"

Esto captura cualquier error entre 500 y 599, consulta el servicio error-service y le pasa el código de estado para que devuelva la página personalizada.

Necesitas un servicio que sirva esas páginas de error,

services:
  error-service:
    image: nginx:alpine
    volumes:
      - ./errores:/usr/share/nginx/html:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.error-router.rule=Host(`errors.internal`)"
      - "traefik.http.routers.error-router.service=error-service"
      - "traefik.http.services.error-service.loadbalancer.server.port=80"

Y en la carpeta errores tendrías archivos como 404.html, 500.html, 503.html.

Pero no hace falta que el servicio sea externo. Puedes usar el file provider para definir un servicio interno que sirva contenido estático desde el propio Traefik (usando el plugin static si lo tienes instalado), pero la opción más común es un contenedor nginx pequeño.

También puedes definir múltiples rangos de error,

labels:
  - "traefik.http.middlewares.pagina-error.errors.status=400-499,500-599"
  - "traefik.http.middlewares.pagina-error.errors.service=error-service"
  - "traefik.http.middlewares.pagina-error.errors.query=/{status}.html"

El {status} en la query se reemplaza por el código de estado real. Así puedes tener una página para cada tipo de error.

ReplacePath (reemplazar ruta)

Reemplaza la ruta de la petición antes de que llegue al servicio. Esto es útil cuando el servicio espera una ruta concreta pero tú quieres exponerla con otra.

labels:
  - "traefik.http.middlewares.reemplazar.replacepath.path=/api/v2"

En el router,

  - "traefik.http.routers.mi-app.rule=Host(`api.dominio.com`) && PathPrefix(`/api`)"
  - "traefik.http.routers.mi-app.middlewares=reemplazar"

Con esto, cualquier petición a /api se reenvía al servicio como /api/v2.

Pero hay una variante más potente: ReplacePathRegex,

labels:
  - "traefik.http.middlewares.reemplazar-regex.replacepathregex.regex=^/api/v1/(.*)"
  - "traefik.http.middlewares.reemplazar-regex.replacepathregex.replacement=/api/v2/$$1"

Esto te permite hacer transformaciones más complejas. Por ejemplo, si estás migrando de una API v1 a v2 y quieres que las peticiones antiguas sigan funcionando,

labels:
  - "traefik.http.middlewares.migracion-api.replacepathregex.regex=^/api/v1/(users|posts|comments)/(.*)"
  - "traefik.http.middlewares.migracion-api.replacepathregex.replacement=/api/v2/$$1/$$2"
  - "traefik.http.routers.api-v1.rule=Host(`api.dominio.com`) && PathPrefix(`/api/v1`)"
  - "traefik.http.routers.api-v1.middlewares=migracion-api"

Así, GET /api/v1/users/123 se transforma en GET /api/v2/users/123 antes de llegar al backend.

Diferencia con StripPrefix: no confundas ReplacePath con StripPrefix. StripPrefix elimina el prefijo de la ruta, mientras que ReplacePath sustituye la ruta completa. Si tu servicio espera /api y tú expones /miapp/api, usa StripPrefix. Si necesitas cambiar la ruta por completo (como en la migración de v1 a v2), usa ReplacePath.

Chain (cadena de middlewares)

Agrupa varios middlewares en una sola cadena para reutilizarlos fácilmente.

labels:
  - "traefik.http.middlewares.cadena-seguridad.chain.middlewares=limit-10,sec-headers,compresion"

Y luego solo tienes que referenciar la cadena,

  - "traefik.http.routers.mi-app.middlewares=cadena-seguridad"

Esto es equivalente a listar los tres middlewares separados por comas, pero con una ventaja: si luego quieres añadir o quitar un middleware de la cadena, solo tienes que cambiar la definición de la cadena, no todos los routers que la usan.

Puedes tener varias cadenas para diferentes propósitos,

labels:
  # Cadena para APIs públicas
  - "traefik.http.middlewares.api-publica.chain.middlewares=limit-100,cors,compresion"
  # Cadena para administración
  - "traefik.http.middlewares.admin.chain.middlewares=solo-local,limit-10,sec-headers"
  # Cadena completa para servicios críticos
  - "traefik.http.middlewares.full-security.chain.middlewares=admin,cortacircuito"

Fíjate en que las cadenas se pueden anidar: la cadena admin incluye middlewares concretos, y la cadena full-security incluye la cadena admin más otros. Esto te permite construir capas de funcionalidad de forma modular.

El orden importa (y mucho)

Ya lo he mencionado antes, pero merece repetirse: el orden de los middlewares es crítico. Aquí tienes una guía de orden recomendado según el caso de uso:

Para APIs públicas:

  1. RateLimit (limita antes de procesar)
  2. IPWhiteList (filtra antes de transformar)
  3. Headers (añade cabeceras de seguridad)
  4. Compress (comprime al final)

Para servicios internos:

  1. IPWhiteList (solo deja pasar a los autorizados)
  2. Headers (cabeceras necesarias)
  3. Retry (reintenta si falla)

Para migraciones:

  1. ReplacePath (transforma la ruta primero)
  2. Redirect (redirige si toca)
  3. Headers (cabeceras de aviso o deprecación)

La regla de oro: primero los middlewares que validan o limitan, luego los que transforman, y al final los que optimizan.

Caso de uso real: microservicios con diferentes middlewares

Vamos a montar un ejemplo completo con varios servicios, cada uno con sus propios middlewares. Un frontend público, una API interna y un panel de administración.

services:
  traefik:
    image: traefik:v3.7
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - traefik-net

  frontend:
    image: nginx:alpine
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.frontend.rule=Host(`midominio.com`)"
      - "traefik.http.routers.frontend.entrypoints=websecure"
      - "traefik.http.routers.frontend.tls=true"
      - "traefik.http.routers.frontend.middlewares=cadena-frontend"
      # Rate limit generoso para el frontend público
      - "traefik.http.middlewares.limit-front.ratelimit.average=100"
      - "traefik.http.middlewares.limit-front.ratelimit.burst=200"
      - "traefik.http.middlewares.limit-front.ratelimit.period=1m"
      # Cabeceras de seguridad frontend
      - "traefik.http.middlewares.sec-front.headers.customresponseheaders.X-Content-Type-Options=nosniff"
      - "traefik.http.middlewares.sec-front.headers.customresponseheaders.X-Frame-Options=DENY"
      - "traefik.http.middlewares.sec-front.headers.customresponseheaders.Content-Security-Policy=default-src 'self'"
      - "traefik.http.middlewares.sec-front.headers.customresponseheaders.Strict-Transport-Security=max-age=31536000; includeSubDomains"
      - "traefik.http.middlewares.cadena-front.chain.middlewares=limit-front,sec-front"
    networks:
      - traefik-net

  api:
    image: your-api-image:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api.midominio.com`)"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.tls=true"
      - "traefik.http.routers.api.middlewares=cadena-api"
      # Rate limit más restrictivo para la API
      - "traefik.http.middlewares.limit-api.ratelimit.average=60"
      - "traefik.http.middlewares.limit-api.ratelimit.burst=100"
      - "traefik.http.middlewares.limit-api.ratelimit.period=1m"
      # CORS para la API
      - "traefik.http.middlewares.cors-api.headers.customresponseheaders.Access-Control-Allow-Origin=https://midominio.com"
      - "traefik.http.middlewares.cors-api.headers.customresponseheaders.Access-Control-Allow-Methods=GET, POST, PUT, DELETE"
      - "traefik.http.middlewares.cors-api.headers.customresponseheaders.Access-Control-Allow-Headers=Authorization, Content-Type"
      # Compresión para respuestas JSON grandes
      - "traefik.http.middlewares.compres-api.compress=true"
      # Circuit breaker: si la API falla más del 30%, que no sature
      - "traefik.http.middlewares.cb-api.circuitbreaker.expression=NetworkErrorRatio() > 0.3"
      # Cadena que lo agrupa todo
      - "traefik.http.middlewares.cadena-api.chain.middlewares=limit-api,cors-api,compres-api,cb-api"
    networks:
      - traefik-net

  admin:
    image: your-admin-panel:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.admin.rule=Host(`admin.midominio.com`)"
      - "traefik.http.routers.admin.entrypoints=websecure"
      - "traefik.http.routers.admin.tls=true"
      - "traefik.http.routers.admin.middlewares=cadena-admin"
      # Solo accesible desde la oficina (IP pública de la empresa)
      - "traefik.http.middlewares.ip-admin.ipwhitelist.sourcerange=81.32.45.0/24"
      # Rate limit muy restrictivo para admin
      - "traefik.http.middlewares.limit-admin.ratelimit.average=20"
      - "traefik.http.middlewares.limit-admin.ratelimit.burst=30"
      - "traefik.http.middlewares.limit-admin.ratelimit.period=1m"
      # Cabeceras de seguridad máximas
      - "traefik.http.middlewares.sec-admin.headers.customresponseheaders.X-Content-Type-Options=nosniff"
      - "traefik.http.middlewares.sec-admin.headers.customresponseheaders.X-Frame-Options=DENY"
      - "traefik.http.middlewares.sec-admin.headers.customresponseheaders.X-XSS-Protection=1; mode=block"
      - "traefik.http.middlewares.sec-admin.headers.customresponseheaders.Referrer-Policy=no-referrer"
      - "traefik.http.middlewares.sec-admin.headers.customresponseheaders.Permissions-Policy=camera=(), microphone=(), geolocation=()"
      - "traefik.http.middlewares.sec-admin.headers.customrequestheaders.Server="
      # Cadena para admin
      - "traefik.http.middlewares.cadena-admin.chain.middlewares=ip-admin,limit-admin,sec-admin"
    networks:
      - traefik-net

networks:
  traefik-net:
    name: traefik-net

Fíjate en cómo cada servicio tiene su propia combinación de middlewares:

  • El frontend tiene rate limit básico y cabeceras de seguridad mínimas.
  • La API añade CORS, compresión y un circuit breaker por si empieza a fallar.
  • El admin es el más restrictivo: solo IPs de la oficina, rate limit bajo, y las máximas cabeceras de seguridad.

Middlewares desde archivo (File provider)

Hasta ahora hemos visto los middlewares definidos en labels de Docker. Pero también puedes definirlos en un archivo YAML usando el File provider. Esto te permite reutilizar middlewares entre varios servicios sin tener que repetirlos, y además tienes más opciones de configuración que con labels.

Crea un archivo middlewares.yml,

http:
  middlewares:
    rate-limit-10:
      rateLimit:
        average: 10
        burst: 20
        period: 1m

    sec-headers:
      headers:
        customResponseHeaders:
          X-Content-Type-Options: "nosniff"
          X-Frame-Options: "DENY"
          X-XSS-Protection: "1; mode=block"
          Strict-Transport-Security: "max-age=31536000; includeSubDomains"
        customRequestHeaders:
          Server: ""

    solo-red-local:
      ipWhiteList:
        sourceRange:
          - "192.168.1.0/24"
          - "10.0.0.0/8"

    compresion-global:
      compress:
        excludedContentTypes:
          - "text/event-stream"
        minResponseBodyBytes: 1024

    cortacircuito-api:
      circuitBreaker:
        expression: "NetworkErrorRatio() > 0.5 || LatencyAtQuantileMS(50.0) > 500"

    reintentos:
      retry:
        attempts: 3
        initialInterval: 200ms

    pagina-error:
      errors:
        status:
          - "500-599"
        service: error-service
        query: "/{status}.html"

    cadena-completa:
      chain:
        middlewares:
          - rate-limit-10
          - sec-headers
          - compresion-global
          - reintentos

Una ventaja del file provider es que puedes definir configuraciones más complejas que con labels. Por ejemplo, con el circuit breaker puedes usar expresiones más largas sin preocuparte por los límites de las labels.

Después tienes que indicarle a Traefik que use ese archivo como provider. En tu traefik.yml,

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: /etc/traefik/middlewares.yml
    watch: true

El watch: true hace que Traefik monitorice el archivo y recargue la configuración automáticamente cuando lo modifiques. Así no tienes que reiniciar el contenedor cada vez que cambies un middleware.

Y montar el archivo en el contenedor de Traefik,

volumes:
  - ./traefik.yml:/etc/traefik/traefik.yml:ro
  - ./middlewares.yml:/etc/traefik/middlewares.yml:ro

Luego, desde cualquier servicio Docker puedes referenciar estos middlewares con @file,

  - "traefik.http.routers.mi-app.middlewares=rate-limit-10@file,sec-headers@file"

O si los tienes en un chain definido en el file provider,

  - "traefik.http.routers.mi-app.middlewares=cadena-completa@file"

Esto te permite tener una configuración centralizada de middlewares y reutilizarlos en todos tus servicios. Cuando tengas varios servicios, esto te ahorrará muchísimo tiempo y evitará errores de copiar y pegar.

Cómo depurar middlewares

Cuando algo no funciona como esperas, lo primero es mirar el dashboard de Traefik. Ahí puedes ver qué middlewares tiene cada router y en qué orden se aplican.

Accede al dashboard (si lo tienes configurado, que vimos en el capítulo anterior) y en la sección de routers verás algo como,

miapp@docker
  Rule: Host(`app.dominio.com`)
  Middlewares: limit-10, sec-headers, compresion
  Service: miapp

Si no ves los middlewares que esperas, repasa las labels. Un error típico es poner mal el nombre del middleware en el router o tener un typo en la label.

Los logs de Traefik también dan mucha información. Activa el nivel de log a DEBUG temporalmente si necesitas ver exactamente qué está pasando,

# En traefik.yml
log:
  level: DEBUG

Pero no lo dejes en producción, que genera mucho volumen.

Y si tienes dudas sobre si un middleware se está aplicando correctamente, puedes usar un servicio whoami para ver las cabeceras que recibe,

services:
  whoami:
    image: traefik/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.dominio.com`)"
      - "traefik.http.routers.whoami.middlewares=sec-headers"
      - "traefik.http.middlewares.sec-headers.headers.customresponseheaders.X-Test=funciona"

Haces una petición a whoami.dominio.com y el servicio whoami te devuelve todas las cabeceras que ha recibido. Si ves X-Test: funciona, el middleware está funcionando.

Buenas prácticas con middlewares

  1. El orden importa: los middlewares se aplican en el orden en que los listas. Pon primero los que validan o limitan (RateLimit, IPWhiteList) y luego los que transforman (Headers, Compress). El circuit breaker debería ir al final, porque no tiene sentido abrir el circuito antes de que el resto de middlewares hayan hecho su trabajo.
  2. Usa Chain para agrupar: si siempre usas los mismos middlewares juntos, agrupaos en una cadena. Así no tienes que listarlos uno por uno en cada router. Y si luego necesitas cambiar la combinación, solo tocas la cadena.
  3. Centraliza los comunes con File provider: los middlewares que usas en varios servicios, defínelos una vez en un archivo y reutilízalos con @file. Esto reduce el riesgo de errores y hace la configuración más mantenible.
  4. No abuses del rate limiting: un límite muy bajo puede dar problemas a usuarios legítimos. Empieza con valores generosos y ajusta según veas en los logs y métricas. No hay nada peor que un rate limit mal configurado que bloquea a tus propios usuarios.
  5. Combina capas de seguridad: el IPWhiteList no es suficiente por sí solo. Combínalo con cabeceras de seguridad, rate limiting, y si es necesario, autenticación (que veremos en el próximo capítulo). La seguridad en profundidad es la clave.
  6. Prueba con whoami primero: antes de poner middlewares en producción, pruébalos con el servicio traefik/whoami. Te permite ver exactamente qué cabeceras llegan y cuáles no, sin tener que tocar tu aplicación real.
  7. Documenta tus middlewares: cuando tengas varios servicios y middlewares, la cosa se complica. Lleva un registro de qué middlewares tienes, para qué sirven y en qué servicios se usan. Te lo agradecerás cuando tengas que tocar algo seis meses después.
  8. Monitoriza los efectos: los middlewares como CircuitBreaker o Retry pueden tener efectos inesperados. Monitoriza las tasas de error, las latencias y el número de circuitos abiertos para detectar problemas antes de que afecten a los usuarios.

Conclusión

Los middlewares son lo que realmente diferencia a Traefik de otros proxies inversos. Con ellos puedes añadir capas de seguridad, control de tráfico y transformaciones sin tocar ni una línea de tus servicios. Y lo mejor es que los defines una vez y los reutilizas donde quieras.

En este capítulo has visto los middlewares básicos: RateLimit para proteger tus servicios de abusos, Headers para controlar las cabeceras HTTP, Redirect para gestionar redirecciones, Compress para ahorrar ancho de banda, IPWhiteList para restringir accesos, Retry y CircuitBreaker para hacer tus servicios más resilientes, ErrorPage para dar una buena experiencia cuando algo falla, ReplacePath para transformar rutas, y Chain para organizarlo todo.

Pero esto no acaba aquí. En el próximo capítulo veremos los Middlewares de autenticación, que merecen un capítulo aparte por su importancia: BasicAuth, DigestAuth, ForwardAuth y cómo integrar con OAuth. Ahí es donde realmente vas a poder proteger tus servicios como se merecen.

Más información

Deja una respuesta