En el último post, en donde comencé a explorar el uso de Traefik en Producción, podemos encontrar una serie de recetas/archivos que deberían permitir instanciar un blog con WordPress en pocos minutos.

Si revisamos el docker-compose.yml usado para desplegar (o disponibilizar) Traefik, en la parte de los labels encontraremos dos muy específicas:

  • traefik.http.routers.traefik.middlewares=auth-traefik
  • traefik.http.middlewares.auth-traefik.basicauth.users=usuario:contraseña_cifrada

Haciendo uso del middleware BasicAuth, encontramos un usuario y una contraseña que, en el ejemplo mostrado, harán que el acceso al Dashboard de Traefik necesite de esos datos para autenticarse y poder tener acceso.

Ahora bien, además de usar la opción users, podemos usar userFile. La diferencia principal es que esta opción nos permite también tener una lista de usuarios en un archivo externo a nuestro docker-compose.yml. Esto podría traducirse, según el caso (y dependiendo del TOC de cada uno), en una mejor legibilidad y mantenibilidad (?) de nuestros archivos/recetas.

Si volvemos sobre el ejemplo compartido en el post anterior, vemos que sólo para el container de Traefik estoy usando autenticación (con la opción users). El docker-compose.yml en cuestión es:

version: '3'

services:
  traefik:
    restart: always
    image: traefik:2.10
    container_name: traefik
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/etc/traefik/letsencrypt
      - ./traefik.yml:/etc/traefik/traefik.yml
      - ./log:/var/log
    labels:
      - traefik.enable=true
      - traefik.http.routers.traefik.rule=Host(`monitor.dominio.com.ar`)
      - traefik.http.routers.traefik.service=api@internal
      - traefik.http.routers.traefik.tls=true
      - traefik.http.routers.traefik.tls.certresolver=letsencrypt
      - traefik.http.routers.traefik.middlewares=auth-traefik
      - traefik.http.middlewares.auth-traefik.basicauth.users=usuario:contraseña_cifrada
    networks:
      - web

networks:
  web:
    external: true

Y el docker-compose.yml usado de ejemplo para levantar un sitio con WordPress fue:

version: "3"

networks:
  web:
    external: true
  internal:
    external: false

services:
  blog:
    image: wordpress:6.3.0-apache
    container_name: blog
    restart: always
    env_file:
      - ./blog.env
    labels:
      - traefik.enable=true
      - traefik.http.routers.blog.rule=Host(`blog.dominio.com.ar`)
      - traefik.http.routers.blog.tls=true
      - traefik.http.routers.blog.tls.certresolver=letsencrypt
      - traefik.port=80
    cap_drop:
      - SETGID
      - SETUID
    networks:
      - internal
      - web
    depends_on:
      - database

  database:
    image: mariadb:10.11
    container_name: database
    restart: always
    env_file:
      - ./blog.env
    networks:
      - internal
    labels:
      - traefik.enable=false
    volumes:
      - ./mysql:/var/lib/mysql

  phpmyadmin:
    image: phpmyadmin/phpmyadmin:latest
    container_name: phpmyadmin
    restart: unless-stopped
    env_file:
      - ./blog.env
    labels:
      - traefik.enable=true
      - traefik.http.routers.phpmyadmin.rule=Host(`phpmyadmin.dominio.com.ar`)
      - traefik.http.routers.phpmyadmin.tls=true
      - traefik.http.routers.phpmyadmin.tls.certresolver=letsencrypt
      - traefik.port=80
    networks:
      - internal
      - web
    depends_on:
      - database

volumes:
  mysql:

Momento de dar contexto al ejemplo que va a explicar lo que vamos a hacer aquí con las autenticaciones.

El objetivo será tener un nuevo middleware que usará para autenticar (únicamente) el container de PhpMyAdmin. Lo haré de esta manera para no compartir las credenciales con el Dashboard de Traefik, pero además para tener una lista de usuarios que podrán autenticarse (pensando que a futuro puedo necesitar impedir a un usuario en particular el acceso a ese servicio, a nivel de container).

Como me voy a concentrar sólo en el container de PhpMyAdmin, voy a seguir el ejemplo sólo con los labels de ese container (al final tendremos el archivo completo).

Lo primero que hago es agregar el nuevo middleware, usando un nombre bien claro (aquí): nuevo-middleware-de-autenticacion

labels:
  - traefik.enable=true
  - traefik.http.routers.phpmyadmin.rule=Host(`phpmyadmin.dominio.com.ar`)
  - traefik.http.routers.phpmyadmin.tls=true
  - traefik.http.routers.phpmyadmin.tls.certresolver=letsencrypt
  - traefik.port=80
  - traefik.http.routers.phpmyadmin.middlewares=nuevo-middleware-de-autenticacion
  - traefik.http.middlewares.nuevo-middleware-de-autenticacion.basicauth.usersfile=./archivo_de_usuarios

