6

Middlewares de seguridad para Self Hosted

Vistas: 1
Middlewares de seguridad para Self Hosted

En el capítulo anterior montaste Traefik como puerta de entrada única. Entrypoints seguros, TLS con Let’s Encrypt, dashboard protegido, logs listos para fail2ban. Tu servidor tiene una puerta robusta. Pero una puerta no es suficiente si dejas pasar a todo el mundo sin preguntar. Imagina que tienes Grafana corriendo detrás de Traefik. Está en grafana.tudominio.com, con HTTPS, todo correcto. Pero no hay ningún control sobre quién accede ni con qué frecuencia. Un bot cualquiera puede empezar a probar credenciales a 100 peticiones por segundo. Tu Grafana va a sudar, y si la contraseña es débil, acabarán entrando.

O peor: tienes una API pública. Sin rate limiting, un cliente mal programado (o con mala intención) puede saturar tu backend en segundos. Adiós al servicio, adiós a la RAM del servidor. Aquí entran los middlewares de seguridad de Traefik. Pequeños módulos que se colocan entre la petición entrante y tu servicio. Deciden si la petición pasa, la modifican, la limitan o la bloquean. Sin que tu servicio tenga que hacer nada.

Este capítulo es práctico. Vas a configurar rate limiting para proteger tus servicios de abusos, whitelist de IPs para restringir accesos sensibles, cadenas de middlewares con el orden correcto, y circuit breaker para cuando un backend falla. También voy a enseñarte a probarlo todo con herramientas reales para que no te quedes con la duda de si funciona.

Al final de este capítulo, tus servicios no solo estarán detrás de una puerta segura. Tendrán su propio portero, con reglas claras y mano dura.

¿Qué es un middleware en Traefik?

Un middleware es un pequeño procesador que se ejecuta antes de que la petición llegue a tu servicio. Piensa en ello como un filtro en una tubería: el agua pasa, pero antes se queda con las impurezas.

Traefik tiene decenas de middlewares integrados. Los que nos interesan para seguridad son:

  • rateLimit: limita el número de peticiones por IP en un periodo de tiempo.
  • ipWhiteList: permite solo peticiones desde IPs o rangos concretos.
  • basicAuth: exige usuario y contraseña antes de pasar.
  • headers: añade, modifica o elimina cabeceras HTTP (HSTS, CSP, etc.).
  • compress: comprime la respuesta con gzip antes de enviarla al cliente.
  • circuitBreaker: detecta fallos en el backend y deja de enviarle tráfico temporalmente.
  • chain: agrupa varios middlewares en una sola referencia.

Lo interesante es que los middlewares se encadenan. Un router puede tener asociados varios middlewares, y se ejecutan en el orden en que los declares. Si uno falla (IP no permitida, rate limit excedido, autenticación incorrecta), la petición se descarta y no llega al siguiente.

# Orden de ejecución: whitelist -> ratelimit -> auth -> backend
middlewares:
  - whitelist-interno
  - ratelimit-estricto
  - auth-grafana

Si la IP no está en la whitelist, whitelist-interno devuelve un 403 y la cadena se rompe. Ni siquiera se evalúa el rate limit ni la autenticación. Esto es importante para el orden: las comprobaciones más restrictivas y baratas (whitelist, rate limit) deben ir primero.

Dónde se definen los middlewares

Tienes dos formas de declarar middlewares en Traefik:

1. Docker labels: directamente en las etiquetas del contenedor en tu docker-compose.yml. Es la forma más común si usas Traefik con Docker y quieres que cada servicio lleve su propia configuración.

services:
  quienquieras:
    image: alpine
    labels:
      - "traefik.http.middlewares.mi-ratelimit.ratelimit.average=100"
      - "traefik.http.middlewares.mi-ratelimit.ratelimit.burst=200"
      - "traefik.http.routers.mi-servicio.middlewares=mi-ratelimit"

2. Archivo dinámico (YAML/TOML): defines todos los middlewares en un archivo centralizado, normalmente traefik-dynamic.yml. Es más ordenado cuando tienes muchos servicios y middlewares compartidos.

# traefik-dynamic.yml
http:
  middlewares:
    mi-ratelimit:
      rateLimit:
        average: 100
        burst: 200

Mi recomendación: usa el archivo dinámico para middlewares globales o compartidos (rate limit general, headers de seguridad, compress) y Docker labels para middlewares específicos de un servicio (whitelist de IP para un servicio interno, autenticación básica para el dashboard).

En este capítulo voy a mostrar ambas opciones. Tú eliges la que mejor se adapte a tu flujo.

Rate limiting: frena los abusos

