You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

43 KiB

weight title date draft summary categories tags
4 Apuntes de Traefik v3 2024-11-08T20:17:21+0100 false Apuntes de Traefik v3
notes
selfhosted
vps
docker
traefik

Apuntes sobre Traefik v3.

{{< admonition type=info title="Artículo Actualizado" open=true >}}

ESTE ARTÍCULO YA ES VÁLIDO

Este artículo es una actualización del [post inicial]({{% ref "/posts/notes_general/notes_traefik" %}}) redactado para Traefik V2.0.

Para que las configuraciones propuestas funcionen en la nueva versión 3 de Traefik en realidad no ha sido necesario cambiar nada. La actualización se ha limitado a eliminar las referecias version en los ficheros docker-compose y a renombrar las redes internas de Docker. Estas redes se han renombrado a frontnety backnet para evitar ambigüedades.

{{< /admonition >}}

Traefik

{{< admonition type=abstract title="Referencias" state=open >}}

Estas notas sobre Traefik son un refrito de las siguientes fuentes:

{{< /admonition >}}

Edge Router

Traefik es un edge router (router de borde, router frontera, proxy inverso) Es decir que es el punto de entrada a tu servidor (o plataforma) y se encarga de interceptar cada petición de entrada y de enrutarlas a los servicios correspondientes. Puede hacer el enrutamiento basándose en muchos criterios diferentes (nombre del host, cabeceras, etc.)

{{< admonition type=tip title="¿Para qué sirve?" state=open >}} Lo normal es que instales Traefik entre tu platafoma de servicios e Internet. De esta forma no tienes que abrir mas que uno o dos puertos hacia la red (tipicamente el 80 y el 443) y se podrá atender todas las peticiones a los distintos servicios implementados en nuestra plataforma a través de sólo estos dos puertos de entrada.

En este artículo Plataforma de servicios equivale a un VPS, a una raspi en tu casa, a un servidor dedicado, etc. etc.

{{< /admonition >}}

auto-discovery de servicios

Normalmente los edge_router o proxy inversos necesitan un fichero de configuración detallando las reglas para enrutar las peticiones a los servicios. Traefik obtiene las reglas de los propios servicios. Así que en el momento de desplegar un nuevo servicio, un contenedor Docker en lo que respecta a este documento, especificaremos la información que ese servicio debe dar a Traefik para establecer los enrutamientos.

De esta forma cuando se despliega un servicio, Traefik lo detecta y actualiza las reglas en tiempo real. De la misma forma, si el servicio se cae o se para, Traefik elimina las rutas asociadas en tiempo real.

Para descubrir los servicios Traefik usa el API de la plataforma que los alberga, en el lenguaje propio de Traefik usa un Provider. Como nosotros vamos a usar Traefik en Docker vamos a centrarnos en el docker provider.

Ejemplo 1: Uno facilito

El siguiente fichero docker-compose contiene una configuración lo más sencilla posible con el docker provider.

Preparamos los ficheros y directorios de trabajo:

mkdir -p ~/work/docker/ejemplo_01/traefik
touch ~/work/docker/ejemplo_01/traefik/docker-compose.yml

El contenido del fichero ~/work/docker/ejemplo01/traefik/docker-compose.yml sería:

services:
  reverse-proxy:
    # The official v3 Traefik docker image
    image: traefik:v3.0
    # Enables the web UI and tells Traefik to listen to docker
    command: --api.insecure=true --providers.docker
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock

Este fichero docker-compose:

  • Define el servicio reverse-proxy
    • Basado en la imagen traefik:v3.0 En realidad en el momento de escribir esto deberíamos usar traefik:v3.2.0
    • pasamos las opciones --api.insecure=true y --providers.docker al comando de arranque de la imagen Traefik
    • Conectamos los puertos 80 y 8080 del contenedor al exterior (a la red host)
    • Definimos un bind mount para que el socket de comunicación con Docker esté disponible para el contenedor. Lo necesita para que Traefik pueda conectar el provider docker al API de Docker en la máquina host

{{< admonition type=danger title="API Insecure" open=true >}}

¡Cuidado! la opción api.insecure no debe usarse nunca en producción. Con esta opción estamos dejando el acceso al API de nuestro Traefik abierto y sin protección.

Más adelante veremos opciones para configurar de forma segura el acceso al API.

{{< /admonition >}}

En cuanto lanzemos nuestro docker-compose:

docker compose --verbose up -d

Podremos acceder al dashboard de Traefik visitando http://my_server_ip:8080, donde my_server_ip será la dirección del server donde has lanzado el contenedor o localhost si estás haciendo las pruebas en tu ordenador personal.

{{< admonition type=tip title="firewall" open=true >}}

