Como te comenté en el capítulo anterior, los middlewares de autenticación de Traefik tienen varias caras. Viste BasicAuth para lo sencillo, DigestAuth para lo retrocompatible, y sobre todo el ForwardAuth, que te permite delegar la autenticación a un servicio externo. Pues bien, este capítulo es la aplicación práctica de todo eso.
Vas a integrar Traefik v3 con PocketID, un proveedor OIDC ligero que puedes tener en tu propio servidor. Si has llegado hasta aquí es porque probablemente tienes varios servicios auto-hospedados y te empieza a molar eso de tener un punto único de autenticación sin pasar por Google, GitHub o Microsoft. PocketID te da justo eso: un servicio OIDC que gestiona usuarios, sesiones, grupos, y es compatible con cualquier cosa que hable OIDC.
Y oye, hay alternativas como Authelia o Authentik, que están muy bien y tienen más funcionalidades. Pero si lo tuyo es algo más minimalista, que ocupe menos recursos y tenga una configuración más llevadera, PocketID te va a gustar. Sobre todo porque está pensado desde el inicio para encajar con Traefik mediante el ForwardAuth que ya conoces del capítulo anterior.
Pero, ¿qué es exactamente PocketID? Es un servidor OIDC (OpenID Connect) que implementa el flujo de autorización. Puedes crear usuarios, organizarlos en grupos, configurar clientes, y lo mejor de todo, corre en tu propio hardware. Sin depender de nadie.
Cómo funciona el baile
Antes de meter mano a los ficheros de configuración, tienes que entender el flujo completo. No es magia, es HTTP bien entendido. Fíjate,
- Un usuario accede desde su navegador a
app.tudominio.com, que está detrás de Traefik. - Traefik tiene un middleware de tipo ForwardAuth que apunta al contenedor de pocket-id-forward-auth.
- El forward-auth ve que el usuario no tiene cookie de sesión válida y responde con
401. - Traefik redirige al usuario al login de PocketID.
- El usuario mete sus credenciales. PocketID las valida, genera un token y redirige de vuelta con una cookie de sesión.
- La siguiente petición llega con esa cookie. El forward-auth la valida, responde con
200y mete cabeceras con los datos del usuario. - Traefik deja pasar la petición al servicio de destino con esas cabeceras.
- El servicio recibe quién es el usuario y puede actuar en consecuencia: mostrar su nombre, restringir secciones, lo que sea.
Para el usuario final todo esto es transparente. Ve un formulario de login, mete usuario y contraseña, y ya está dentro. Ni siquiera sabe que hay un proxy inverso, un forward-auth y un OIDC por medio. Y para ti, un solo punto donde decides quién tiene acceso a qué. Todo en casa, sin terceros.
Desplegar PocketID
Vamos al lío. PocketID necesita tres cosas: una base de datos PostgreSQL para guardar usuarios y configuraciones, Redis para gestionar sesiones en memoria, y el propio PocketID. Aquí tienes el docker-compose.yml completo,
services:
pocketid-db:
image: postgres:16-alpine
container_name: pocketid-db
environment:
POSTGRES_DB: pocketid
POSTGRES_USER: pocketid
POSTGRES_PASSWORD: cambia_esto_por_una_contraseña_segura
volumes:
- pocketid-db-data:/var/lib/postgresql/data
networks:
- pocketid-net
restart: unless-stopped
pocketid-redis:
image: redis:7-alpine
container_name: pocketid-redis
volumes:
- pocketid-redis-data:/data
networks:
- pocketid-net
restart: unless-stopped
pocketid:
image: ghcr.io/pocket-id/pocket-id:latest
container_name: pocketid
depends_on:
- pocketid-db
- pocketid-redis
environment:
PUBLIC_APP_URL: https://auth.tudominio.com
DB_CONNECTION: postgres://pocketid:cambia_esto_por_una_contraseña_segura@pocketid-db:5432/pocketid
REDIS_URL: redis://pocketid-redis:6379
# Opcional: registro de nuevos usuarios
# DISABLE_REGISTRATION: false
volumes:
- pocketid-data:/app/data
networks:
- pocketid-net
- proxy
restart: unless-stopped
volumes:
pocketid-db-data:
pocketid-redis-data:
pocketid-data:
networks:
pocketid-net:
internal: true
proxy:
external: true
Fíjate en un par de detalles. La red pocketid-net es interna, lo que significa que PostgreSQL y Redis no son accesibles desde fuera del stack. Solo PocketID puede hablar con ellos. La red proxy es externa, la misma que usas para Traefik. Así PocketID puede recibir peticiones del mundo exterior.
Ojo con PUBLIC_APP_URL. Tiene que ser la URL pública exacta bajo la que accedes a PocketID. Si luego cambias el dominio o el subdominio, las URLs de callback dejarán de funcionar y te tocará reconfigurar todo. Es la base del tinglado, así que acertar con esto desde el principio te ahorrará más de un disgusto.
Una vez levantado con docker compose up -d, accedes a https://auth.tudominio.com y te pedirá crear el primer usuario, que será administrador. A partir de ahí ya tienes tu propio proveedor OIDC funcionando.
Crear una aplicación cliente
Ahora necesitas que Traefik pueda hablar con PocketID. Para eso creas una aplicación cliente desde el panel de administración de PocketID. Los pasos son,
- Ve a Applications y haz clic en Create Application.
- Ponle un nombre que te ayude a identificarla. Algo como «Traefik Forward Auth» o «Proxy principal».
- En Redirect URIs añade la URL de callback. Normalmente es
https://auth.tudominio.com/api/authorization-code/callback. - Configura el tipo como Confidential. No uses público, que esto no es una app de frontend.
- Anota el Client ID y el Client Secret que te genera. Si los pierdes, tendrás que generar otros nuevos y reconfigurar el forward-auth.
No te olvides de apuntarlos.
Es de esas cosas que parecen obvias pero cuando llevas dos horas depurando un error de autenticación, lo primero que piensas es que el client secret está mal. Créeme.
Montar el middleware ForwardAuth
Aquí viene lo interesante. No puedes apuntar el ForwardAuth directamente a la URL de PocketID y esperar que funcione. Necesitas un proxy de autenticación que hable OIDC con PocketID, reciba los tokens y devuelva a Traefik las cabeceras con la identidad del usuario. Ese es el forward-auth propiamente dicho.
Puedes usar oauth2-proxy, que es el todoterreno, o pocket-id-forward-auth, que está hecho específicamente para PocketID. Este último es más ligero y la configuración es más directa. Vamos con él.
Añade este servicio a tu stack,
services:
pocketid-fwd:
image: ghcr.io/pocket-id/pocket-id-forward-auth:latest
container_name: pocketid-fwd
environment:
POCKET_ID_URL: https://auth.tudominio.com
CLIENT_ID: el_client_id_que_anotaste
CLIENT_SECRET: el_client_secret_que_anotaste
REDIRECT_URI: https://auth.tudominio.com/api/authorization-code/callback
SECRET: una_clave_secreta_aleatoria_para_firmar_cookies
COOKIE_DOMAIN: tudominio.com
SESSION_MAX_AGE: 24h
networks:
- proxy
restart: unless-stopped
El SECRET es la clave con la que se firman las cookies de sesión. Si alguien descubre este secreto, puede falsificar cookies y suplantar usuarios. Así que nada de poner «mi-super-secreto» o «1234». Usa esto,
openssl rand -base64 32
Pega el resultado en el SECRET. Y no, no uses el mismo para desarrollo y producción. Ni para varias instancias del forward-auth con distintos ámbitos. Cada una con el suyo.
Configurar el middleware en Traefik
Tienes el forward-auth funcionando. Ahora tienes que decirle a Traefik que lo use. Puedes hacerlo con el File provider o con labels de Docker, según cómo tengas montada la configuración. Aquí está con File provider,
# dynamic-conf.yml
http:
middlewares:
pocketid-auth:
forwardAuth:
address: http://pocketid-fwd:8080/auth
trustForwardHeader: true
authResponseHeaders:
- X-Forwarded-User
- X-Forwarded-Email
- X-Forwarded-Preferred-Username
- X-Forwarded-Groups
La clave está en authResponseHeaders. Esas cabeceras son las que Traefik va a inyectar en la petición cuando el forward-auth responda OK. El servicio de destino las recibe y sabe quién es el usuario. Sin esas cabeceras, el servicio recibe la petición pero no sabe quién la hace. No te olvides de configurarlas.
Si usas labels de Docker, es así,
labels:
- "traefik.http.middlewares.pocketid-auth.forwardAuth.address=http://pocketid-fwd:8080/auth"
- "traefik.http.middlewares.pocketid-auth.forwardAuth.trustForwardHeader=true"
- "traefik.http.middlewares.pocketid-auth.forwardAuth.authResponseHeaders=X-Forwarded-User, X-Forwarded-Email, X-Forwarded-Preferred-Username, X-Forwarded-Groups"
Con esto ya tienes el middleware. Lo siguiente es aplicarlo a los servicios.
Proteger servicios
Un servicio entero detrás de login
El caso más directo. Un subdominio completo solo accesible para usuarios autenticados,
http:
routers:
mi-servicio:
rule: "Host(`app.tudominio.com`)"
service: mi-servicio
middlewares:
- pocketid-auth
Con labels,
labels:
- "traefik.http.routers.mi-servicio.rule=Host(`app.tudominio.com`)"
- "traefik.http.routers.mi-servicio.middlewares=pocketid-auth"
Sin tocar una línea de código de la aplicación. El usuario llega, no tiene sesión, lo redirige a PocketID, se autentica, y vuelve. Todo transparente.
Solo una parte del servicio
¿Y si quieres que la parte pública de la app sea accesible sin login pero el panel de administración esté protegido? Ahí necesitas dos routers para el mismo servicio,
http:
routers:
mi-servicio-publico:
rule: "Host(`app.tudominio.com`) && !PathPrefix(`/admin`)"
service: mi-servicio
mi-servicio-admin:
rule: "Host(`app.tudominio.com`) && PathPrefix(`/admin`)"
service: mi-servicio
middlewares:
- pocketid-auth
El primer router coge todo lo que no sea /admin. El segundo solo /admin y le aplica el middleware. Traefik evalúa los routers en orden, así que el orden en el que los pones importa. Pon primero el más específico o el que usa negaciones, y después el general.
Rate limiting antes y después de autenticar
Aquí puedes liar buena. Una de las cosas que más juego da en Traefik es la capacidad de encadenar middlewares. Puedes combinar rate limiting con PocketID para tener un control fino. Por ejemplo, limitar el tráfico después de autenticar, para no saturar PocketID con peticiones que ya han pasado el corte,
http:
middlewares:
rate-limit-100:
rateLimit:
average: 100
burst: 50
pocketid-auth-y-rate:
chain:
middlewares:
- pocketid-auth
- rate-limit-100
O al revés: rate limiting antes de la autenticación para cortar a los bots calenturientos antes de que toquen PocketID,
http:
middlewares:
rate-limit-antes-auth:
rateLimit:
average: 10
burst: 5
rate-limit-y-pocketid:
chain:
middlewares:
- rate-limit-antes-auth
- pocketid-auth
¿Por qué querrías esto último? Piensa en un bot que está golpeando tu servicio. Cada petición que llega, el forward-auth tiene que validar la cookie, comprobar la sesión en Redis, y decidir si deja pasar. Eso consume recursos. Si cortas antes con rate limiting, el bot ni siquiera llega a molestar a PocketID. Te ahorras CPU, Redis, y dolores de cabeza.
Cómo funcionan las sesiones y los refresh
Una de las dudas más comunes cuando montas esto es cómo gestiona las sesiones. Porque no quieres que el usuario tenga que identificarse cada vez que cambia de página. PocketID, a través del forward-auth, maneja esto con cookies de sesión y refresh tokens. El flujo es el siguiente,
- El usuario se autentica en PocketID con usuario y contraseña.
- PocketID devuelve un ID token que es un JWT con validez corta (suele ser de minutos) y un refresh token con validez más larga (horas o días).
- El forward-auth establece una cookie firmada con el
SECRETque configuraste. Esa cookie contiene los datos de la sesión. - En cada petición, el forward-auth valida la cookie. Si es válida, responde con
200y pasa las cabeceras. - Si el ID token ha expirado pero el refresh token sigue vivo, el forward-auth usa el refresh token para pedir uno nuevo sin que el usuario se entere.
- Si el refresh token también ha expirado, redirige al login de PocketID. El usuario tiene que volver a identificarse.
Para el usuario todo es invisible. Abre la aplicación, si tiene sesión entra directo, si no la tiene ve el formulario de login. Para ti, puedes controlar la duración con estas variables de entorno en el forward-auth,
| Variable | Descripción | Valor típico |
|---|---|---|
SESSION_MAX_AGE | Duración máxima de la sesión activa | 24h |
SESSION_REFRESH_AFTER | Tiempo tras el que se renueva la cookie | 1h |
COOKIE_EXPIRES | Expiración de la cookie en el navegador | 7d |
Si quieres sesiones que duren días sin pedir login, sube SESSION_MAX_AGE a 168h. Pero ojo, a mayor duración, más ventana de exposición si alguien roba la cookie. Es el clásico equilibrio entre comodidad y seguridad.
Cómo depurar si algo falla
Cuando algo no funciona, lo peor es ir a ciegas. Te dejo una batería de comprobaciones rápidas para saber dónde está el problema.
Primero, verifica que el forward-auth responde. Desde cualquier máquina de la red interna,
curl -v http://pocketid-fwd:8080/auth
Si responde con algo que no sea un error de conexión, el forward-auth está vivo. Si no responde, revisa que el contenedor esté corriendo y en la red correcta.
Segundo, comprueba que Traefik está aplicando el middleware. En el dashboard de Traefik (lo vimos en el capítulo de configuración), ve a la sección de routers y busca tu servicio. Deberías ver el middleware pocketid-auth@file o pocketid-auth@docker en la columna de middlewares. Si no aparece, es que no lo has referenciado bien en el router.
Tercero, si el middleware aparece pero la autenticación no funciona, prueba a hacer una petición con las cabeceras adecuadas. Desde tu máquina,
curl -v https://app.tudominio.com/
Si ves una redirección a auth.tudominio.com, el flujo de autenticación está funcionando. Si ves un 503 o 502, el forward-auth no está respondiendo como toca. Revisa los logs,
docker logs pocketid-fwd
Ahí suele estar la respuesta. «Invalid client secret», «Redirect URI mismatch», «Token expired». Todo lo que necesitas saber para dar con el fallo.
Otra comprobación útil: si tienes la sesión iniciada en PocketID y accedes al servicio protegido, puedes inspeccionar las cabeceras que llegan al servicio de destino. Si el servicio es una API, puedes hacer que devuelva las cabeceras que recibe para depurar. Si es una web, abre las herramientas de desarrollador del navegador y mira las peticiones de red. La cookie de sesión debería aparecer en las cabeceras de la petición.
Cerrar sesión
PocketID tiene un endpoint de logout. Puedes crear un middleware que lo invoque,
http:
middlewares:
pocketid-logout:
forwardAuth:
address: http://pocketid-fwd:8080/logout
Aplica ese middleware a una ruta como tudominio.com/logout y cuando el usuario acceda, el forward-auth borra la cookie y redirige al login de PocketID. Sin misterio.
Caso real: tres servicios con distintos niveles
Vamos a montar un escenario que seguro te suena. Tienes tres servicios en tu servidor,
- Una wiki accesible para cualquier usuario autenticado.
- Un panel de administración donde solo entran los admins.
- Una API con dos zonas: una pública y otra privada.
En PocketID creas dos grupos: usuarios y admins. El primer grupo lo tiene cualquiera que se registre. El segundo solo los que tú añadas a mano.
http:
middlewares:
pocketid-auth:
forwardAuth:
address: http://pocketid-fwd:8080/auth
trustForwardHeader: true
authResponseHeaders:
- X-Forwarded-User
- X-Forwarded-Groups
routers:
wiki:
rule: "Host(`wiki.tudominio.com`)"
service: wiki
middlewares:
- pocketid-auth
admin:
rule: "Host(`admin.tudominio.com`)"
service: admin
middlewares:
- pocketid-auth
api-publica:
rule: "Host(`api.tudominio.com`) && PathPrefix(`/v1/public`)"
service: api
api-privada:
rule: "Host(`api.tudominio.com`) && PathPrefix(`/v1/private`)"
service: api
middlewares:
- pocketid-auth
Ahora, ¿cómo gestionas que en el admin solo entren los admins? Tienes dos opciones. La primera es que el propio servicio reciba la cabecera X-Forwarded-Groups y decida. La segunda es que el forward-auth filtre antes de llegar al servicio.
Para la segunda opción despliegas una instancia del forward-auth específica para admins,
services:
pocketid-fwd-admin:
image: ghcr.io/pocket-id/pocket-id-forward-auth:latest
container_name: pocketid-fwd-admin
environment:
POCKET_ID_URL: https://auth.tudominio.com
CLIENT_ID: ...
CLIENT_SECRET: ...
REDIRECT_URI: https://auth.tudominio.com/api/authorization-code/callback
SECRET: ...
ALLOWED_GROUPS: admins
networks:
- proxy
restart: unless-stopped
Con ALLOWED_GROUPS=admins, si un usuario del grupo usuarios intenta entrar en el admin, el forward-auth responde con 403 directamente. El tráfico ni llega al servicio. Es más seguro y más limpio que dejar que la aplicación decida, porque si la aplicación tiene un bug o está mal configurada, el filtro ya ha ocurrido antes.
Solucionar problemas típicos
Cuando montas esto, van a pasar cosas. Te dejo los problemas más frecuentes y cómo salir del paso.
El forward-auth no responde
Lo primero es comprobar que el forward-auth alcanza a PocketID. Desde el contenedor,
docker exec pocketid-fwd wget -qO- http://auth.tudominio.com/.well-known/openid-configuration
Si no responde, tienes un problema de red o de DNS interno. Revisa que ambos contenedores estén en la misma red de Docker y que POCKET_ID_URL sea accesible desde dentro del contenedor. A veces configuras la URL con HTTPS y el contenedor no tiene resolución DNS para el dominio externo, o el certificado no es válido internamente.
Redirige al login aunque ya estoy autenticado
La cookie no se está estableciendo bien. Revisa tres cosas,
COOKIE_DOMAIN: tiene que coincidir con el dominio del servicio protegido, no con el de PocketID. Si tus servicios están en*.tudominio.com, pon.tudominio.comcon el punto delante. Ese punto indica que la cookie es válida para todos los subdominios.SECRET: si tienes varias instancias del forward-auth, todas tienen que usar el mismoSECRETo cada una firmará las cookies de forma distinta y ninguna reconocerá las de las otras.- HTTPS contra HTTP: las cookies de sesión seguras solo se envían en conexiones HTTPS. Si estás probando en local con HTTP, no funciona. No es que esté mal configurado, es que el navegador no las envía por seguridad.
Las cabeceras X-Forwarded no llegan al servicio
Esto no es problema de Traefik ni de PocketID, es cosa de la aplicación de destino. Muchas aplicaciones (Nextcloud, Jellyfin, etc.) ignoran las cabeceras X-Forwarded-* a menos que configures explícitamente qué proxies son de confianza. Busca en la documentación de la app la opción de «trusted proxies» o «reverse proxy» y pon la IP de Traefik (o la red de Docker). Sin eso, la aplicación ve al usuario como anónimo porque descarta las cabeceras.
La sesión se cierra cada pocos minutos
Tienes SESSION_MAX_AGE demasiado bajo o el refresh no se está produciendo bien. Sube SESSION_REFRESH_AFTER a 30m y verifica que el forward-auth puede contactar con PocketID para refrescar los tokens. Si el forward-auth no llega a PocketID para renovar, la sesión caduca cuando expira el ID token.
El login de PocketID no carga o da error SSL
Si PocketID está detrás de Traefik (que es lo normal) y todavía no has configurado TLS, te encuentras con que intentas acceder por HTTPS pero no hay certificado. En el siguiente capítulo verás cómo solucionar eso con Let’s Encrypt, pero hasta entonces puedes acceder a PocketID por HTTP si estás en la misma red, o configurar un certificado autofirmado para las pruebas.
Un apunte sobre seguridad
Ya que estamos, un par de recomendaciones. La primera, no expongas el panel de administración de PocketID a internet sin protección. Si puedes, ponle un middleware de BasicAuth adicional o, mejor, accede solo por VPN. Es el punto que gestiona usuarios, grupos y clientes, y si alguien lo compromete, tiene la llave de todo tu sistema de autenticación.
La segunda, usa contraseñas diferentes para cada servicio de la cadena. La base de datos de PocketID, Redis, el secreto del forward-auth, el client secret de la aplicación. Si uno cae, que no caigan todos.
La tercera, revisa los logs del forward-auth de vez en cuando. Te vas a llevar sorpresas de bots intentando acceder a servicios que no deberían. Ver qué intentos de acceso hay te da una idea de quién está ahí fuera.
En el siguiente capítulo verás cómo añadir TLS y certificados automáticos con Let’s Encrypt para que todo este tinglado vaya por HTTPS. Porque, seamos sinceros, tener autenticación sin HTTPS es como poner una puerta blindada en una casa de cartón.