El rate limiting limita el número de peticiones que una IP puede hacer en un intervalo de tiempo. Es la primera línea de defensa contra fuerza bruta, web scraping, y bots mal comportados.

Traefik implementa rate limiting con el algoritmo de token bucket. Cada IP tiene un depósito de tokens. Por cada petición, consume un token. Cuando se queda sin tokens, las peticiones se rechazan con 429 Too Many Requests. Los tokens se regeneran a una velocidad constante.

Dos parámetros clave:

  • average: velocidad media de peticiones por segundo. Cuántos tokens se regeneran por segundo.
  • burst: tamaño máximo del depósito. Cuántas peticiones puede hacer una IP de golpe antes de que el límite haga efecto.

Pongamos un ejemplo. Configuras average: 100 y burst: 200. Durante los primeros 2 segundos, una IP puede hacer 200 peticiones porque el depósito está lleno. A partir de ahí, solo puede hacer 100 peticiones por segundo. Si para un segundo, el depósito se vuelve a llenar hasta 200.

Rate limiting global para todos los servicios

Este es el middleware que deberías aplicar por defecto a todas las rutas que pasan por Traefik. Lo defines en tu archivo dinámico y lo enganchas al entrypoint websecure.

# traefik-dynamic.yml
http:
  middlewares:
    ratelimit-global:
      rateLimit:
        average: 100
        burst: 200
        sourceCriterion:
          ipStrategy:
            depth: 1

Luego, en la configuración del entrypoint:

entryPoints:
  websecure:
    address: ":443"
    http:
      middlewares:
        - ratelimit-global

Y ya está. Todas las peticiones HTTPS pasan por este rate limit. Cada IP puede hacer hasta 200 peticiones de golpe, y luego 100 por segundo. Si alguien supera eso, recibe un 429.

Un detalle importante: sourceCriterion define cómo identifica Traefik al cliente. Por defecto usa la IP de conexión directa. Si tienes otro proxy delante (Cloudflare, por ejemplo), necesitas configurar ipStrategy.depth para que lea la IP de la cabecera X-Forwarded-For.

sourceCriterion:
  ipStrategy:
    depth: 2
# Profundidad 2 significa: coge la penúltima IP de X-Forwarded-For

Si usas Cloudflare, además tienes que confiar en sus IPs. Pero eso lo veremos en un capítulo aparte. De momento, con depth: 0 o sin especificar, Traefik usa la IP real de conexión. Perfecto si solo tienes Traefik expuesto directamente a internet.

Rate limiting específico por servicio

El rate limiting global protege tu servidor en conjunto, pero puede ser demasiado permisivo para servicios sensibles. Un servicio de login (Authelia, Authentik, o el propio Grafana) no debería permitir 200 peticiones por segundo.

Para estos casos, crea un middleware más restrictivo y aplícalo solo a ese servicio.

# Docker labels para Grafana
services:
  grafana:
    image: grafana/grafana:latest
    labels:
      - "traefik.http.routers.grafana.rule=Host(`grafana.tudominio.com`)"
      - "traefik.http.routers.grafana.middlewares=ratelimit-login,chain-seguridad"
      - "traefik.http.middlewares.ratelimit-login.ratelimit.average=5"
      - "traefik.http.middlewares.ratelimit-login.ratelimit.burst=10"

La cadena chain-seguridad la definiremos más adelante en este mismo capítulo, en la sección de archivo dinámico.

Aquí el rate limit es mucho más agresivo: 5 peticiones por segundo de media, con un burst de 10. Una persona real no necesita más que eso para hacer login. Un bot haciendo fuerza bruta va a encontrarse con el 429 a los 2 segundos.

Pero ojo: no pongas un rate limit demasiado bajo en servicios que cargan muchos recursos (imágenes, CSS, JS). Una página de Grafana con decenas de paneles puede generar fácilmente 30-40 peticiones al cargar. Si pones average: 5, el usuario legítimo va a ver 429 en lugar de gráficos.

La solución: aplica el rate limit restrictivo solo a la ruta de login, no a todo el servicio. Puedes hacerlo con un router específico.

labels:
  # Router para el login (rate limit estricto)
  - "traefik.http.routers.grafana-login.rule=Host(`grafana.tudominio.com`) && PathPrefix(`/login`)"
  - "traefik.http.routers.grafana-login.middlewares=ratelimit-login"
  - "traefik.enable=true"
  - "traefik.http.middlewares.ratelimit-login.ratelimit.average=5"
  - "traefik.http.middlewares.ratelimit-login.ratelimit.burst=10"

  # Router general (menos restrictivo)
  - "traefik.http.routers.grafana.rule=Host(`grafana.tudominio.com`)"
  - "traefik.http.routers.grafana.middlewares=ratelimit-global"
  - "traefik.enable=true"