Si estás lanzando el contenedor en un servidor moderno (Debian 12 Server en nuestro caso) ya no usas iptables sino nftables para implementar el firewall.

Docker sigue usando comandos iptables para abrir los puertos y establecer las reglas nat necesarias para los contenedores arrancados. Esos comandos se traducen a comandos nftables con herramientas del sistema operativo (iptables-nft)

Puedes comprobar las reglas nftables creadas por Docker listando todas las reglas con sudo nft list ruleset.

Docker inserta nuevas reglas en las tablas ip filter e ip6 filter Y también en las dos tablas ip nat e ip6 nat.

Para nuestro contenedor de ejemplo tendríamos:

sudo nft list ruleset

# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
.
.
chain DOCKER {
		iifname != "br-c044d323c53d" oifname "br-c044d323c53d" ip daddr 172.18.0.2 tcp dport 80 counter packets 12 bytes 600 accept
		iifname != "br-c044d323c53d" oifname "br-c044d323c53d" ip daddr 172.18.0.2 tcp dport 8080 counter packets 10 bytes 564 accept
	}

	chain DOCKER-ISOLATION-STAGE-1 {
		iifname "br-c044d323c53d" oifname != "br-c044d323c53d" counter packets 814 bytes 1292817 jump DOCKER-ISOLATION-STAGE-2
		iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2
		counter packets 1931 bytes 1511228 return
	}

	chain DOCKER-ISOLATION-STAGE-2 {
		oifname "br-c044d323c53d" counter packets 0 bytes 0 drop
		oifname "docker0" counter packets 0 bytes 0 drop
		counter packets 847 bytes 1295253 return
	}

	chain DOCKER-USER {
		counter packets 1931 bytes 1511228 return
	}
