Federación con Active Directory
En una empresa, las personas ya existen: están en el Active Directory, con su contraseña de Windows y sus grupos. Aquí conectas Keycloak a un AD para que sofia y diego entren en la tienda con esa cuenta. Lo interesante es lo poco que cambia el código Go, y por qué.
En este capítulo
- Trabajas en
- Keycloak conectado a un Active Directory de prueba, y admin-tool, que aprende a tratar usuarios de AD. tienda-web, api-pedidos y facturacion no cambian.
- Carpeta
tienda/pasos/paso-11· realm nuevo- Archivos
- Nuevos:
infra/ad/(AD de prueba) einfra/docker-compose.ldaps.yml; cambianinfra/docker-compose.yml, el realm yinternal/admin/admin.go· ver todos los cambios del paso. - En marcha
- Keycloak y el AD del paso 11 (los levanta el mismo
docker compose up -d), api, web y facturacion. - Comprueba
go -C tools/comprobar run . -paso 11 -servicios(desdecourse/)
Al terminar sabrás
- Qué hace Keycloak con un directorio LDAP o AD (User Federation) y por qué tus servicios Go no se enteran.
- Configurar el proveedor y sus mappers: atributos de AD, grupos de AD como roles de realm.
- Quién manda en cada dato (AD o Keycloak): altas, grupos, contraseñas, bajas.
- Adaptar
admin-toola usuarios que no son de Keycloak y pasar a LDAPS.
El paso trae un controlador de dominio Samba AD DC en Docker: implementa el mismo LDAP que un AD de Windows (sAMAccountName, memberOf, userAccountControl, objectGUID…), así que la configuración de Keycloak es la que usarías contra uno real. Funciona en Windows, macOS (también Apple Silicon) y Linux.
1. La idea: Keycloak habla con el AD; tú hablas con Keycloak
Cuando sofia escribe su usuario y contraseña en la página de login de Keycloak:
- Keycloak busca
sofiaen el AD con una cuenta de servicio de solo lectura (svc-keycloak). - Comprueba la contraseña haciendo un bind LDAP como sofia: si el AD lo acepta, la contraseña es buena. Keycloak no la guarda.
- Lee sus atributos (nombre, correo) y sus grupos (
memberOf), los convierte en datos y roles de Keycloak, y emite los tokens de siempre.
Por eso tienda-web, api-pedidos y facturacion funcionan sin tocar una línea. Solo cambia la herramienta que administra usuarios, porque ahora algunos usuarios no son de Keycloak:
| Componente | ¿Cambia? | Por qué |
|---|---|---|
tienda-web | No | Sigue haciendo Authorization Code + PKCE contra Keycloak. El formulario de login es de Keycloak. |
api-pedidos | No | Valida el JWT con JWKS. Los roles llegan en realm_access, vengan de donde vengan. |
facturacion | No | Client Credentials y Token Exchange no dependen del origen del usuario. |
admin-tool | Sí, poco | Debe distinguir usuarios locales y de AD, y no intentar cambiar en Keycloak lo que manda el AD (sección 6). |
Hay tres maneras de conectar Keycloak con un directorio de Microsoft. Esta lección cubre la primera:
| Forma | Para qué | Cómo |
|---|---|---|
| User Federation LDAP (esta lección) | AD de la empresa (en sus servidores) | Keycloak consulta el AD por LDAP. El login lo pinta Keycloak. |
| Kerberos / SPNEGO | Entrar sin escribir la contraseña desde un PC del dominio | Se añade al proveedor LDAP. Requiere un keytab y configurar el navegador. |
| Identity brokering con Microsoft Entra ID (o ADFS) | Cuentas de Microsoft 365 / Azure | Keycloak delega el login en Microsoft por OIDC o SAML. No hay LDAP. |
2. El AD de prueba
cd tienda/pasos/paso-10/infra && docker compose down -v
cd ../../paso-11/infra && docker compose up -d
docker compose logs -f ad-seed # espera a ver «AD de prueba listo» (Ctrl+C para salir)
La primera vez tarda alrededor de un minuto: Samba crea el dominio tienda.local, ad-seed lo llena y solo entonces arranca Keycloak (que importa el realm con el proveedor ya configurado). El docker-compose.yml añade dos servicios a los de siempre:
# Keycloak + PostgreSQL + un «Active Directory» de prueba (Samba AD DC) para
# el curso "Go + Keycloak". Solo para desarrollo.
#
# docker compose up -d # arrancar (la primera vez tarda: crea el dominio)
# docker compose logs -f ad-seed
# docker compose down -v # parar y BORRAR datos (dominio y realm incluidos)
services:
# Controlador de dominio tienda.local (TIENDA\…). Samba implementa el LDAP
# de Active Directory: grupos, sAMAccountName, memberOf, userAccountControl…
ad:
image: instantlinux/samba-dc:4.23.10-r0
hostname: dc1
cap_add: [SYS_ADMIN] # Samba guarda atributos extendidos del sistema de archivos
environment:
REALM: tienda.local
WORKGROUP: TIENDA
NETBIOS_NAME: DC1
DOMAIN_ACTION: provision # crea el dominio la primera vez
BIND_INTERFACES_ONLY: "no"
secrets:
- samba-admin-password
volumes:
- ad-etc:/etc/samba
- ad-lib:/var/lib/samba
- ./ad/0globals.conf:/etc/samba/conf.d/0globals.conf:ro
# Llena el AD (unidad Tienda, grupos, usuarios) y termina. Es idempotente.
ad-seed:
image: instantlinux/samba-dc:4.23.10-r0
entrypoint: ["sh", "/seed.sh"]
environment:
AD_ADMIN_PASSWORD_FILE: /run/secrets/samba-admin-password
secrets:
- samba-admin-password
volumes:
- ./ad/seed.sh:/seed.sh:ro
depends_on:
- ad
postgres:
image: postgres:17
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
interval: 5s
timeout: 3s
retries: 10
keycloak:
image: quay.io/keycloak/keycloak:26.8.0
# --import-realm importa cada .json de /opt/keycloak/data/import
# (si el realm ya existe, la importación se omite).
command: start-dev --import-realm
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak
KC_HEALTH_ENABLED: "true"
ports:
- "127.0.0.1:8080:8080" # consola y endpoints OIDC
- "127.0.0.1:9000:9000" # puerto de gestión: /health/ready
volumes:
- ./realm:/opt/keycloak/data/import
# «host.docker.internal» = tu máquina, vista desde el contenedor. Docker
# Desktop (Windows, macOS) ya lo define; esta línea lo añade en Linux.
# Lo usa el back-channel logout (lección 4) para llamar a tienda-web.
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
postgres:
condition: service_healthy
ad-seed:
condition: service_completed_successfully
secrets:
samba-admin-password:
file: ./ad/admin-password # contraseña del administrador del dominio (solo desarrollo)
volumes:
pgdata:
ad-etc:
ad-lib:
ad-seed usa samba-tool, la herramienta de administración de Samba, para crear lo que un equipo de TI ya tendría hecho en un AD real:
#!/bin/sh
# Llena el «Active Directory» de prueba (Samba AD DC) con lo que usa la
# tienda. Es idempotente: si algo ya existe, lo deja como está.
set -e
AD_ADMIN_PASSWORD=$(cat "$AD_ADMIN_PASSWORD_FILE")
AUTH="-H ldap://ad -U TIENDA\\Administrator%${AD_ADMIN_PASSWORD}"
echo "esperando al controlador de dominio..."
until samba-tool ou list $AUTH >/dev/null 2>&1; do sleep 3; done
ou() { samba-tool ou list $AUTH | grep -qx "$1" || samba-tool ou create "$1,DC=tienda,DC=local" $AUTH; }
group() { samba-tool group show "$1" $AUTH >/dev/null 2>&1 || samba-tool group add "$1" --groupou="OU=Tienda" $AUTH; }
# user <login> <contraseña> <nombre> <apellido> <ou>
user() {
if ! samba-tool user show "$1" $AUTH >/dev/null 2>&1; then
samba-tool user create "$1" "$2" --use-username-as-cn --given-name="$3" --surname="$4" \
--mail-address="$1@tienda.local" --userou="$5" $AUTH
fi
samba-tool user setexpiry "$1" --noexpiry $AUTH >/dev/null
}
member() { samba-tool group addmembers "$1" "$2" $AUTH 2>/dev/null || true; }
ou "OU=Tienda"
# Cuenta de servicio con la que Keycloak lee el directorio (solo lectura).
user svc-keycloak "Keycloak-Lectura-2026!" Keycloak Servicio "CN=Users"
# Grupos con el mismo nombre que los roles de realm de Keycloak.
group cliente
group admin
user sofia "Sofia-Tienda-2026!" Sofia Ventas "OU=Tienda"
user diego "Diego-Tienda-2026!" Diego Soporte "OU=Tienda"
member cliente sofia,diego
member admin diego
echo "AD de prueba listo: sofia (cliente), diego (cliente, admin)"
| Cuenta | Contraseña | Grupos de AD | Para qué |
|---|---|---|---|
sofia | Sofia-Tienda-2026! | cliente | Una clienta, como ana. |
diego | Diego-Tienda-2026! | cliente, admin | Un administrador, como carlos. |
svc-keycloak | Keycloak-Lectura-2026! | (ninguno) | La cuenta con la que Keycloak lee el directorio. |
Los usuarios locales ana y carlos siguen existiendo: un realm puede mezclar usuarios propios y de uno o varios directorios.
Un AD rechaza por defecto los bind con contraseña en claro por el puerto 389 (en Samba: «Strong(er) authentication required»). ad/0globals.conf lo permite para que el primer contacto sea simple. En la sección 7 pasas a LDAPS, que es lo que usarás contra un AD real.
3. El proveedor LDAP en Keycloak
El realm del paso trae el proveedor ya creado (en tools/realm/tienda-11.json se ve como un component de tipo UserStorageProvider). Míralo en la consola: User federation → active-directory. Para crearlo a mano: Add Ldap providers, Vendor = Active Directory (rellena los atributos típicos de AD) y los datos de la tabla.
| Opción | Valor | Qué significa |
|---|---|---|
| Connection URL | ldap://ad:389 | El servicio ad de Compose. En producción, ldaps://…:636. |
| Bind DN / Bind credentials | CN=svc-keycloak,CN=Users,DC=tienda,DC=local | La cuenta de servicio. Solo necesita leer el directorio. |
| Edit mode | READ_ONLY | Keycloak nunca escribe en el AD: contraseñas, datos y grupos se cambian allí. |
| Users DN | OU=Tienda,DC=tienda,DC=local | Dónde buscar. Solo esta unidad: svc-keycloak y el Administrator, que están en CN=Users, no pueden entrar en la tienda. |
| Username LDAP attribute | sAMAccountName | El «usuario» de Windows (TIENDA\sofia → sofia). |
| UUID LDAP attribute | objectGUID | El identificador inmutable del AD. Keycloak lo guarda en el atributo LDAP_ID del usuario. |
| Import users | On | Keycloak guarda una copia local del usuario (con su propio ID) la primera vez que lo ve. |
| Sync registrations | Off | Los usuarios que se registren en Keycloak no se crean en el AD. |
| Cache policy | NO_CACHE | Consulta el AD en cada login: los cambios de grupos se notan al momento (sección 5). |
| Trust email | On | El correo del AD se da por verificado (email_verified: true). |
Test connection y Test authentication (bajo la contraseña) son lo primero que debes probar con un AD nuevo: el primero comprueba red y certificado; el segundo, la cuenta de servicio.
Los mappers: de atributos de AD a datos de Keycloak
La pestaña Mappers dice qué atributo del AD alimenta cada dato de Keycloak. Al elegir el vendor Active Directory, Keycloak crea casi todos; el realm cambia dos cosas: sustituye «full name» (que esperaría el nombre completo en cn) por first name desde givenName, y añade el mapper de roles.
| Mapper | Atributo de AD → Keycloak |
|---|---|
| username | sAMAccountName → usuario |
| first name · last name · email | givenName, sn, mail → nombre, apellido, correo (y de ahí los claims name y email) |
| MSAD account controls | userAccountControl → si la cuenta está deshabilitada en AD, Keycloak no la deja entrar |
| roles desde grupos de AD | grupos cliente y admin → roles de realm con el mismo nombre |
| creation date · modify date · Kerberos principal | Metadatos; el de Kerberos solo importa si activas SPNEGO |
El mapper de roles es el que conecta el AD con la autorización de la lección 6: api-pedidos exige el rol de realm admin en /admin/pedidos, y ahora diego lo obtiene por estar en el grupo admin del AD.
| Opción | Valor | Por qué |
|---|---|---|
| LDAP Roles DN | OU=Tienda,DC=tienda,DC=local | Dónde están los grupos. |
| LDAP Filter | (|(cn=cliente)(cn=admin)) | Un AD real tiene cientos de grupos («Todos-Madrid», «VPN»…). Sin filtro, cada uno se convertiría en un rol de realm. |
| User Roles Retrieve Strategy | GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE | Lee los grupos del atributo memberOf del usuario: una sola consulta, lo habitual en AD. |
| Use Realm Roles Mapping | On | Los grupos se convierten en roles de realm (no de un client). |
| Mode | LDAP_ONLY | Los roles de un usuario de AD son solo los de sus grupos. Nadie puede añadirle uno desde Keycloak. |
Probado con el modo READ_ONLY del mapper: Keycloak no escribe en el AD, pero sí permite asignar roles locales a un usuario de AD. Un administrador de Keycloak pudo dar admin a sofia aunque no está en ese grupo del AD. Si la política es «los permisos se gestionan en el AD», usa LDAP_ONLY: entonces Keycloak responde 400 Could not add user role mappings!.
4. Probarlo: usuarios de AD en la tienda
Arranca los tres servicios del paso como siempre (go run ./cmd/api, ./cmd/facturacion y ./cmd/web, cada uno en su terminal) y entra en http://localhost:3000 como sofia / Sofia-Tienda-2026!. Compra algo: funciona igual que con ana. Su access token, decodificado (salida real):
{
"sub": "33e38e06-2ad2-4f5a-864c-ea12177327ea",
"preferred_username": "sofia",
"name": "Sofia Ventas",
"email": "sofia@tienda.local",
"email_verified": true,
"realm_access": { "roles": ["cliente"] },
"aud": ["api-pedidos", "facturacion"]
}
Nada en el token dice «Active Directory»: es exactamente la forma que espera api-pedidos. Sal y entra como diego / Diego-Tienda-2026!: ve el enlace Admin y la lista de todos los pedidos, porque su grupo de AD le da el rol admin. Sofia, en cambio, recibe el 403 de la lección 6 en /admin.
admin del AD, en /admin. El pedido #1004 es de sofia: su «cliente» es el sub que le dio Keycloak.En la consola, Users aparece vacío con un aviso: con un directorio federado, Keycloak no lista todo (podrían ser miles de cuentas). Busca * para verlos todos, o un nombre:
sub lo pone Keycloak, no el AD
El sub de sofia es el ID de su copia local en Keycloak, creado cuando se importó. El identificador del AD (objectGUID) queda en el atributo LDAP_ID. Tus servicios guardan el sub (los pedidos lo usan como dueño), y eso tiene una consecuencia que verás en la sección 5.
5. Quién manda en cada dato
Con READ_ONLY y LDAP_ONLY, el AD es la fuente de verdad de los usuarios que vienen de él. Todo lo de esta tabla está probado con el paso 11:
| Qué | Dónde se hace | Qué pasa en Keycloak |
|---|---|---|
| Dar de alta | AD | Se importa la primera vez que entra, que alguien lo busca o con una sincronización. |
| Cambiar de grupo | AD | Con NO_CACHE, el siguiente token (login o renovación) ya trae los roles nuevos. Con la caché por defecto, no: siguió con admin hasta vaciar la caché (Action → Clear user cache). |
| Cambiar la contraseña | AD | Vale la nueva. La antigua siguió funcionando un rato: es un periodo de gracia del propio AD, no de Keycloak. «Reset password» en Keycloak responde 400 Can't reset password as account is read only. |
| Deshabilitar | AD | Login: «Invalid username or password». Refresh de una sesión abierta: invalid_grant «User disabled». El access token ya emitido sigue valiendo hasta que caduca (5 min): la API respondió 200. |
| Borrar | AD | La copia local desaparece la siguiente vez que Keycloak busca a ese usuario. |
| AD caído | — | Los usuarios de AD no pueden entrar (StorageUnavailableException en el log). Los locales (ana, carlos) sí. |
Probado: borrar a sofia en la consola no toca el AD (es de solo lectura). La siguiente vez que entra, Keycloak la importa de nuevo con otro ID, y su sub pasa de 33e38e06-… a 8b2bd3fa-…. Para api-pedidos es otra persona: GET /pedidos devuelve {"pedidos":[]} y el #1004 queda huérfano. Si necesitas que alguien no entre, deshabilítalo en el AD.
Para cambios en AD que deben notarse ya (alguien sale de la empresa), deshabilitar no basta para las sesiones abiertas: combínalo con cerrar sus sesiones (admin expulsar, lección 9). El access token vivo aún tiene hasta 5 minutos; es el precio de que la API valide tokens sin preguntar a nadie (lección 5).
6. admin-tool ante usuarios de AD
La CLI de la lección 9 funcionaba, pero mentía: baja de un usuario de AD «funcionaba» sin efecto, y rol devolvía un 400 críptico. Tres cambios en internal/admin:
- Saber el origen. Un usuario federado trae
federationLink(el ID del proveedor). La columnaORIGENdeusuariossale de ahí. - Decir dónde se hace cada cosa. Un error
ErrFederatedque explica que ese cambio se hace en el AD. - Sincronizar. Un comando que pide a Keycloak importar y actualizar todos los usuarios del AD.
// origin dice de dónde viene un usuario: "AD" si está federado, "local" si no.
func origin(u *gocloak.User) string {
if gocloak.PString(u.FederationLink) != "" {
return "AD"
}
return "local"
}
// SetRole asigna (add=true) o quita un rol de realm a un usuario.
// En usuarios de AD, los roles que vienen de grupos de AD (mapper LDAP_ONLY)
// no se pueden cambiar aquí: Keycloak responde 400 y lo explicamos.
func (a *Admin) SetRole(ctx context.Context, username, role string, add bool) error {
u, err := a.findUser(ctx, username)
if err != nil {
return err
}
id := gocloak.PString(u.ID)
r, err := a.kc.GetRealmRole(ctx, a.token, a.realm, role)
if err != nil {
return explain(err)
}
if add {
err = a.kc.AddRealmRoleToUser(ctx, a.token, a.realm, id, []gocloak.Role{*r})
} else {
err = a.kc.DeleteRealmRoleFromUser(ctx, a.token, a.realm, id, []gocloak.Role{*r})
}
var apiErr *gocloak.APIError
if errors.As(err, &apiErr) && apiErr.Code == http.StatusBadRequest && origin(u) == "AD" {
return fmt.Errorf("%w: el rol %s sale de los grupos de AD; cambia la pertenencia al grupo en AD (%v)", ErrFederated, role, err)
}
return explain(err)
}
Keycloak responde un 400 genérico; la función lo traduce solo cuando sabe la causa (usuario de AD). Con un usuario local, el error sigue su camino normal.
// Disable deshabilita a un usuario: no podrá iniciar sesión ni renovar tokens.
// Es preferible a borrarlo: se conserva su historial (y su «sub»).
// Los usuarios de AD se deshabilitan en AD: con el proveedor en solo lectura,
// Keycloak ignoraría el cambio sin dar error, así que ni lo intentamos.
func (a *Admin) Disable(ctx context.Context, username string) error {
u, err := a.findUser(ctx, username)
if err != nil {
return err
}
if origin(u) == "AD" {
return fmt.Errorf("%w: deshabilita a %s en AD (Keycloak lo verá en su siguiente login)", ErrFederated, username)
}
return explain(a.kc.UpdateUser(ctx, a.token, a.realm, gocloak.User{ID: u.ID, Enabled: gocloak.BoolP(false)}))
}
Aquí no hay error que traducir: con el proveedor en solo lectura, Keycloak responde 204 a la actualización, pero diego sigue habilitado y entra sin problema (probado). Por eso la función se niega antes de llamar.
// Sync lanza una sincronización completa de cada directorio (LDAP/AD)
// federado en el realm: importa a Keycloak los usuarios nuevos de AD y
// actualiza los existentes. gocloak no tiene una función para esto, así que
// usamos su cliente HTTP autenticado para llamar al endpoint directamente.
func (a *Admin) Sync(ctx context.Context) ([]SyncResult, error) {
providers, err := a.kc.GetComponentsWithParams(ctx, a.token, a.realm, gocloak.GetComponentsParams{
ProviderType: gocloak.StringP("org.keycloak.storage.UserStorageProvider"),
})
if err != nil {
return nil, explain(err)
}
var out []SyncResult
for _, p := range providers {
var res SyncResult
url := fmt.Sprintf("%s/admin/realms/%s/user-storage/%s/sync", a.baseURL, a.realm, gocloak.PString(p.ID))
resp, err := a.kc.GetRequestWithBearerAuth(ctx, a.token).
SetQueryParam("action", "triggerFullSync").
SetResult(&res).
Post(url)
if err != nil {
return out, err
}
if resp.IsError() {
return out, fmt.Errorf("sincronizar %s: %s", gocloak.PString(p.Name), resp.Status())
}
res.Provider = gocloak.PString(p.Name)
out = append(out, res)
}
return out, nil
}
gocloak no cubre todos los endpoints de la Admin API. Para los que faltan, GetRequestWithBearerAuth te da su cliente HTTP (resty) ya autenticado: solo pones la URL y el tipo del resultado.
Salida real:
cd tienda/pasos/paso-11
go run ./cmd/admin usuarios
# USUARIO ORIGEN NOMBRE EMAIL ACTIVO ROLES GRUPOS
# ana local Ana Cliente ana@tienda.test true cliente
# carlos local Carlos Admin carlos@tienda.test true admin,cliente
# diego AD Diego Soporte diego@tienda.local true admin,cliente
# sofia AD Sofia Ventas sofia@tienda.local true cliente
go run ./cmd/admin rol -usuario sofia -rol admin
# el usuario viene de Active Directory: el rol admin sale de los grupos de AD; cambia la
# pertenencia al grupo en AD (400 Bad Request: invalid_request: Could not add user role mappings!)
go run ./cmd/admin baja -usuario sofia
# el usuario viene de Active Directory: deshabilita a sofia en AD (Keycloak lo verá en su siguiente login)
go run ./cmd/admin sincronizar
# active-directory: 0 nuevos, 2 actualizados, 0 eliminados, 0 fallidos
go run ./cmd/admin rol -usuario ana -rol admin # ana es local: funciona como antes
# rol admin asignado a ana
La columna ROLES muestra los roles de diego aunque no están asignados en Keycloak: el mapper los calcula desde el AD cada vez que alguien los pide.
7. LDAPS: cifrar la conexión con el AD
Por el puerto 389 sin cifrar viajan la contraseña de svc-keycloak y la de cada persona que entra. Un AD real exige LDAPS (puerto 636, TLS), y eso supone dos cosas para Keycloak: confiar en la CA que firmó el certificado del AD y conectarse con el nombre que figura en él.
# LDAPS (opcional, lección 11): Keycloak habla con el AD cifrado, en el puerto 636.
#
# 1) docker compose cp ad:/var/lib/samba/private/tls/ca.pem ad/ad-ca.pem
# 2) docker compose -f docker-compose.yml -f docker-compose.ldaps.yml up -d
# 3) En la consola: User federation → active-directory → Connection URL:
# ldaps://dc1.tienda.local:636 y vuelve a escribir Bind credentials
# (Keycloak lo exige al cambiar la URL): Keycloak-Lectura-2026!
#
# La CA cambia cada vez que se crea el dominio (docker compose down -v):
# entonces repite el paso 1.
services:
ad:
networks:
default:
aliases: [dc1.tienda.local] # el nombre que figura en el certificado del AD
keycloak:
environment:
# Confía en la CA que firmó el certificado del AD (archivo PEM).
KC_TRUSTSTORE_PATHS: /opt/keycloak/conf/truststores/ad-ca.pem
volumes:
- ./ad/ad-ca.pem:/opt/keycloak/conf/truststores/ad-ca.pem:ro
docker compose cp ad:/var/lib/samba/private/tls/ca.pem ad/ad-ca.pem
docker compose -f docker-compose.yml -f docker-compose.ldaps.yml up -d
Luego, en la consola, cambia Connection URL a ldaps://dc1.tienda.local:636, vuelve a escribir Bind credentials (Keycloak lo exige al cambiar la URL) y pulsa Test authentication y Save. diego entra igual, ahora cifrado. KC_TRUSTSTORE_PATHS añade la CA al almacén de confianza de Keycloak; la opción Use Truststore SPI = Always del proveedor (la de por defecto) hace que la conexión LDAP lo use.
Lo que pasa si falta una pieza (probado):
| Falta | Resultado |
|---|---|
El nombre dc1.tienda.local (sin el alias de red) | Test connection: UnknownHost. |
La CA (KC_TRUSTSTORE_PATHS) | Nadie de AD puede entrar; en el log, PKIX path building failed: unable to find valid certification path to requested target. |
| Volver a escribir la contraseña al cambiar la URL | 400 «Bind credentials must be re-entered when the Connection URL or Bind DN is changed.» |
Para volver al estado del paso, levanta solo el compose base y deja la URL en ldap://ad:389. La CA cambia cada vez que se crea el dominio (docker compose down -v): repite la copia si recreas el AD. Por eso ad-ca.pem está en .gitignore.
8. Con un AD de verdad
Lo que tendrás que pedir al equipo de sistemas:
- Nombre del controlador y LDAPS:
ldaps://dc01.empresa.local:636y el certificado de la CA interna (en PEM). Si hay varios controladores, Keycloak acepta varias URL separadas por espacios (no por comas) y usa la primera que responde. - Una cuenta de servicio de solo lectura, con contraseña que no caduque o con un procedimiento de rotación. Si caduca, nadie del AD podrá entrar.
- Users DN y filtro: en qué unidad están las personas que deben entrar. Para limitarlo a un grupo, el User LDAP filter admite algo como
(memberOf=CN=App-Tienda,OU=Grupos,DC=empresa,DC=local). - Los grupos que serán roles, con nombres pactados y un filtro que los liste explícitamente.
- Caché:
NO_CACHEhace una consulta al AD por login. Con muchos usuarios, una caché de minutos (MAX_LIFESPAN) es un término medio: los cambios de grupo tardan ese tiempo en notarse.
Si la empresa usa Microsoft 365, quizá las cuentas vivan en Entra ID (antes Azure AD) y no haya LDAP accesible. Entonces el camino es identity brokering: Keycloak redirige el login a Microsoft por OIDC. Para tu código Go vuelve a ser invisible.
Ejercicios
1. Un empleado nuevo · fácil
Da de alta a lucas en el AD, en el grupo cliente, y haz que compre en la tienda. ¿Hace falta tocar Keycloak?
Ver solución
docker compose exec -T ad sh -c 'samba-tool user create lucas "Lucas-Tienda-2026!" --use-username-as-cn --given-name=Lucas --surname=Compras --mail-address=lucas@tienda.local --userou="OU=Tienda" && samba-tool group addmembers cliente lucas'
El mismo comando funciona en PowerShell (las comillas simples protegen el &&). No hace falta tocar Keycloak: lucas entra con su contraseña de AD y Keycloak lo importa en ese momento con el rol cliente. go run ./cmd/admin sincronizar lo habría importado antes de su primer login.
2. Quitarle a diego el rol admin · fácil
Intenta quitárselo con admin-tool y después hazlo bien. ¿Cuándo pierde el acceso a /admin?
Ver solución
go run ./cmd/admin rol -usuario diego -rol admin -quitar
# el usuario viene de Active Directory: el rol admin sale de los grupos de AD; cambia la
# pertenencia al grupo en AD (400 Bad Request: invalid_request: Could not remove user role mappings!)
cd infra
docker compose exec -T ad samba-tool group removemembers admin diego
Con NO_CACHE, el siguiente token que Keycloak emita para diego ya no lleva admin, incluso el de una renovación (probado: ["cliente", "admin"] → ["cliente"]). En cuanto tienda-web renueve, o al volver a entrar, /admin da 403. El token que tenía antes del cambio sigue valiendo hasta que caduca. Vuelve a ponerlo con samba-tool group addmembers admin diego.
3. ¿Y si queremos dar de baja desde admin-tool? · media
Recursos humanos pide que admin baja funcione también con usuarios de AD. ¿Qué opciones hay y qué implican?
Ver solución
Con el proveedor en READ_ONLY, Keycloak no puede. Opciones:
- Edit mode
WRITABLE: Keycloak escribe los cambios en el AD (también contraseñas y, con el mapper enLDAP_ONLY, grupos). La cuenta de servicio necesita permisos de escritura en esa unidad: pasa a ser una cuenta muy valiosa. Pocos equipos de sistemas lo aceptan. - Edit mode
UNSYNCED: los cambios se quedan en la copia local de Keycloak y nunca llegan al AD. Deshabilitar en Keycloak funcionaría, pero AD y Keycloak dirían cosas distintas: confuso. - Hablar con el AD directamente (por ejemplo, con la librería
github.com/go-ldap/ldap/v3) o, mejor, con la herramienta de altas y bajas que ya use la empresa. Keycloak solo debe enterarse.
La recomendación habitual es la última: un único sitio para el ciclo de vida de las personas (el AD) y Keycloak de solo lectura. Mientras tanto, admin expulsar sí sirve para cortar sus sesiones en Keycloak.
Errores comunes
Strong(er) authentication required Test authentication falla con un AD real
El AD no acepta un bind con contraseña sin cifrar. Usa LDAPS (sección 7). Rebajar la exigencia del AD, como hace 0globals.conf en el de prueba, solo es aceptable en desarrollo.
Invalid credentials con la cuenta de servicio y la contraseña correctas
El Bind DN está mal. En AD, el CN suele ser el nombre completo («Keycloak Servicio»), no el usuario: el DN es CN=Keycloak Servicio,CN=Users,…. El seed crea las cuentas con --use-username-as-cn para que el CN sea el usuario. Pregunta el DN exacto o búscalo con samba-tool user show svc-keycloak (o Get-ADUser en Windows).
PKIX path building failed nadie de AD puede entrar
Keycloak no confía en el certificado del AD. Añade la CA con KC_TRUSTSTORE_PATHS y comprueba que Use Truststore SPI está en Always. Si el AD se recreó, su CA es otra: vuelve a copiarla.
UnknownHost o error de nombre del certificado
La URL debe usar el nombre que figura en el certificado (dc1.tienda.local), no una IP ni otro alias, y ese nombre debe resolverse desde el contenedor de Keycloak.
ad-ca.pem es una carpeta Keycloak no arranca con el override LDAPS
Si montas un archivo que no existe, Docker crea en su lugar una carpeta vacía. Bórrala, copia la CA con docker compose cp y vuelve a levantar.
400 Could not add user role mappings!
Es el mapper en LDAP_ONLY haciendo su trabajo: los roles de ese usuario vienen del AD. Cambia sus grupos en el AD.
sigue siendo admin Lo quité del grupo y conserva el rol
Caché del proveedor. Con la política por defecto, Keycloak reutiliza al usuario importado. Vacía la caché (Action → Clear user cache en el proveedor) o usa NO_CACHE/MAX_LIFESPAN. Y recuerda que un token ya emitido no cambia.
perdió sus pedidos Un usuario de AD ya no ve lo suyo
Alguien lo borró en Keycloak y se reimportó con otro sub (sección 5). Recupera la relación con el ID antiguo o, si tu sistema debe sobrevivir a eso, guarda también un identificador estable del directorio (el atributo LDAP_ID, que puedes añadir al token con un mapper de atributo de usuario).
Users vacío La consola no lista a nadie
Con un directorio federado, Keycloak no lista todo por defecto. Busca * o un nombre.
Keycloak no arranca se queda esperando a ad-seed
Keycloak espera a que ad-seed termine bien. Mira docker compose logs ad ad-seed: lo habitual es que el dominio aún se esté creando (la primera vez) o que falte cap_add: [SYS_ADMIN], que Samba necesita para sus atributos de archivo.
Keycloak puede delegar en un Active Directory quién es cada persona, su contraseña y sus grupos. Tus servicios Go siguen validando los mismos tokens: el cambio está en la configuración del realm y en las herramientas que administran usuarios, que deben respetar que el AD manda. Dos reglas prácticas: los permisos se gestionan en un solo sitio (LDAP_ONLY) y a un usuario de AD se le deshabilita, no se le borra.