Fíjate en lo que hemos hecho: dos routers para el mismo servicio. El primero solo captura peticiones a /login y les aplica un rate limit duro. El segundo captura todo lo demás con el rate limit global suave. Traefik evalúa los routers por orden de definición, así que el más específico va primero.

Proteger una API pública con rate limit

Las APIs son especialmente sensibles al abuso. Una API sin rate limit es como un bar con la barra libre: alguien se va a aprovechar.

Para una API pública, el rate limit debe ser más restrictivo que para páginas web. Las APIs suelen recibir peticiones automatizadas, no humanos navegando.

# En docker-compose.yml o en traefik-dynamic.yml
http:
  middlewares:
    ratelimit-api:
      rateLimit:
        average: 30
        burst: 60
        sourceCriterion:
          requestHeaders:
            headerName: X-Api-Key
            depth: 0

Este ejemplo usa sourceCriterion con requestHeaders. En lugar de limitar por IP, limita por cabecera X-Api-Key. Así cada cliente de tu API tiene su propio depósito de tokens, independientemente de la IP desde la que se conecte.

Útil si varios clientes comparten la misma IP (por ejemplo, están detrás de un NAT corporativo). No quieres que un cliente acapare todo el presupuesto de tokens de los demás.

# Aplicado a una API
services:
  api:
    image: my-api:latest
    labels:
      - "traefik.http.routers.api.rule=Host(`api.tudominio.com`)"
      - "traefik.http.routers.api.middlewares=ratelimit-api,chain-seguridad"

IP whitelist: solo para invitados seleccionados

El middleware ipWhiteList permite bloquear todo el tráfico excepto el que viene de IPs o rangos que tú autorices. Es el equivalente a tener una lista de invitados en la puerta de una discoteca. Si tu nombre no está en la lista, no entras.

Este middleware es perfecto para:

  • Dashboard de Traefik: solo accesible desde tu IP de casa o desde la VPN.
  • Servicios internos: herramientas de administración, bases de datos administradas desde interfaz web, paneles de monitorización.
  • Entornos de staging: que solo tú y tu equipo puedan ver.
  • Endpoints de administración: rutas /admin que no deberían ser públicas.

Configuración básica de whitelist

# En docker labels
labels:
  - "traefik.http.middlewares.whitelist-casa.ipwhitelist.sourcerange=192.168.1.0/24,84.123.45.67/32"
  - "traefik.http.routers.admin.middlewares=whitelist-casa"
# En traefik-dynamic.yml
http:
  middlewares:
    whitelist-casa:
      ipWhiteList:
        sourceRange:
          - "192.168.1.0/24"
          - "84.123.45.67/32"

El parámetro sourceRange acepta direcciones IPv4 e IPv6 en notación CIDR. Una IP concreta la pones como /32. Un rango de red como 10.0.0.0/8.

Si la IP del cliente no está en ningún rango, Traefik devuelve un 403 Forbidden. Sin más. Sin pantalla de error, sin formulario de login. Solo un 403 seco.

Whitelist para el dashboard de Traefik

En el capítulo anterior configuramos el dashboard con autenticación básica. Vamos a mejorarlo añadiendo whitelist de IP. Así solo las IPs autorizadas llegan a ver la pantalla de login.

labels:
  - "traefik.http.routers.dashboard.rule=Host(`traefik.tudominio.com`)"
  - "traefik.http.routers.dashboard.service=api@internal"
  - "traefik.http.routers.dashboard.middlewares=whitelist-dashboard,auth-dashboard"
  - "traefik.http.middlewares.whitelist-dashboard.ipwhitelist.sourcerange=10.0.0.0/8,192.168.1.0/24"
  - "traefik.http.middlewares.auth-dashboard.basicauth.users=lorenzo:$$apr1$$xxxx..."

Fíjate en el orden de los middlewares: whitelist-dashboard va primero. Si la IP no está autorizada, la petición se rechaza antes de llegar a la pantalla de autenticación. Esto tiene dos ventajas:

  1. Ahorras recursos: no procesas autenticación innecesaria.
  2. No filtras información: un atacante ni siquiera sabe que existe un formulario de login. Solo ve un 403.

Si pusieras auth-dashboard primero, cualquier IP del mundo llegaría a ver la pantalla de login. Y aunque la contraseña sea segura, ya has revelado que existe un punto de autenticación. Los bots pueden intentar fuerza bruta contra ella. Con la whitelist primero, ni siquiera llegan a saber que hay algo detrás.