.
.
# Warning: table ip6 filter is managed by iptables-nft, do not touch!
table ip6 filter {
.
.
	chain DOCKER {
	}

	chain DOCKER-ISOLATION-STAGE-1 {
		iifname "br-c044d323c53d" oifname != "br-c044d323c53d" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2
		iifname "docker0" oifname != "docker0" counter packets 0 bytes 0 jump DOCKER-ISOLATION-STAGE-2
		counter packets 0 bytes 0 return
	}

	chain DOCKER-ISOLATION-STAGE-2 {
		oifname "br-c044d323c53d" counter packets 0 bytes 0 drop
		oifname "docker0" counter packets 0 bytes 0 drop
		counter packets 0 bytes 0 return
	}

	chain DOCKER-USER {
		counter packets 0 bytes 0 return
	}
.
.
.
# Warning: table ip nat is managed by iptables-nft, do not touch!
table ip nat {
	chain DOCKER {
		iifname "br-c044d323c53d" counter packets 0 bytes 0 return
		iifname "docker0" counter packets 0 bytes 0 return
		iifname != "br-c044d323c53d" tcp dport 80 counter packets 12 bytes 600 dnat to 172.18.0.2:80
		iifname != "br-c044d323c53d" tcp dport 8080 counter packets 10 bytes 564 dnat to 172.18.0.2:8080
	}

	chain POSTROUTING {
		type nat hook postrouting priority srcnat; policy accept;
		oifname != "br-c044d323c53d" ip saddr 172.18.0.0/16 counter packets 6 bytes 410 masquerade
		oifname != "docker0" ip saddr 172.17.0.0/16 counter packets 0 bytes 0 masquerade
		ip saddr 172.18.0.2 ip daddr 172.18.0.2 tcp dport 80 counter packets 0 bytes 0 masquerade
		ip saddr 172.18.0.2 ip daddr 172.18.0.2 tcp dport 8080 counter packets 0 bytes 0 masquerade
	}

	chain PREROUTING {
		type nat hook prerouting priority dstnat; policy accept;
		fib daddr type local counter packets 15705 bytes 853686 jump DOCKER
	}

	chain OUTPUT {
		type nat hook output priority -100; policy accept;
		ip daddr != 127.0.0.0/8 fib daddr type local counter packets 0 bytes 0 jump DOCKER
	}
}
table ip6 nat {
	chain DOCKER {
	}
}

Podemos comprobar que, como comentamos, la interacción de Docker con nuestro cortafuegos nftables se hace a través de comandos iptables, y las reglas se insertan mediante la herramienta iptables-nft, que se encarga de traducir comandos de iptables en comandos de nftables.

Si quieres aprender más de la gestión que hace Docker del cortafuegos aquí tienes mas información

{{< /admonition >}}

Con este Traefik básico que hemos lanzado, ya prodríamos lanzar servicios que conectaran con el mismo. Vamos a añadir unas lineas a nuestro fichero docker-compose, que quedaría así:

services:
  reverse-proxy:
    # The official v3 Traefik docker image
    image: traefik:v3.2.0
    # Enables the web UI and tells Traefik to listen to docker
    command: --api.insecure=true --providers.docker
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
  # ...
  whoami:
    # A container that exposes an API to show its IP address
    image: traefik/whoami
    labels:
      - "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)"

Hemos incluido un nuevo servicio whoami en nuestro fichero docker-compose.yml. Es un servicio bastante chorras que nos facilita la gente de Traefik para hacer pruebas. Simplemente responde peticiones con algunos datos como direcciones IP o o el nombre de la máquina servidora. En este caso responderá con datos de la "máquina virtual", es decir el contenedor whoami dentro de Docker.

Si levantamos el nuevo servicio con docker-compose up -d whoami podremos consultar el dashboard de Traefik http://my_server_ip:8080 o directamente el raw api http://my_server_ip:8080/api/rawdata y comprobaremos que Traefik ha detectado el nuevo servicio whoami y ha configurado la ruta.

Si queremos comprobarlo en la práctica podemos ejecutar una request con curl, con el comando: curl -H Host:whoami.docker.localhost http://my_server_ip

curl -H Host:whoami.docker.localhost http://my_server_ip
Hostname: 0e1584c3151f
IP: 127.0.0.1
IP: 172.19.0.3
RemoteAddr: 172.19.0.2:55008
GET / HTTP/1.1
Host: whoami.docker.localhost
User-Agent: curl/7.68.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 192.168.0.154
X-Forwarded-Host: whoami.docker.localhost
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: 3b4d7b2f09be
X-Real-Ip: 192.168.0.154

¿Qué esta pasando aquí?

Traefik abre dos entry points uno atiende peticiones en el puerto 80 y otro en el 8080 (para acceder al dashboard).

Cuando lanzamos nuestra petición con curl Traefik la recibe por el puerto 80, comprueba que hay un router que encaja con la cabecera (whoami.docker.localhost) y, de acuerdo con esa regla, le pasa la petición al contenedor del servicio whoami.

Este router se configuró automáticamente gracias a la etiqueta traefik.http.routers.whoami.rule=Host('whoami.docker.localhost') que asociamos al contenedor whoami en el fichero docker-compose.yml. Estas etiquetas son procesadas a través del provider Docker para reconfigurar Traefik en tiempo real.

{{< admonition type=warning title="localhost vs my_server_ip" state=open >}}

Yo estoy probando Traefik en un servidor en mi intranet, en mi caso un mini-pc, en tu caso podría ser una raspi, o podrías estar haciendo las pruebas en un vps. O también podrías estar haciendo las pruebas en tu propio ordenador.

Si estás haciendo las pruebas en tu propio ordenador, tienes que usar localhost o 127.0.0.1 en todos los sitios donde he dicho my_server_ip, si estás usando un servidor de cualquier tipo, pues la ip correspondiente. Si tienes un VPS correctamente enrutado en el DNS podrías usar el nombre de tu VPS con dominio, p. ej. http://miHost.miDominio.com/whoami, tanto para la etiqueta en el fichero docker-compose como para la dirección a visitar con el navegador.

En nuestro caso de ejemplo hemos declarado la ruta para Traefik como si estuvieramos en nuestro ordenador (localhost), así que para hacer la prueba rápida con curl, especificamos la cabecera de nuestra request como whoami.docker.localhost aunque después encaminamos la request a nuestro servidor con la URL http://<direccionIp>, donde dirección IP es 127.0.0.1 si estás probando en tu ordenador local, o la que corresponda a la máquina remota.

{{< /admonition >}}

Con esta configuración tan sencilla podemos ver otra funcionalidad muy importante de Traefik, si lanzamos un segundo servidor whoami (un segundo contenedor) con el comando:

docker compose up -d --scale whoami=2

Traefik se encargará de hacer reparto de carga entre los dos servidores. Si lanzamos el comando curl anterior, veremos como nos van respondiendo los dos contenedores; veremos como la dirección correspondiente al contenedor whoami que contesta la petición va cambiando entre los dos valores: 172.19.0.3 y 172.19.0.4 (aunque en cada escenario de pruebas estas IP pueden cambiar)

Para parar los contenedores ejecuta docker compose down.

Conceptos centrales en Traefik

Configuraciones de Traefik

En Traefik distinguimos entre dos configuraciónes:

static configuration

La configuración de arranque de Traefik, aquí se definen los _Providers (Docker en nuestro ejemplo anterior) y los entry points (los puertos 80 y 8080 en nuestro ejemplo anterior). Es decir, elementos que no cambian muy a menudo a lo largo de la vida del edge router

Traefik lee su configuración estática de tres posibles orígenes (mutuamente exclusivos):

  1. Fichero de configuración
  2. Parámetros por linea de comandos
  3. En variables de entorno
dynamic configuration

La configuración dinámica del enrutado. Comprende toda la definición del procesado de las request recibidas por el router. Esta configuración puede cambiar y se recarga en caliente sin parar el router y sin provocar pérdidas de conexión o interrupción del servicio.

Traefik obtiene su configuración dinámica a través de los Providers. En el ejemplo anterior Docker informa a Traefik de las rutas que necesita cada servicio gracias a las etiquetas (Labels) que añadimos en la configuración (del servicio) (ver referencia). Es un caso de configuración dinámica de Traefik a traves del Docker Provider.

Providers

Son los agentes capaces de cambiar la configuración dinámica de Traefik hay varios tipos: file, Kubernetes, Docker etc. etc.

En nuestro caso nos vamos a centrar en el docker provider

Los providers son parte de la configuración estática de Traefik.

Entry Point

Son los componentes principales del "frontend" de nuestro edge router. Básicamente son los puertos de entrada donde se reciben las peticiones de servicio del "exterior".

Los entry point son parte de la configuración estática.

{{< admonition type=tip title="Traefik requests" state=open >}}

Traefik puede atender peticiones HTTP o peticiones TCP por qué puede actuar como un router de nivel 7 (HTTP) o de nivel 4 (TCP).

{{< /admonition >}}

Services

Serían los componentes del "backend", en nuestro caso serán servicios implementados en contenedores Docker.

Los services son parte de la configuración dinámica.

Routers

Son las reglas para pasar peticiones desde el "frontend" al "backend". Se implementan y reconfiguran por la información recibida por los providers.

Los routers son parte de la configuración dinámica.

Middlewares

Pueden hacer cambios en las peticiones o en las respuestas a peticiones. Hay multitud de middlewares y además se pueden encadenar para acumular cambios.

Los middlewares son parte de la configuración dinámica.

Ejemplo 2: Certificados SSL con Let's Encrypt

Vamos con un ejemplo un pelín más complejo. Vamos a definir un contenedor de Traefik que simplemente expone su Dashboard, de momento no vamos a definir más contenedores, pero podremos añadirlos a este escenario en el futuro.

La gracia del ejemplo es que Traefik obtendrá los certificados SSL del dominio (p.ej. miDominio.com) automáticamente desde Let's Encrypt. Y expondrá el dashboard en un subdominio, digamos en https://dashtraefik.miDominio.com.

Preparamos los directorios y los ficheros de trabajo.

mkdir -p ~/work/docker/ejemplo_02/traefik
touch ~/work/docker/ejemplo_02/traefik/{traefik.yml,traefik_dynamic.yml,acme.json}
chmod 600 ~/work/docker/ejemplo_02/traefik/acme.json
  • traefik.yml será el fichero de configuración estática de Traefik.
  • traefik_dynamic.yml será el fichero de configuración dinámica de Traefik
  • acme.json es el fichero que almacenará los certificados HTTPS. Es imprescindible configurar los permisos correctamente o no funcionará.

Configuración estática de Traefik

Primero vamos a definir la configuración estática de nuestro Traefik, es imprescindible tener una. La almacenaremos en el fichero traefik.yml y más adelante tendremos que hacer que el contenedor Traefik pueda acceder a esa configuración (con un bind mount por ejemplo)

Dentro de esta configuración definiremos:

entry points

Definiremos dos entry points: http y https que aceptarán peticiones en los puertos 80 y 443.

Los nombres de los entry points son etiquetas arbitrarias, puedes llamarlos como quieras.

entryPoints:
  http:
    address: :80
  https:
    address: :443

La redirección de puertos se solía hacer con un middleware pero desde la version 2.2 de Traefik podemos definirla en la configuración estática añadiendo unas lineas en la propia definición de los entry points:

entryPoints:
  http:
    address: :80
    http:
      redirections:
        entrypoint:
          to: https
  https:
    address: :443

__providers___

: Vamos a definir dos _providers_ diferentes, uno de tipo `docker` y
  otro de tipo `file`.

  Nuestro objetivo es usar **Traefik** en Docker así que siempre deberíamos
  definir el _provider_ docker. Recordemos que a través de este
  _provider_ los contenedores (servicios) que definamos en nuestro
  _Docker_ informarán a **Traefik** de que rutas necesitan para recibir
  peticiones.

  ```traefik.yml
  entryPoints:
    http:
      address: :80
      http:
        redirections:
          entrypoint:
            to: https
    https:
      address: :443

  providers:
    docker:
      endpoint: unix:///var/run/docker.sock
      exposedByDefault: false
    file:
      filename: traefik_dynamic.yml

El docker provider de Traefik se comunica con Docker (en la máquina host) a traves del socket de Docker (en el sistema de ficheros de la máquina _host). Así que necesitaremos que el contenedor Docker pueda acceder al socket con otro bind mount

El parámetro exposedByDefault por defecto vale true y hace que todos los contenedores en Docker sean visibles para Traefik. El valor recomendado es justo el contrario: false, para que Traefik "vea" únicamente los contenedores que tiene que tiene que enrutar.

{{< admonition type=warning >}}

En este ejemplo concreto el docker provider no hara nada. Pero como estamos viendo como usar Traefik con Docker es mejor acostumbrarse a definirlo siempre. En cuanto definamos algún contenedor nos hará falta.

{{< /admonition >}}

Además del provider docker definimos un segundo provider de tipo file. Este segundo provider nos permitirá especificar la configuración dinámica mediante un fichero. En el futuro la configuración dinámica podría ampliarse a través del docker provider si definimos nuevos contenedores.

{{< admonition type=info title="provider file: dirname vs filename" state=open >}}

Podemos definir un provider file especificando un dirname en lugar de un filename. En ese caso podríamos tener varios ficheros en el directorio especificado para añadir modificaciones a la configuración dinámica de Traefik

{{< /admonition >}}

certificates resolvers

Se encargará de negociar los certificados SSL con Let's Encrypt ¡automáticamente!

Los certificates resolvers son parte de la configuración estática.

Hay varias maneras de que Traefik obtenga los certificados, en este ejemplo usamos una muy simple, el http challenge.

entryPoints:
  http:
    address: :80
    http:
      redirections:
        entrypoint:
          to: https
  https:
    address: :443

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
  file:
    filename: traefik_dynamic.yml

certificatesResolvers:
  myresolver:
    acme:
      email: <my_email@mailprovider.com>
      storage: acme.json
      httpChallenge:
        entryPoint: http

Las lineas añadidas al fichero traefik.yml definen un certificates resolver llamado myresolver (es una etiqueta, puedes llamarlo como quieras). Tenemos que especificar un correo electrónico (sólo a efectos de notificaciones) y el fichero (o la ruta) donde Traefik almacenará los certificados obtenidos. Además para el httpChallenge tenemos que usar obligatoriamente el puerto 80, así que especificamos entryPoint: http que previamente asociamos a ese puerto.

Traefik no va a pedir certificados de dominio por iniciativa propia, tendremos que indicar, en la configuración dinámica, que servicios queremos asegurar con un certificado (en principio todos)

Tambien tenemos que acordarnos de que más adelante tendremos que crear un bind mount para tener fácil acceso al fichero acme.json.

{{< admonition type=tip title="Protocolo ACME" state=open >}}

El agente ACME de Traefik genera una pareja de claves pública-privada para hablar con la autoridad certificadora (CA) de Let's Encrypt. Después "pregunta" a la CA que debe hacer para probar que controla un dominio determinado.

La CA le planteará un conjunto de posibles "desafíos". Puede pedirle por ejemplo, que cree un subdominio determinado en el dominio reclamado, o servir un contenido arbitrario en una dirección HTTP del dominio. Junto con los desafíos la CA envia un nonce (un número de un sólo uso)

Si el agente ACME consigue completar alguno de los desafíos planteados. Informa a la CA y le devuelve el nonce firmado con su clave.

La CA comprueba la solución del desafío y que la firma del nonce son válidas. A partir de este punto la pareja de claves asociada al dominio se considera autorizada, y el agente ACME ya puede solicitar certificados y/o revocarlos usando esta pareja de claves.

{{< /admonition >}}

dashboard

Activamos el dashboard. Necesitamos el dashboard activo. Más adelante cuando definamos la configuración dinámica indicaremos en que URL podemos acceder al dashboard y le añadiremos seguridad para que no sea público.

entryPoints:
  http:
    address: :80
    http:
      redirections:
        entrypoint:
          to: https
  https:
    address: :443

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
  file:
    filename: traefik_dynamic.yml

certificatesResolvers:
  myresolver:
    acme:
      email: <my_email@mailprovider.com>
      storage: acme.json
      httpChallenge:
        entryPoint: http

api:
  dashboard: true

Con esto tendríamos completa la configuración estática de nuestro Traefik. Podríamos incluso arrancar nuestro contenedor Traefik en Docker, teniendo cuidado de mapear los puertos 80 y 443 al host y estableciendo los bind mounts necesarios para que el contenedor lea el fichero de configuración estática y el almacen de claves acme.json. Por ejemplo podríamos ejecutar:

    docker run -d \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v $PWD/traefik.yml:/traefik.yml \
      -v $PWD/acme.json:/acme.json \
      -p 80:80 \
      -p 443:443 \
      --name traefik \
      traefik:v3.2.0

Claro que la configuración dinámica estaría vacía, así que nuestro Traefik sería bastante tonto. Expondría los puertos 80 y 443, pero no sabría que hacer con las peticiones que le llegaran a esos puertos. Además, aunque levantaría el dashboard y su acceso en el puerto 8080 del contenedor, este no sería accesible. Por un lado no hemos añadido la opción insecure: true y por otro no hemos establecido usuario y password así que no hay manera de consultar el dashboard (todavía)

Configuración dinámica de Traefik

Vamos a completar la configuración de nuestro edge router definiendo la parte dinámica de la misma.

La vamos a definir en un fichero, pero igual que la estática no hay por que hacerlo así.

Ya hemos dicho que nuestro router básico solo expondrá el dashboard, así que tenemos que definir la capa de seguridad de acceso al dashboard y una ruta apuntando al servicio.

Para usar el middleware Basic Auth, necesitamos declarar parejas de usuario:password. Las contraseñas deben ser hash MD5, SHA1 o BCrypt.

Para generar las credenciales, es decir la pareja de usuario y password "hasheada" podemos usar distintas herramientas.

Con htpasswd:

sudo apt install apache-utils
echo $(htpasswd -nb <USER> <PASSWORD>)

Con openssl:

openssl passwd -apr1

{{< admonition type=info title="Hashes" state=open >}}

Si ejecutas varias veces el comando openssl con la misma contraseña los resultados serán distintos.

Esto es normal, en cada ejecución se cambia el salt para que funcione así. Esto impide, por ejemplo, que un atacante detecte si estas reutilizando contraseñas.

Si te fijas en la salida tiene tres campos delimitados por $$

El primer campo es apr1 el tipo de hash que estamos usando.

El segundo campo es el salt (semilla) utilizado para generar el hash

El tercer campo es el hash propiamente dicho

Por otro lado veréis que en muchos sitios de internet indican que hay que duplicar los símbolos '$' para escaparlos, tipicamente usando algún filtro sed como | sed -E "s:[\$]:\$\$:g" pero solo aplica cuando usamos labels para configurar los servicios, no en nuestro caso ya que usamos un fichero la configuración dinámica.

{{< /admonition >}}

Una vez que tenemos nuestra contraseña super segura en formato hash podemos añadir las siguientes lineas a nuestra configuración dinámica, para definir un middleware que llamaremos myauth:

# Declaring the user list
http:
  middlewares:
    myauth:
      basicAuth:
        users:
          - "user1:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"
          - "user2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"

Una vez definido nuestro middleware de tipo basicAuth solo nos falta definir el router para nuestro dashboard. Lo llamaremos mydash:

http:
  middlewares:
    myauth:
      basicAuth:
        users:
          - "user1:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"
          - "user2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"

  routers:
    mydash:
      rule: Host(`dashtraefik.miDominio.com`)
      middlewares:
      - myauth
      service: api@internal # This is the defined name for api. You cannot change it.
      tls:
        certresolver: myresolver

Con esto completamos la configuración de nuestro Traefik, antes de poder probar tendríamos que asegurarnos de apuntar el nombre dashtraefik.miDominio.com a la dirección IP correcta en nuestro gestor del dominio.

Si ya tenemos el DNS correctamente configurado podríamos lanzar la prueba con:

    docker run -d \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v $PWD/traefik.yml:/traefik.yml \
      -v $PWD/traefik_dynamic.yml:/traefik_dynamic.yml \
      -v $PWD/acme.json:/acme.json \
      -p 80:80 \
      -p 443:443 \
      --name traefik \
      traefik:v3.2.0

Si no hay errores en nuestro DNS ni en la configuración del Traefik comprobaremos que podemos acceder tanto a http://dashtraefik.miDominio.com como https://dashtraefik.miDominio.com (el primero estará redirigido al segundo)

Veremos también que Traefik ha negociado con Let's Encrypt para obtener los certificados SSL para nuestro dominio. Prueba a hacer un cat acme.json en el servidor para ver el contenido del fichero.

En caso de problemas, probablemente el primer paso sea comprobar los logs del contenedor Traefik con docker logs traefik

Ejemplo 3: Una configuración sencilla para empezar con Traefik en Producción

Vamos a ver una configuración sencilla que nos sirva de base para empezar en producción.

Posiblemente quieras crear un usuario específico para mantener el docker en tu servidor.

sudo adduser dockadmin
gpasswd -a dockadmin sudo
gpasswd -a dockadmin docker

Preparamos los directorios y ficheros de trabajo:

mkdir -p ~/work/docker/ejemplo_03/{portainer,traefik/{configurations,letsencrypt}}
touch ~/work/docker/ejemplo_03/{docker-compose.yml,traefik/{traefik.yml,letsencrypt/acme.json,configurations/middlewares.yml}}
chmod 600 ~/work/docker/ejemplo03/traefik/letsencrypt/acme.json

Networks

Vamos a organizar nuestro Docker con dos networks:

frontnet

En la red frontnet tendremos todos los contenedores que expongan un interfaz de usuario "al exterior". Evidentemente nuestro contenedor Traefik tiene que estar en esta red. Otro ejemplo sería un servidor web. En realidad todos los contenedores que van implementar servicios a través de Traefik tienen que estar conectados a esta red. (Podríamos haberla llamado traefiknet)

backnet

Los contenedores que no exponen servicios al exterior solo tienen que estar en esta red. Por ejemplo un servidor de base de datos es muy posible que esté conectado exclusivamente a la red backnet.

Los contenedores de la red frontnet pueden necesitar una conexión adicional con la red backnet. Por ejemplo un contenedor Wordpress tiene que estar en la red frontnet para publicar las páginas web a través de Traefik pero también en la red backnet para comunicarse con el servidor de base de datos.

{{< image src="/images/traefik/TraefikNetworks.jpg" >}}

Creamos las redes en Docker con:

  docker network create --subnet 172.20.0.0/16 backnet
  docker network create --subnet 172.21.0.0/16 frontnet
  docker network ls    # Para listar las redes existentes

Los rangos de direcciones IP son arbitrarios, igual que los nombres (frontnet y backnet) de las redes. Podríamos usar otros cualquiera.

Traefik

Vamos a especificar la configuración de Traefik:

  • Tendremos la configuración estática en el fichero traefik.yml
  • Definiremos un directorio configurations mediante un provider file, todos los ficheros que pongamos en ese directorio se cargarán como parte de la configuración dinámica. Cualquier modificación en los ficheros o cualquier nuevo fichero se cargará "al vuelo", sin necesidad de reiniciar Traefik.

middlewares comunes

En el fichero /configurations/middlewares.yml vamos a definir middlewares que usaremos con muchos (o todos) nuestros contenedores.

Ya sabemos que en Traefik podemos definir la configuración dinámica de muchas formas, podríamos usar labels asociadas al contenedor Traefik para definir estos middlewares, pero creo que así queda más ordenado.

Definimos los middlewares:

secureHeaders

Que define una serie de cabeceras que vamos a aplicar a todas las conexiones HTTP de nuestro Traefik.

  • frameDeny: true Para evitar ataques click-jacking
  • sslRedirect: true Para permitir solo peticiones https
  • browserXssFilter Para paliar ataques de cross site scripting
  • contentTypeNosniff: true Ver referencia
  • forceSTSHeader: true: fuerza cabeceras STS para todas las conexiones, con el flag preload y la directiva includeSubdomains y un max-age de un año.
local-whitelist

Los servicios a los que apliquemos este middleware sólo se podrán acceder desde IP en la whitelist. Este middleware es interesante si accedes a los servicios desde la intranet.

user-auth

Para definir credenciales de acceso a nuestros servicios con basicAuth. Traefik nos pedirá usuario y contraseña para los servicios a los que apliquemos este middleware.

localsec

Este es un ejemplo de "cadena de middlewares", aplicar este middleware equivale a aplicar secureHeaders y local-whitelist. chain es muy interesante para organizar nuestros middlewares y aplicar la misma configuración a varios contenedores.

Contenido del fichero middlewares.yml:

http:
  middlewares:
    secureHeaders:
      headers:
        frameDeny: true
        sslRedirect: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000

    local-whitelist:
      ipWhiteList:
        sourceRange:
          - "10.0.0.0/24"
          - "192.168.0.0/16"
          - "172.0.0.0/8"

    user-auth:
      basicAuth:
        users:
          - "user1:$apr1$MTqfVwiE$FKkzT5ERGFqwH9f3uipxA1"
          - "user2:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"
          - "user3:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"
    localsec:
      chain:
        middlewares:
          - local-whitelist
          - secureHeaders

Configuración estática

En el fichero traefik.yml definimos la configuración estática de Traefik:

  • Activamos el dashboard
  • Definimos los entry points
    • htpp redirigido a https
    • https al que aplicamos los middleware con las cabeceras de seguridad y el certresolver apuntando a Let's Encrypt para generar certificados SSL
  • Definimos los providers
    • docker imprescindible claro
    • file apuntando al directorio /configurations para cargar ficheros que amplien la configuración dinámica
  • Definimos el certresolver para obtener los certificados SSL automáticamente desde Let's Encrypt

El contenido del fichero traefik.yml nos quedaría:

# traefik.yml
api:
  dashboard: true

entryPoints:
  http:
    address: :80
    http:
      redirections:
        entryPoint:
          to: https

  https:
    address: :443
    http:
      middlewares:
        - secureHeaders@file
      tls:
        certResolver: letsencrypt

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    directory: /configurations

certificatesResolvers:
  letsencrypt:
    acme:
      email: youruser@mailprovider.com
      storage: acme.json
      httpChallenge:
        entryPoint: http

docker-compose.yml

Ahora que ya tenemos listos los ficheros de configuración de Traefik vamos a definir el fichero docker-compose.yml:

  • Definimos siempre la versión de nuestras imágenes Docker explicitamente. Es muy mala idea usar :latest en producción, hay que saber que versión usamos y hacer las actualizaciones con conocimiento de causa.
  • Definimos un nombre (traefik) para nuestro contenedor, y definimos una política para restarts
  • Establecemos la opción security_opt: no-new-privileges para impedir escaladas de privilegios con setuid
  • Definimos los volumes del contenedor Traefik
    • Mapeamos /etc/localtime para que el contenedor sincronice la hora con el host
    • Mapeamos el socket de Docker, imprescindible para el provider docker
    • Mapeamos el fichero de configuración estática traefik,yml, el directorio de configuraciones dinámicas /configurations y el fichero acme.json para el certresolver
  • Añadimos etiquetas (labels) para la configuración dinámica del contenedor Traefik (concretamente del dashboard):
    • El servicio (se refiere al dashboard)se proveerá a través de Traefik
    • El contenedor se conecta a la red frontnet
    • Se configura el router traefik-secure
      • Para el servicio Traefik dashboard(api@internal)
      • Con middleware user-auth
      • Con la regla Host(`dashtraefik.yourdomain.com`)
  • Por último declaramos la red (network) frontnet como external ya que la hemos creado previamente.
services:
  traefik:
    image: traefik:v3.2.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - frontnet
    ports:
      - 80:80
      - 443:443
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/traefik.yml:/traefik.yml:ro
      - ./traefik/configurations:/configurations:ro
      - ./traefik/letsencrypt/acme.json:/acme.json
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=frontnet"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`dashtraefik.yourdomain.com`)"
      - "traefik.http.routers.traefik-secure.service=api@internal"
      - "traefik.http.routers.traefik-secure.middlewares=user-auth@file"

networks:
  frontnet:
    external: true

Si ya tenemos nuestro DNS configurado para que el nombre dashtraefik.yourdomain.com se resuelva correctamente ya podemos arrancar nuestro Traefik:

cd ~/work/docker/ejemplo_03
docker-compose --verbose up -d

Ya tenemos nuestro contenedor Traefik funcionando correctamente. Estamos listos para añadir nuevos servicios a nuestra plataforma.

Nuestro primer servicio: Portainer

Vamos a definir nuestro primer servicio conectado por Traefik en el mismo fichero docker-compose.yml. Añadimos la definición del nuevo servicio en las lineas que van de la 27 a la 45:

services:
  traefik:
    image: traefik:v3.2.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - frontnet
    ports:
      - 80:80
      - 443:443
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik/traefik.yml:/traefik.yml:ro
      - ./traefik/configurations:/configurations:ro
      - ./traefik/letsencrypt/acme.json:/acme.json
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=frontnet"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`dashtraefik.yourdomain.com`)"
      - "traefik.http.routers.traefik-secure.service=api@internal"
      - "traefik.http.routers.traefik-secure.middlewares=user-auth@file"

  portainer:
    image: portainer/portainer-ce:2.24.0-alpine
    container_name: portainer
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - frontnet
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./portainer:/data
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=frontnet"
      - "traefik.http.routers.portainer-secure.entrypoints=https"
      - "traefik.http.routers.portainer-secure.rule=Host(`portainer.yourdomain.com`)"
      - "traefik.http.routers.portainer-secure.service=portainer"
      - "traefik.http.services.portainer.loadbalancer.server.port=9000"

networks:
  frontnet:
    external: true

Añadimos un nuevo servicio portainer, con las opciones de rearranque y de seguridad iguales a las de Traefik. Mapeamos también el fichero /etc/localtime para que el contenedor se ponga en hora con el host; y el socket docker.sock que Portainer necesita (Portainer es un frontend para Docker)

Añadimos también las etiquetas para informar a Traefik del nuevo servicio.

  • El servicio se llama portainer
  • Acepta peticiones en el puerto 9000
  • Portainer usará el entry point https de Traefik
  • Y la regla de enrutado será Host(`portainer.yourdomain.com`)

Una vez completada la configuración del DNS, podemos levantar nuestro nuevo servicio con:

docker-compose up -d portainer

{{< admonition type=warning title="Login en Portainer" state=open >}}

Portainer implementa su propio sistema de gestión y autenticación de usuarios. Además da problemas si activamos el middleware user-auth

Tendrás que crear un usuario admin y protegerlo con contraseña tan pronto como lo arranques.

{{< /admonition >}}