Keycloak en Docker y el realm tienda
Levantas Keycloak 26.8 con PostgreSQL, recorres la consola para entender cómo se organiza y escribes tu primer programa Go que habla con Keycloak.
En este capítulo
- Trabajas en
- Keycloak (el realm
tienda) y un primer programa Go,discover. - Carpeta
tienda/pasos/paso-02- Archivos
- Todos nuevos:
infra/docker-compose.yml,infra/realm/tienda-realm.json,cmd/discover/main.go. - En marcha
- Docker. Al final de la lección, Keycloak del paso 02.
- Comprueba
go -C tools/comprobar run . -paso 2(desdecourse/); no descarga nada, solo usa la biblioteca estándar de Go.
Al terminar sabrás
- Arrancar Keycloak en modo desarrollo con Docker Compose y PostgreSQL.
- Qué son un realm, un client, un rol y un usuario, y cómo se relacionan.
- Importar un realm desde JSON al arrancar y exportarlo después.
- Leer el documento de descubrimiento y el JWKS desde Go.
Cómo se organiza Keycloak
| Concepto | Qué es | En la tienda |
|---|---|---|
| Realm | Un espacio aislado (un tenant) con sus propios usuarios, clients, roles y claves de firma. Es el issuer de los tokens. | tienda |
| Client | Una aplicación o servicio registrado que puede pedir tokens o recibirlos. | tienda-web; luego api-pedidos, facturacion… |
| Usuario | Una persona (o la service account de un client) que se autentica. | ana, carlos |
| Rol de realm | Permiso válido en todo el realm. Aparece en realm_access.roles. | cliente, admin |
| Rol de client | Permiso que solo tiene sentido para un client. Aparece en resource_access.<client>.roles. | Lección 6 |
| Grupo | Conjunto de usuarios que heredan roles. | Lección 9 |
| Client scope | Paquete reutilizable de claims (mappers) y roles que se añade a los tokens. | Lecciones 5–6 |
master contiene a los administradores de Keycloak. Mezclar ahí a tus clientes significa que un fallo de configuración podría dar a un cliente poderes sobre el propio servidor. Crea siempre un realm por producto o entorno.
1. Levantar Keycloak
Todo está en tienda/pasos/paso-02/infra/:
# Keycloak + PostgreSQL para el curso "Go + Keycloak".
# Solo para desarrollo: usa start-dev (HTTP, cachés locales, sin hostname fijo).
#
# docker compose up -d # arrancar
# docker compose logs -f keycloak
# docker compose down -v # parar y BORRAR datos (fuerza reimportar el realm)
services:
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
volumes:
pgdata:
| Pieza | Por qué |
|---|---|
start-dev | Modo desarrollo: HTTP sin TLS, hostname deducido de cada petición, temas sin caché. Nunca en producción (allí: start). |
--import-realm | Al arrancar importa los .json de /opt/keycloak/data/import. Si el realm ya existe, no lo toca. |
KC_BOOTSTRAP_ADMIN_* | Crea el administrador temporal del realm master solo la primera vez (base de datos vacía). Sustituyen a las antiguas KEYCLOAK_ADMIN*, obsoletas desde la versión 26. |
KC_DB* | Usar PostgreSQL en vez de la base H2 embebida: los datos sobreviven a reinicios y el comportamiento se parece al de producción. |
KC_HEALTH_ENABLED | Expone /health/ready en el puerto de gestión 9000. |
extra_hosts | Define host.docker.internal (tu máquina vista desde el contenedor) también en Linux; Docker Desktop ya lo trae. Solo se usa en la lección 4. |
127.0.0.1:8080 | Publica el puerto solo en tu máquina, no en la red local. |
Arranca y espera a que esté listo:
cd tienda/pasos/paso-02/infra
docker compose up -d
docker compose logs -f keycloak # espera a ver "Listening on: http://0.0.0.0:8080" y sal con Ctrl+C
curl -s http://localhost:9000/health/ready
# {"status": "UP", ...}
La primera vez tarda: descarga las imágenes, crea las tablas e importa el realm. En los logs verás una línea que dice que el realm tienda se ha importado.
2. El realm como código
En lugar de configurar todo con clics, el realm vive en un archivo versionable. Este es el estado inicial:
{
"realm": "tienda",
"displayName": "Tienda Go",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"accessTokenLifespan": 300,
"ssoSessionIdleTimeout": 1800,
"roles": {
"realm": [
{
"name": "cliente",
"description": "Puede ver el catálogo y gestionar sus propios pedidos"
},
{
"name": "admin",
"description": "Puede ver y gestionar todos los pedidos"
},
{
"name": "offline_access",
"description": "${role_offline-access}"
},
{
"name": "uma_authorization",
"description": "${role_uma_authorization}"
},
{
"name": "default-roles-tienda",
"description": "${role_default-roles}",
"composite": true,
"composites": {
"realm": ["offline_access", "uma_authorization"],
"client": {
"account": ["view-profile", "manage-account"]
}
}
}
]
},
"defaultRole": {
"name": "default-roles-tienda",
"description": "${role_default-roles}",
"composite": true
},
"users": [
{
"username": "ana",
"enabled": true,
"email": "ana@tienda.test",
"emailVerified": true,
"firstName": "Ana",
"lastName": "Cliente",
"credentials": [
{ "type": "password", "value": "ana123", "temporary": false }
],
"realmRoles": ["default-roles-tienda", "cliente"]
},
{
"username": "carlos",
"enabled": true,
"email": "carlos@tienda.test",
"emailVerified": true,
"firstName": "Carlos",
"lastName": "Admin",
"credentials": [
{ "type": "password", "value": "carlos123", "temporary": false }
],
"realmRoles": ["default-roles-tienda", "cliente", "admin"]
}
],
"clients": [
{
"clientId": "tienda-web",
"name": "Tienda Web",
"description": "Aplicación web en Go (login con Authorization Code + PKCE)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"clientAuthenticatorType": "client-secret",
"secret": "tienda-web-secret",
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"rootUrl": "http://localhost:3000",
"baseUrl": "/",
"redirectUris": ["http://localhost:3000/callback"],
"webOrigins": ["http://localhost:3000"],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "http://localhost:3000/"
}
}
]
}
Lo esencial:
- Roles
clienteyadmin. Los otros tres (default-roles-tienda,offline_access,uma_authorization) son los que Keycloak crea en todo realm; los declaramos para poder asignar el rol por defecto a los usuarios importados, que si no, no podrían usar su consola de cuenta. - Usuarios con contraseña no temporal y sus roles.
- Client
tienda-web: confidencial (publicClient: false) con secreto fijo, solo Authorization Code (standardFlowEnabled), PKCE obligatorio conS256, y las URLs exactas de vuelta tras el login y el logout. accessTokenLifespan: 300(5 min) yssoSessionIdleTimeout: 1800(30 min) son los valores por defecto; están explícitos para que sepas dónde cambiarlos.
3. Recorrido por la consola
Abre http://localhost:8080/admin y entra con admin / admin.
master.Verás un aviso amarillo: «You are logged in as a temporary admin user». Es el administrador que crearon las variables KC_BOOTSTRAP_ADMIN_*; en producción crearías un administrador permanente y borrarías este. Para el curso, ignóralo.
Estás en el realm master. Cambia a Tienda Go (el display name del realm tienda) desde Manage realms en el menú lateral, o desde el selector de la parte superior del menú.
El client tienda-web
En Clients aparecen el client que importamos y los que Keycloak crea en todo realm (account, account-console, admin-cli, broker, realm-management, security-admin-console):
tienda-web es el nuestro.Entra en tienda-web. La pestaña Settings está dividida en secciones; las dos importantes son Capability config (qué puede hacer el client) y Access settings (sus URLs):
S256.
La pestaña Credentials guarda el secreto del client. En un proyecto real lo regenerarías (botón Regenerate) y se lo pasarías a la app por una variable de entorno, nunca en el código:
Del JSON a la pantalla
| JSON | Consola (Clients → tienda-web → Settings) |
|---|---|
"publicClient": false | Capability config → Client authentication: On |
"standardFlowEnabled": true | Capability config → Authentication flow → Standard flow |
"directAccessGrantsEnabled": false | Capability config → Direct access grants desmarcado (el flujo Password) |
"pkce.code.challenge.method": "S256" | Capability config → Require PKCE: On y PKCE Method: S256 |
"rootUrl", "baseUrl" | Access settings → Root URL, Home URL |
"redirectUris" | Access settings → Valid redirect URIs |
"post.logout.redirect.uris" | Access settings → Valid post logout redirect URIs |
"webOrigins" | Access settings → Web origins (CORS) |
"secret" | Pestaña Credentials → Client Secret |
Roles y usuarios
Realm roles lista los roles del realm: los nuestros (cliente, admin) y los que crea Keycloak.
En Users → carlos → Role mapping ves qué roles tiene asignados directamente. Si desmarcas Hide inherited roles, aparecen también los que hereda de default-roles-tienda:
cliente, admin y default-roles-tienda.Tokens y claves del realm
Realm settings → Tokens controla cuánto viven los tokens. Fíjate en Access Token Lifespan (5 minutos) y en la recomendación de Keycloak: que sea menor que la inactividad de la sesión SSO (30 minutos).
Realm settings → Keys muestra las claves del realm. La clave RS256 firma los tokens; su Kid es el que verás en el header de cada JWT y en el JWKS.
4. Tu primer login (sin escribir código)
Cada realm trae un client account-console: la página donde un usuario gestiona su cuenta. Es un client público que usa exactamente el flujo de la lección 1, así que sirve para verlo en directo.
- Abre una ventana privada y las DevTools (F12) en la pestaña Network con «Preserve log» activado.
- Visita http://localhost:8080/realms/tienda/account.
- En la petición a
…/protocol/openid-connect/authbusca los parámetrosclient_id,redirect_uri,state,nonce,code_challengeycode_challenge_method=S256. - Entra como
ana/ana123. Después del login, busca la redirección con?code=…&state=…(en este client va en el fragmento#de la URL) y la peticiónPOST …/tokenconcode_verifier.
Los pasos 3 a 10 del diagrama de la lección 1. Como account-console es una SPA (client público), el canje del código lo hace el propio navegador y no hay client_secret. En tienda-web lo hará tu servidor Go.
5. Primer programa Go: descubrimiento
Las librerías OIDC solo necesitan la URL del issuer: el resto lo leen del documento de descubrimiento. Vamos a hacer a mano lo que ellas harán por ti, para que no sea magia.
// Command discover consulta el documento de descubrimiento OIDC de un realm
// de Keycloak y muestra los endpoints y las claves públicas (JWKS).
//
// Uso:
//
// go run ./cmd/discover
// go run ./cmd/discover -issuer http://localhost:8080/realms/tienda
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// discovery contiene los campos del documento
// /.well-known/openid-configuration que nos interesan en el curso.
type discovery struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
EndSessionEndpoint string `json:"end_session_endpoint"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
JWKSURI string `json:"jwks_uri"`
GrantTypesSupported []string `json:"grant_types_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
IDTokenSigningAlgs []string `json:"id_token_signing_alg_values_supported"`
}
// jwks es el conjunto de claves públicas con las que Keycloak firma los tokens.
type jwks struct {
Keys []struct {
Kid string `json:"kid"`
Kty string `json:"kty"`
Alg string `json:"alg"`
Use string `json:"use"`
} `json:"keys"`
}
func main() {
issuer := flag.String("issuer", "http://localhost:8080/realms/tienda", "URL del realm (issuer)")
flag.Parse()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := &http.Client{Timeout: 5 * time.Second}
var doc discovery
wellKnown := strings.TrimSuffix(*issuer, "/") + "/.well-known/openid-configuration"
if err := getJSON(ctx, client, wellKnown, &doc); err != nil {
log.Fatalf("descubrimiento: %v", err)
}
// OIDC exige que el issuer del documento sea idéntico al que usamos para pedirlo.
// Si no coincide (por ejemplo, localhost frente a 127.0.0.1), las librerías
// de validación rechazarán los tokens más adelante.
if doc.Issuer != *issuer {
fmt.Fprintf(os.Stderr, "AVISO: el issuer del documento (%s) no coincide con %s\n", doc.Issuer, *issuer)
}
fmt.Println("== Endpoints del realm ==")
fmt.Printf("issuer: %s\n", doc.Issuer)
fmt.Printf("authorization: %s\n", doc.AuthorizationEndpoint)
fmt.Printf("token: %s\n", doc.TokenEndpoint)
fmt.Printf("userinfo: %s\n", doc.UserinfoEndpoint)
fmt.Printf("end_session: %s\n", doc.EndSessionEndpoint)
fmt.Printf("introspection: %s\n", doc.IntrospectionEndpoint)
fmt.Printf("jwks_uri: %s\n", doc.JWKSURI)
fmt.Printf("grant types: %s\n", strings.Join(doc.GrantTypesSupported, ", "))
fmt.Printf("PKCE: %s\n", strings.Join(doc.CodeChallengeMethodsSupported, ", "))
fmt.Printf("firmas ID tok: %s\n", strings.Join(doc.IDTokenSigningAlgs, ", "))
var keys jwks
if err := getJSON(ctx, client, doc.JWKSURI, &keys); err != nil {
log.Fatalf("jwks: %v", err)
}
fmt.Println("\n== Claves públicas (JWKS) ==")
for _, k := range keys.Keys {
fmt.Printf("kid=%s kty=%s alg=%s use=%s\n", k.Kid, k.Kty, k.Alg, k.Use)
}
}
// getJSON hace un GET a url y decodifica la respuesta JSON en v.
func getJSON(ctx context.Context, client *http.Client, url string, v any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GET %s: estado %s", url, resp.Status)
}
return json.NewDecoder(resp.Body).Decode(v)
}
Ejecútalo desde tienda/pasos/paso-02:
cd tienda/pasos/paso-02
go run ./cmd/discover
Esta es la salida real contra Keycloak 26.8.0 (tus kid serán otros, porque cada realm genera sus propias claves):
== Endpoints del realm ==
issuer: http://localhost:8080/realms/tienda
authorization: http://localhost:8080/realms/tienda/protocol/openid-connect/auth
token: http://localhost:8080/realms/tienda/protocol/openid-connect/token
userinfo: http://localhost:8080/realms/tienda/protocol/openid-connect/userinfo
end_session: http://localhost:8080/realms/tienda/protocol/openid-connect/logout
introspection: http://localhost:8080/realms/tienda/protocol/openid-connect/token/introspect
jwks_uri: http://localhost:8080/realms/tienda/protocol/openid-connect/certs
grant types: authorization_code, client_credentials, implicit, password, refresh_token, urn:ietf:params:oauth:grant-type:device_code, urn:ietf:params:oauth:grant-type:jwt-bearer, urn:ietf:params:oauth:grant-type:token-exchange, urn:ietf:params:oauth:grant-type:uma-ticket, urn:openid:params:grant-type:ciba
PKCE: plain, S256
firmas ID tok: PS384, RS384, EdDSA, ES384, HS256, HS512, ES256, RS256, HS384, ES512, PS256, PS512, RS512
== Claves públicas (JWKS) ==
kid=hZj8DLzfzhlzMB6w7asAJRwH6lLgW-aIOKUPeeuFo1s kty=RSA alg=RS256 use=sig
kid=36oKxvEgvZX2ON4uhApY6x3dulQpQgZJUa6Tim6H9CE kty=RSA alg=RSA-OAEP use=enc
Observa dos cosas: el servidor anuncia flujos que tu client tiene desactivados (implicit, password); lo que manda es la configuración de cada client. Y hay una clave use=enc para cifrar, que no se usa para verificar firmas: un validador debe elegir la clave por kid, no «la primera».
6. Exportar e importar el realm
Cuando cambies algo en la consola, querrás llevarlo al JSON. Hay dos maneras:
| Exportación parcial (consola) | Exportación completa (CLI) | |
|---|---|---|
| Dónde | Realm settings → menú Action → Partial export | Comando kc.sh export |
| Usuarios | No | Sí |
| Secretos | Enmascarados con * | Incluidos |
| Servidor | En marcha | Parado |
| Uso | Copiar un client o un rol concreto al JSON | Copias de seguridad, mover realms entre entornos |
Exportación completa con Docker Compose (desde infra/):
mkdir -p export && chmod 777 export # Linux y WSL: el contenedor escribe como el usuario 1000
docker compose stop keycloak
docker compose run --rm -v "$PWD/export:/tmp/export" keycloak \
export --dir /tmp/export --realm tienda --users realm_file
docker compose start keycloak
ls export/ # tienda-realm.json, con usuarios incluidos
En Windows: PowerShell o Git Bash (probado)
mkdir export # sin chmod: Docker Desktop gestiona los permisos
docker compose stop keycloak
docker compose run --rm -v "${PWD}\export:/tmp/export" keycloak `
export --dir /tmp/export --realm tienda --users realm_file
docker compose start keycloak
# En Git Bash, los comandos de bash valen, pero sin MSYS_NO_PATHCONV=1 Git Bash
# convierte /tmp/export en una ruta de Windows y la exportación falla:
# MSYS_NO_PATHCONV=1 docker compose run --rm -v "$PWD/export:/tmp/export" keycloak \
# export --dir /tmp/export --realm tienda --users realm_file
docker compose run crea un contenedor temporal con la misma configuración del servicio (base de datos incluida) pero sustituye el comando: en vez de start-dev ejecuta export. Con --users realm_file los usuarios van dentro del mismo archivo.
Para reimportar desde cero (por ejemplo, después de editar el JSON):
docker compose down -v # -v borra el volumen de PostgreSQL: se pierde todo
docker compose up -d # base de datos vacía → importa el realm de nuevo
Para este realm tan pequeño ocupa unas 2.200 líneas, porque incluye todos los valores por defecto (flujos de autenticación, client scopes, mappers…). Para el curso mantenemos un JSON mínimo escrito a mano e importamos solo lo que cambia. Úsala para copias de seguridad o para buscar el nombre exacto de un campo.
7. Comprobar que todo está en orden
Cada lección termina con el mismo gesto: ejecutar tools/comprobar, un programa Go del curso que revisa que Keycloak (y, desde la lección 3, tus programas) están como los deja esa lección. Hace un login real como ana, mira los tokens y llama a los servicios. Ejecútalo desde la carpeta course/:
go -C tools/comprobar run . -paso 2
# ✔ Keycloak responde y el realm tienda existe
# ✔ ana entra en tienda-web (Authorization Code + PKCE)
# ✔ ana tiene el rol de realm cliente; carlos, admin
#
# Todo en orden para el paso 2.
go -C carpeta ejecuta Go como si estuvieras en esa carpeta: así funciona igual en Windows, macOS y Linux. Si algo falla, el mensaje dice qué (por ejemplo, «login de ana rechazado») y conviene arreglarlo antes de seguir. Desde la lección 3 añade -servicios para que compruebe también los programas que tengas en marcha.
Ejercicios
1. Constrúyelo tú: el realm practica · fácil
Sin tocar tienda, crea desde la consola un realm practica que reproduzca lo esencial: rol cliente, usuario pepe con ese rol y contraseña no temporal, y un client confidencial practica-web con Standard flow, PKCE S256 y redirect http://localhost:3000/callback. Después entra como pepe en http://localhost:8080/realms/practica/account.
Ver solución
- Manage realms → Create realm: nombre
practica→ Create. - Realm roles → Create role:
cliente→ Save. - Users → Create new user:
pepe→ Create. En Credentials → Set password, desactiva Temporary. En Role mapping → Assign role, filtra por roles de realm y eligecliente. - Clients → Create client: tipo OpenID Connect, Client ID
practica-web→ Next. Activa Client authentication, deja solo Standard flow, activa Require PKCE y eligeS256→ Next. En Valid redirect URIs ponhttp://localhost:3000/callback→ Save.
Al crearlo a mano, Keycloak le asigna a pepe default-roles-practica automáticamente; por eso en el JSON de tienda tuvimos que declararlo nosotros para los usuarios importados.
2. Añade una clienta por JSON · fácil
Añade al JSON a lucia (contraseña lucia123, rol cliente), reimporta el realm y comprueba que puede entrar en su consola de cuenta.
Ver solución
Añade este objeto al array "users":
{
"username": "lucia",
"enabled": true,
"email": "lucia@tienda.test",
"emailVerified": true,
"firstName": "Lucía",
"lastName": "Cliente",
"credentials": [
{ "type": "password", "value": "lucia123", "temporary": false }
],
"realmRoles": ["default-roles-tienda", "cliente"]
}
Y reimporta con docker compose down -v && docker compose up -d. Un simple restart no sirve: el realm ya existe y la importación se omite.
3. Más campos del descubrimiento · fácil
Amplía discover para mostrar también scopes_supported y token_endpoint_auth_methods_supported. ¿Qué métodos de autenticación de clients ofrece Keycloak?
Ver solución
Añade dos campos al struct y dos líneas de salida:
type discovery struct {
// ... campos existentes ...
ScopesSupported []string `json:"scopes_supported"`
TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"`
}
// en main(), tras las otras líneas:
fmt.Printf("scopes: %s\n", strings.Join(doc.ScopesSupported, ", "))
fmt.Printf("auth clients: %s\n", strings.Join(doc.TokenEndpointAuthMethods, ", "))
Verás, entre otros, client_secret_basic y client_secret_post (secreto compartido, lo que usará tienda-web), client_secret_jwt, private_key_jwt (el client firma un JWT con su clave privada; más seguro) y tls_client_auth (mTLS).
4. localhost frente a 127.0.0.1 · media
Ejecuta go run ./cmd/discover -issuer http://127.0.0.1:8080/realms/tienda. ¿Qué issuer devuelve Keycloak? Si tienda-web obtiene tokens usando localhost y api-pedidos se configura con 127.0.0.1, ¿qué pasará?
Ver solución
En start-dev sin --hostname, Keycloak construye el issuer a partir del host de cada petición, así que devuelve http://127.0.0.1:8080/realms/tienda y el programa no avisa. Pero los tokens emitidos a través de localhost llevan iss: http://localhost:8080/realms/tienda, y la API, configurada con 127.0.0.1, los rechazará con un error de issuer inválido aunque la firma sea correcta. Regla: usa en todas partes exactamente la misma URL de issuer; en producción fíjala con --hostname.
Errores comunes
docker: command not found / permission denied … docker.sock
Según tu sistema: Docker Desktop no está arrancado (Windows, macOS); en WSL falta activar Settings → Resources → WSL integration para tu distro; en Linux o WSL, tu usuario no está en el grupo docker o la terminal es anterior a añadirlo (sudo usermod -aG docker $USER y abre una sesión nueva). Detalles en Preparar tu equipo.
address already in use El puerto 8080 está ocupado
Otro proceso usa el 8080. Arreglo: cambia el mapeo a "127.0.0.1:8180:8080" y usa http://localhost:8180/realms/tienda como issuer en todo el curso (también en las URLs de redirección del client si cambia el puerto de la app).
import skipped Mis cambios en el JSON no aparecen
--import-realm no sobrescribe un realm que ya existe. Arreglo: docker compose down -v y up -d para empezar con la base de datos vacía, o aplica el cambio en la consola.
Invalid username or password No puedo entrar como admin
Dos causas típicas: estás en la página de login de tienda en vez de en la de /admin (el admin vive en master), o cambiaste KC_BOOTSTRAP_ADMIN_* después del primer arranque; esas variables solo actúan con la base de datos vacía. Arreglo: usa /admin, o down -v para regenerar.
HTTPS required Keycloak exige HTTPS
Con sslRequired: external, Keycloak acepta HTTP solo desde direcciones locales o privadas. Si entras por una IP pública o algunos proxies, lo rechaza. Arreglo: usa http://localhost:8080. No pongas sslRequired: none salvo para una prueba puntual.
connection refused discover falla al arrancar
Keycloak tarda en arrancar (sobre todo la primera vez). Arreglo: espera a que curl -s localhost:9000/health/ready devuelva UP.
AccessDeniedException La exportación no puede escribir
El contenedor corre con el usuario 1000 y la carpeta montada no le pertenece. Pasa en Linux y WSL. Arreglo: crea la carpeta antes y dale permisos (mkdir -p export && chmod 777 export).
404 /realms/tienda/… no existe
El realm no se importó: el JSON tiene un error o el volumen no está montado. Arreglo: busca en docker compose logs keycloak el mensaje de la importación y comprueba que lanzas docker compose desde la carpeta infra/ (el montaje ./realm es relativo).
Tienes Keycloak 26.8 en marcha con el realm tienda versionado en JSON, sabes moverte por la consola y has visto un login OIDC real en las DevTools. En el módulo 2 escribirás tienda-web: el mismo flujo, pero con tu propio servidor Go como client confidencial.