Whitelist para servicios internos (Grafana)

Imagina que tienes Grafana como panel de monitorización. No quieres que cualquiera pueda ver tus métricas. Solo tú y tu equipo.

services:
  grafana:
    image: grafana/grafana:latest
    labels:
      - "traefik.http.routers.grafana.rule=Host(`grafana.tudominio.com`)"
      - "traefik.http.routers.grafana.middlewares=whitelist-oficina,ratelimit-global,chain-seguridad"
      - "traefik.http.middlewares.whitelist-oficina.ipwhitelist.sourcerange=81.23.45.0/24,10.0.0.0/8"

Aquí solo las IPs de la oficina (81.23.45.0/24) y de la VPN WireGuard (10.0.0.0/8) pueden acceder a Grafana. El resto del mundo recibe un 403.

Si además tienes un rate limit global y cadenas de seguridad (headers, compresión), se aplican después de la whitelist. El orden es correcto: primero compruebo quién eres (whitelist), luego limito tu velocidad (rate limit), luego proceso cabeceras de seguridad (headers), y finalmente llego al servicio.

El orden correcto de la cadena de middlewares

El orden de los middlewares no es trivial. Cada uno tiene un coste de procesamiento y un propósito distinto. Una cadena mal ordenada puede:

  • Dejar pasar tráfico no deseado a capas más costosas.
  • Filtrar información sensible a clientes no autorizados.
  • Consumir recursos innecesariamente.

Esta es la secuencia que recomiendo para servicios protegidos con autenticación:

1. ipWhiteList     -> bloqueo por IP (el más barato y restrictivo)
2. rateLimit       -> limitar velocidad (evita abusos en capas superiores)
3. basicAuth       -> autenticación (solo si la IP pasó los filtros anteriores)
4. headers         -> cabeceras de seguridad (HSTS, CSP, etc.)
5. compress        -> compresión gzip (opcional, mejora rendimiento)
6. backend         -> tu servicio

Nota: el circuit breaker no es un middleware de control de acceso, sino de resiliencia del backend. Por eso va primero en la cadena cuando se usa: si el backend está caído, no tiene sentido procesar whitelist ni rate limit.

¿Por qué este orden?

  • Whitelist primero: es la comprobación más rápida. Un simple match de IP contra una lista. Si no pasas, te vas con un 403 sin procesar nada más.
  • Rate limit segundo: antes de gastar recursos en autenticación, comprueba que el cliente no está abusando.
  • Auth tercero: solo los clientes que han pasado los dos filtros anteriores llegan a pedir credenciales.
  • Headers cuarto: las cabeceras de seguridad se aplican a respuestas de clientes autenticados. No tiene sentido ponerlas antes, porque un 403 no necesita cabeceras de seguridad de contenido.
  • Compress quinto: la compresión se aplica al contenido que ya va a viajar al cliente. Es el último paso antes del backend.
# Ejemplo completo: cadena para servicio interno protegido
http:
  middlewares:
    whitelist-oficina:
      ipWhiteList:
        sourceRange:
          - "10.0.0.0/8"
          - "192.168.1.0/24"

    ratelimit-estricto:
      rateLimit:
        average: 10
        burst: 20

    auth-basica:
      basicAuth:
        users:
          - "lorenzo:$$apr1$$xxxx..."
          - "admin:$$apr1$$yyyy..."

    headers-seguridad:
      headers:
        strictTransportSecurity:
          maxAge: 31536000
          includeSubDomains: true
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: "strict-origin-when-cross-origin"

    compress-gzip:
      compress:
        excludedContentTypes:
          - "text/event-stream"

    chain-servicio-interno:
      chain:
        middlewares:
          - whitelist-oficina
          - ratelimit-estricto
          - auth-basica
          - headers-seguridad
          - compress-gzip

Fíjate en el uso de chain al final. El middleware chain-servicio-interno agrupa los cinco middlewares en uno solo. Así, en el router solo pones un middleware en lugar de cinco.

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

Mucho más limpio, ¿verdad?

Compresión (gzip): opcional pero recomendada

El middleware compress de Traefik comprime las respuestas con gzip antes de enviarlas al cliente. No es estrictamente seguridad, pero tiene un impacto positivo en el rendimiento y, por tanto, en la seguridad por reducción de superficie.

Una respuesta comprimida viaja más rápido, ocupa menos ancho de banda, y libera antes la conexión. Menos tiempo de conexión significa menos ventana para ataques basados en tiempo.

http:
  middlewares:
    compress-gzip:
      compress:
        excludedContentTypes:
          - "text/event-stream"