Y al final defino para ese middleware el uso de BasicAuth con la opción UsersFile, con el path a mi archivo.

Si ahora intento recrear mis containers, veremos que el container de PhpMyAdmin no va a funcionar y que si miramos el middleware en el Dashboard veremos algo así:

Lo que nos pasa, más allá de lo obvio, es que Traefik no encuentra el archivo que le estamos diciendo que debe usar. Para ahorrar algo de tiempo, voy a saltar a la explicación del problema y su solución (sin pasar por las varias pruebas que tuve que hacer hasta que llegué a la solución).

El primer detalle a considerar aquí es que la documentación no es lo suficientemente clara (o al menos se presta a confusión). El path al archivo debe ser indicado en relación al container de Traefik y no del container del servicio que quiere usarlo o del lugar físico en el host.

Esto quiere decir que el primer cambio debemos aplicarlo sobre el docker-compose.yml de Traefik (recordar que estamos basando todo sobre el ejemplo de este post).

Supongamos que creamos entonces, dentro del mismo directorio en donde tenemos los archivos/recetas de Traefik, un archivo (o varios si vamos a usar diferentes middlewares para distintos containers) al que llamaremos autenticacion (o lo que quieran) con algo como:

usuario1:$apr1$B95Jx5XI$0xeUpcNoepwMgJ2h2uLJe0
usuario2:$apr1$CVa1azC3$Stcv7.43rKES6X74sz6Sw1

En donde el par usuario y contrseña es usuario1:password1 y usuario2:password2.

Vamos a cambiar el docker-compose de Traefik.

version: '3'

services:
  traefik:
    restart: always
    image: traefik:2.10
    container_name: traefik
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/etc/traefik/letsencrypt
      - ./traefik.yml:/etc/traefik/traefik.yml
      - ./log:/var/log
      - ./autenticacion:/etc/traefik/autenticacion
    labels:
      - traefik.enable=true
      - traefik.http.routers.traefik.rule=Host(`monitor.dominio.com.ar`)
      - traefik.http.routers.traefik.service=api@internal
      - traefik.http.routers.traefik.tls=true
      - traefik.http.routers.traefik.tls.certresolver=letsencrypt
      - traefik.http.routers.traefik.middlewares=auth-traefik
      - traefik.http.middlewares.auth-traefik.basicauth.users=usuario:contraseña_cifrada
    networks:
      - web

networks:
  web:
    external: true

En la lista de volúmenes, en la última línea, estoy montando el archivo que acabo de crear y lo mapeo con /etc/traefik/autenticacion dentro del container de Traefik (es importante no confundir este detalle, ya que es Traefik el que debe poder leer el archivo).

Ahora volvamos sobre la receta del blog y cambiemos los labels de PhpMyAdmin.

version: "3"

networks:
  web:
    external: true
  internal:
    external: false

services:
  blog:
    image: wordpress:6.3.0-apache
    container_name: blog
    restart: always
    env_file:
      - ./blog.env
    labels:
      - traefik.enable=true
      - traefik.http.routers.blog.rule=Host(`blog.dominio.com.ar`)
      - traefik.http.routers.blog.tls=true
      - traefik.http.routers.blog.tls.certresolver=letsencrypt
      - traefik.port=80
    cap_drop:
      - SETGID
      - SETUID
    networks:
      - internal
      - web
    depends_on:
      - database

  database:
    image: mariadb:10.11
    container_name: database
    restart: always
    env_file:
      - ./blog.env
    networks:
      - internal
    labels:
      - traefik.enable=false
    volumes:
      - ./mysql:/var/lib/mysql

  phpmyadmin:
    image: phpmyadmin/phpmyadmin:latest
    container_name: phpmyadmin
    restart: unless-stopped
    env_file:
      - ./blog.env
    labels:
      - traefik.enable=true
      - traefik.http.routers.phpmyadmin.rule=Host(`phpmyadmin.dominio.com.ar`)
      - traefik.http.routers.phpmyadmin.tls=true
      - traefik.http.routers.phpmyadmin.tls.certresolver=letsencrypt
      - traefik.port=80
      - traefik.http.routers.phpmyadmin.middlewares=nuevo-middleware-de-autenticacion
      - traefik.http.middlewares.nuevo-middleware-de-autenticacion.basicauth.usersfile=/etc/traefik/autenticacion
    networks:
      - internal
      - web
    depends_on:
      - database

volumes:
  mysql:

Al recrear tanto el container de Traefik como el de PhpMyAdmin, veremos que el middleware ya no presenta errores.

Además, la autenticación debería responder correctamente (y de forma independiente de la definida para el Dashboard de Traefik).