Los text/event-stream (Server-Sent Events) no se deben comprimir porque son streams en tiempo real. La compresión los rompería.

Si quieres excluir más tipos:

compress:
  excludedContentTypes:
    - "text/event-stream"
    - "image/webp"
    - "image/avif"

Las imágenes ya vienen comprimidas de origen. Comprimirlas otra vez con gzip es gastar CPU para nada.

¿Dónde poner el compress en la cadena? Al final, justo antes del backend. No tiene sentido comprimir un 403 de whitelist o una respuesta de autenticación fallida. Son respuestas pequeñas y efímeras. La compresión solo aporta valor en respuestas exitosas con contenido.

Circuit breaker: protege tus backends

El middleware circuitBreaker es el menos conocido de los que vamos a ver, pero puede salvarte de un desastre.

Funciona así: monitoriza las respuestas de tu backend. Si detecta demasiados errores (500, timeout, conexión rechazada), abre el circuito y deja de enviar tráfico al backend durante un tiempo. En lugar de eso, devuelve un error 503 directamente, sin molestar al backend.

¿Para qué sirve? Imagina que uno de tus servicios se cae o empieza a responder con errores. Sin circuit breaker, Traefik sigue enviando peticiones al servicio caído. Cada petición espera un timeout, se acumulan, y acaban saturando los recursos de Traefik (conexiones abiertas, memoria). El resultado: un servicio caído puede tirar indirectamente a Traefik y a todos los demás servicios.

Con circuit breaker, Traefik detecta el patrón de errores, abre el circuito y deja de enviar tráfico al backend. El backend puede recuperarse sin recibir más presión. Cuando pasa el tiempo de recuperación, Traefik cierra el circuito gradualmente (modo half-open) y comprueba si el backend ya responde correctamente.

http:
  middlewares:
    circuitbreaker-api:
      circuitBreaker:
        expression: "NetworkErrorRatio() > 0.5 || LatencyAtQuantileMS(50.0) > 5000"
        checkPeriod: 10s
        fallbackDuration: 30s
        recoveryDuration: 10s

Vamos a desglosar la expresión:

  • NetworkErrorRatio() > 0.5: si más del 50% de las peticiones a este backend fallan (timeout, conexión rechazada, 5xx), se abre el circuito.
  • LatencyAtQuantileMS(50.0) > 5000: si la latencia mediana supera los 5000ms, también se abre. Esto cubre casos donde el backend no falla pero va muy lento.

Los otros parámetros:

  • checkPeriod: cada cuánto se evalúa la expresión. 10 segundos es un buen valor.
  • fallbackDuration: cuánto tiempo se mantiene el circuito abierto antes de intentar recuperarse. 30 segundos.
  • recoveryDuration: tiempo en estado half-open antes de cerrar el circuito completamente. 10 segundos.

Aplica este middleware a servicios críticos donde un fallo del backend no debería afectar al resto del sistema.

labels:
  - "traefik.http.routers.api-pública.middlewares=circuitbreaker-api,ratelimit-api,chain-seguridad"

El circuit breaker va primero en la cadena, antes incluso que la whitelist. ¿Por qué? Porque si el circuito está abierto, no tiene sentido procesar nada más. La petición se rechaza directamente con un 503.

Archivo dinámico vs Docker labels

Ya hemos visto ejemplos de ambas formas. Vamos a aclarar cuándo usar cada una.

Docker labels: ventajas

  • Configuración junto al servicio: cada servicio lleva sus middlewares definidos en sus propias etiquetas. Si mueves el servicio a otro servidor, la configuración se mueve con él.
  • Fácil de entender: abres el docker-compose.yml de un servicio y ves toda su configuración de red en un solo sitio.
  • Ideal para middlewares específicos: whitelist de IP para un servicio concreto, rate limit específico para otro.

Archivo dinámico: ventajas

  • Centralizado: todos los middlewares están en un solo archivo. Más fácil de auditar y mantener.
  • Reutilizable: defines un middleware una vez y lo referencias desde cualquier router, tanto en Docker labels como en el propio archivo dinámico.
  • Ideal para middlewares globales: rate limit global, headers de seguridad, compress, chain de seguridad base.

Mi recomendación práctica

Usa una combinación de ambos:

  1. En traefik-dynamic.yml defines los middlewares base: ratelimit-global, headers-seguridad, compress-gzip, chain-seguridad.
  2. En los Docker labels de cada servicio añades middlewares específicos y referencias a los del archivo dinámico.
# traefik-dynamic.yml
http:
  middlewares:
    ratelimit-global:
      rateLimit:
        average: 100
        burst: 200

    headers-seguridad:
      headers:
        strictTransportSecurity:
          maxAge: 31536000
          includeSubDomains: true
        contentTypeNosniff: true
        browserXssFilter: true

    compress-gzip:
      compress:
        excludedContentTypes:
          - "text/event-stream"

    chain-seguridad:
      chain:
        middlewares:
          - headers-seguridad
          - compress-gzip
          - ratelimit-global
# docker-compose.yml para un servicio concreto
services:
  mi-servicio:
    image: alpine
    labels:
      - "traefik.http.routers.mi-servicio.rule=Host(`app.tudominio.com`)"
      - "traefik.http.routers.mi-servicio.middlewares=whitelist-oficina,chain-seguridad"
      - "traefik.http.middlewares.whitelist-oficina.ipwhitelist.sourcerange=10.0.0.0/8"

¿Ves? En las labels solo defines la whitelist (específica de este servicio) y referencias chain-seguridad que está en el archivo dinámico. Lo mejor de ambos mundos.

Recarga sin reinicio

Tanto los Docker labels como el archivo dinámico se recargan en caliente. No necesitas reiniciar Traefik cuando añades, modificas o eliminas middlewares. Traefik detecta los cambios automáticamente.

Si usas archivo dinámico, asegúrate de que Traefik está configurado para vigilarlo:

# En la configuración estática (traefik.yml)
providers:
  file:
    filename: /etc/traefik/traefik-dynamic.yml
    watch: true

Con watch: true, Traefik monitoriza el archivo y lo recarga al instante cuando cambia. Sin reinicios, sin interrupciones.

Ejemplos prácticos

Vamos a ver dos casos completos. De principio a fin.

Caso 1: Proteger Grafana con whitelist + rate limit

Grafana es tu panel de monitorización. No quieres que el mundo entero vea tus métricas. Solo tu equipo, y con limitaciones.

# docker-compose.yml
services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    networks:
      - traefik
    volumes:
      - grafana-data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=contraseñamuysegura
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.grafana.rule=Host(`grafana.tudominio.com`)"
      - "traefik.http.routers.grafana.entrypoints=websecure"
      - "traefik.http.routers.grafana.tls=true"
      - "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
      - "traefik.http.routers.grafana.middlewares=chain-grafana"
      - "traefik.http.middlewares.whitelist-grafana.ipwhitelist.sourcerange=10.0.0.0/8,192.168.1.0/24"
      - "traefik.http.middlewares.ratelimit-grafana.ratelimit.average=20"
      - "traefik.http.middlewares.ratelimit-grafana.ratelimit.burst=40"
      - "traefik.http.middlewares.chain-grafana.chain.middlewares=whitelist-grafana,ratelimit-grafana"

¿Qué tenemos aquí?

  1. Whitelist: solo IPs de la VPN (10.0.0.0/8) y de la red local (192.168.1.0/24) pueden acceder.
  2. Rate limit: 20 peticiones por segundo, burst de 40. Suficiente para un usuario real, insuficiente para un bot.
  3. Chain: agrupa whitelist y rate limit en un solo middleware para el router.

El orden dentro de la chain es el correcto: whitelist primero, rate limit después. Si la IP no está autorizada, recibe 403 antes de gastar tokens de rate limit.

Caso 2: Proteger una API pública con rate limit

Tu API es pública, cualquiera puede llamarla. Pero necesitas controlar cuánto puede llamar cada cliente.

# traefik-dynamic.yml
http:
  middlewares:
    ratelimit-api-publica:
      rateLimit:
        average: 60
        burst: 120
        sourceCriterion:
          requestHeaders:
            headerName: X-Api-Key
            depth: 0

    circuitbreaker-api:
      circuitBreaker:
        expression: "NetworkErrorRatio() > 0.5"
        checkPeriod: 10s
        fallbackDuration: 30s
        recoveryDuration: 10s

    headers-api:
      headers:
        accessControlAllowOriginList:
          - "*"
        accessControlAllowMethods:
          - "GET"
          - "POST"
          - "OPTIONS"
        accessControlMaxAge: 86400

    chain-api-publica:
      chain:
        middlewares:
          - circuitbreaker-api
          - ratelimit-api-publica
          - headers-api
# docker-compose.yml
services:
  api:
    image: my-api:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`api.tudominio.com`)"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.tls=true"
      - "traefik.http.routers.api.tls.certresolver=letsencrypt"
      - "traefik.http.routers.api.middlewares=chain-api-publica"

Puntos clave de esta configuración:

  • Circuit breaker primero: si la API backend empieza a fallar, el circuito se abre y Traefik responde 503 sin saturar el backend.
  • Rate limit por API key: cada cliente tiene su propio límite de 60 peticiones/segundo. Da igual que compartan IP.
  • Headers CORS: la API responde con cabeceras CORS para que aplicaciones web puedan llamarla desde cualquier origen.

Doble capa: rate limit + fail2ban

En el capítulo 3 configuramos fail2ban para proteger servicios web. Vimos el filtro traefik-auth que detecta errores 401, y el filtro traefik-botsearch que detecta 404 repetidos. Y creamos un filtro personalizado para 429.

Ahora vamos a juntarlo con el rate limiting de Traefik. La combinación es letal:

  1. Primera capa (Traefik): rate limit suave. Limita a 100 peticiones por segundo. Cualquiera que supere esto recibe un 429 y punto. No hay baneo, solo ralentización.
  2. Segunda capa (fail2ban): si alguien insiste después del 429, fail2ban detecta esos 429 repetidos y banea la IP a nivel de firewall.

La pregunta es: para qué necesitas fail2ban si ya tienes rate limit en Traefik? Porque son complementarios, no sustitutos.

El rate limit de Traefik es aplicación: actúa a nivel de peticiones HTTP. Cuando alguien supera el límite, recibe un 429 pero la conexión TCP sigue existiendo. El cliente puede seguir abriendo conexiones nuevas y haciendo peticiones justo por debajo del límite.

fail2ban es red: actúa a nivel de firewall. Cuando alguien es baneado, todas las conexiones desde su IP se bloquean. No llegan ni a Traefik. Ni siquiera ve un 429, porque la conexión TCP no se establece.

La doble capa funciona así:

  1. Un cliente empieza a hacer peticiones agresivas.
  2. Traefik le devuelve 429 cuando supera el rate limit.
  3. El cliente ignora el 429 y sigue insistiendo (o baja el ritmo pero sigue).
  4. fail2ban detecta los 429 en los logs de acceso de Traefik.
  5. Tras 3 o 4 veces con 429, fail2ban banea la IP en nftables.
  6. A partir de ahí, todas las conexiones desde esa IP se bloquean antes de llegar a Traefik.

El filtro de fail2ban para 429 lo creamos en el capítulo 3:

# /etc/fail2ban/filter.d/traefik-ratelimit.conf
[Definition]
failregex = ^<HOST> - - \[.*\] ".*" 429 .*$
ignoreregex =

Y la jail:

# /etc/fail2ban/jail.d/traefik-ratelimit.conf

[traefik-ratelimit]

enabled = true port = http,https filter = traefik-ratelimit logpath = /var/log/traefik/access.log maxretry = 3 bantime = 1800

¿Ves cómo encaja? El rate limit de Traefik frena el primer envite. Si el atacante es persistente, fail2ban le cierra la puerta por completo. No necesita ni un solo 429 más.

Verificación

Has configurado middlewares, cadenas, whitelists. Ahora toca comprobar que todo funciona. No te fíes de la configuración. Pruébala.

Probar rate limit con curl

El método más simple para comprobar que el rate limit funciona es hacer peticiones rápidas con curl.

# Bucle rápido para superar el rate limit
for i in $(seq 1 300); do
  curl -so /dev/null -w "%{http_code}\n" https://tudominio.com/
done | sort | uniq -c

Esto lanza 300 peticiones y te muestra cuántas han recibido cada código HTTP. Si el rate limit está configurado a 100 peticiones/segundo con burst 200, verás algo como:

200 200
100 429

200 peticiones han pasado (el burst), y 100 han recibido 429. El rate limit funciona.

Puedes ser más preciso midiendo el tiempo:

# Una petición cada 0.05 segundos (20 por segundo)
for i in $(seq 1 50); do
  curl -so /dev/null -w "%{http_code} " https://tudominio.com/
  sleep 0.05
done
echo

Si tu rate limit es 100 average, 20 peticiones por segundo no deberían activarlo. Si ves 429, algo está mal configurado.

Probar rate limit con herramientas de benchmark

Para pruebas más serias, usa ab (Apache Bench) o wrk. Están diseñados para estresar servidores HTTP.

# Instalar ab
sudo apt install apache2-utils

# 500 peticiones, 10 concurrentes
ab -n 500 -c 10 https://tudominio.com/

ab te muestra:

  • Número de peticiones completadas vs fallidas.
  • Tiempo medio por petición.
  • Peticiones por segundo.

Si ves peticiones fallidas con código 429, el rate limit está haciendo su trabajo.

wrk es más moderno y configurable:

# Instalar wrk
sudo apt install wrk

# 30 segundos, 20 conexiones concurrentes
wrk -t4 -c20 -d30s https://tudominio.com/

wrk no muestra códigos HTTP individuales, pero te da el throughput y la latencia. Si el rate limit se activa, verás una caída drástica en el throughput después del burst inicial.

Importante: no hagas estas pruebas contra servidores en producción sin avisar. Puede parecer un ataque real. Hazlo en un entorno de pruebas o en horas de bajo tráfico.

Probar whitelist

Para comprobar la whitelist, necesitas hacer peticiones desde una IP que no esté en la lista.

# Desde una IP fuera de la whitelist
curl -I https://grafana.tudominio.com/

Deberías recibir:

HTTP/2 403

Si recibes un 200, la whitelist no está bien configurada. Revisa que el middleware está aplicado al router correcto y que los sourceRange son los que crees.

Puedes simular una IP diferente usando curl con --header para cambiar X-Forwarded-For:

# Falsificar IP (no siempre funciona)
curl -H "X-Forwarded-For: 1.2.3.4" https://grafana.tudominio.com/

Pero esto solo funciona si Traefik está configurado para confiar en esa cabecera. Por defecto, Traefik usa la IP real de conexión. La cabecera X-Forwarded-For solo se tiene en cuenta si configuras ipStrategy en el middleware.

Por eso, la prueba real es desde una máquina externa con una IP diferente. Un VPS, un servidor en la nube, o el móvil con datos.

Probar circuit breaker

Simular un backend caído es fácil. Para un servicio, simplemente páralo:

docker stop nombre-del-servicio

Luego haz peticiones al servicio a través de Traefik:

for i in $(seq 1 20); do
  curl -so /dev/null -w "%{http_code}\n" https://servicio.tudominio.com/
  sleep 1
done

Al principio recibirás 502 (Bad Gateway) o timeout. Después de unos segundos (el checkPeriod), Traefik abre el circuito y empiezas a recibir 503 directamente. El backend ya no recibe peticiones.

Vuelve a arrancar el servicio:

docker start nombre-del-servicio

Después del fallbackDuration, Traefik intentará recuperarse (half-open) y, si el backend responde, cerrará el circuito.

Verificar la cadena completa

La mejor forma de verificar que toda la cadena de middlewares funciona correctamente es revisar el dashboard de Traefik. Accede a la sección de routers y busca tu servicio. Deberías ver los middlewares listados en el orden correcto.

# Si el dashboard está accesible (por VPN o whitelist)
curl -s https://traefik.tudominio.com/api/http/routers | jq .

Esto te da la configuración actual de todos los routers, incluyendo los middlewares asociados. Si no ves algún middleware que esperabas, revisa la etiqueta o el archivo dinámico.

Resumen

Este capítulo ha sido denso, pero has ganado herramientas para controlar quién y cómo accede a tus servicios.

  • Middlewares: módulos que procesan peticiones antes de que lleguen al backend. Se encadenan y se ejecutan en orden.
  • Rate limiting: limita peticiones por IP con token bucket. Average y burst definen el límite suave y el pico permitido. Úsalo en todos los servicios, con límites más estrictos en logins y APIs.
  • IP whitelist: solo IPs autorizadas pasan. Perfecto para dashboards, administración y servicios internos. Ponlo siempre primero en la cadena.
  • Orden correcto: whitelist > rate limit > auth > headers > compress. Cada capa filtra antes de llegar a la siguiente.
  • Compresión gzip: opcional, mejora rendimiento. Al final de la cadena, solo para respuestas exitosas.
  • Circuit breaker: protege backends caídos. Abre el circuito cuando detecta muchos errores y deja de enviar tráfico.
  • Archivo dinámico vs Docker labels: los middlewares globales van en el archivo dinámico. Los específicos, en las labels del contenedor. Combínalos.
  • Doble capa con fail2ban: el rate limit de Traefik frena abusos suaves. fail2ban banea IPs persistentes. Las dos capas se complementan.
  • Verificación: curl, ab, wrk para probar rate limit. Peticiones desde IP externa para whitelist. Parar servicios para circuit breaker. Dashboard de Traefik para revisión general.

En el próximo capítulo vamos a por las cabeceras de seguridad HTTP. HSTS para forzar HTTPS, CSP para controlar qué scripts se ejecutan, X-Frame-Options para evitar clickjacking, y un puñado más que harán que tu servidor saque un 10/10 en seguridad web.

Tus servicios ya tienen portero, lista de invitados y límite de consumiciones. Ahora toca ponerles el traje antibalas.


Más información,

Deja una respuesta