Skip to content

Creating an OIDC client (PocketID)

Cómo registrar una aplicación que soporta OIDC nativo (e.g. Grafana) en PocketID, el IdP que usa el homelab para SSO LAN (ver ADR-0007 §F5).

PocketID corre en VM 208 (stacks/pocket-id, puerto host 1411). Datos en /home/monxas/stacks/pocket-id/data/pocket-id.db (SQLite).

Issuer canónico

El issuer/hostname de PocketID es pocketid.monxas.casa (:1411). No confundir con pass.monxas.casa (:8055), que es Vaultwarden, un servicio distinto. Todas las URLs OIDC (authorize/token/userinfo/jwks/ discovery) cuelgan de pocketid.monxas.casa.

Workflow normal (via UI)

  1. Abrir https://pocketid.monxas.casa/ (PocketID UI).
  2. Login como admin.
  3. OIDC Clients → New.
  4. Rellenar:
  5. Name: Grafana
  6. Callback URLs: https://grafana.monxas.casa/login/generic_oauth
  7. Logo URL (opcional): icono del servicio
  8. Guardar. Anotar:
  9. client_id
  10. client_secret (solo se muestra una vez)
  11. Configurar el servicio destino (Grafana, Immich, etc.) con esos valores + los endpoints OIDC de PocketID:
  12. Authorization URL: https://pocketid.monxas.casa/authorize
  13. Token URL: https://pocketid.monxas.casa/api/oidc/token
  14. Userinfo URL: https://pocketid.monxas.casa/api/oidc/userinfo
  15. JWKS URL: https://pocketid.monxas.casa/.well-known/jwks.json
  16. Guardar secrets en SOPS (ver Rotating secrets):
    sops secrets/pocketid.sops.yaml
    # añadir:
    # grafana:
    #   client_id: <id>
    #   client_secret: <secret>
    

Workflow alternativo: SQL directo

Si la UI de PocketID no es accesible (e.g. recovery, bootstrap inicial), se puede INSERT directamente en la DB SQLite.

Solo en caso necesario

Tocar la DB a mano es frágil. La UI de PocketID valida cosas (FK, esquema) que INSERT directo no. Toma snapshot ZFS antes.

Bcrypt gotcha

PocketID almacena el client_secret hasheado con bcrypt (cost 10, $2a$ o $2b$). Hay un gotcha crítico al generarlo:

NO generes bcrypt vía SSH desde la Mac

Comando como este se rompe:

ssh [email protected] 'python3 -c "..."'
El shell remoto interpreta $2a$10$... como variable shell vacía y mutila el hash. Resultado: el INSERT mete un hash inválido, login silenciosamente roto, y debugging muy doloroso.

Solución: SSH primero, generar el hash en sesión interactiva en VM 208, copiar a mano, luego INSERT.

Procedimiento correcto

# 1) SSH a VM 208
ssh [email protected]

# 2) Generar el secret plano + su bcrypt EN la VM
python3 <<'EOF'
import secrets, crypt
plain = secrets.token_urlsafe(32)
hashed = crypt.crypt(plain, crypt.mksalt(crypt.METHOD_BLOWFISH, rounds=10))
print("PLAIN: " + plain)
print("HASH:  " + hashed)
EOF
# Salida ejemplo:
# PLAIN: xK9_aP3F...
# HASH:  $2b$10$abcdef...

Guarda el PLAIN en SOPS (es lo que el cliente final usa); el HASH es lo que va a PocketID.

INSERT pattern

# 3) (todavía en VM 208) INSERT en la DB
sqlite3 /home/monxas/stacks/pocket-id/data/pocket-id.db <<EOF
INSERT INTO oidc_clients (
    id,
    name,
    secret,
    callback_urls,
    is_public,
    pkce_enabled,
    created_at
) VALUES (
    '<client-id-uuid-aqui>',
    'Grafana',
    '\$2b\$10\$abcdef...',  -- el HASH bcrypt completo, con \$ escapados
    '["https://grafana.monxas.casa/login/generic_oauth"]',  -- callback_urls = array JSON
    0,
    1,
    datetime('now')
);
EOF

# 4) Restart PocketID para recargar
docker compose -f ~/stacks/pocket-id/compose.yaml restart

El client-id-uuid lo generas con python3 -c "import uuid; print(uuid.uuid4())".

Verificar

# desde VM 208
sqlite3 /home/monxas/stacks/pocket-id/data/pocket-id.db \
  'SELECT id, name, length(secret) FROM oidc_clients;'
# length(secret) debe ser 60 (bcrypt $2b$10$22charsalt53charshash). Si es <60, mal.

Test end-to-end:

curl -sS https://pocketid.monxas.casa/.well-known/openid-configuration | jq
# Pegar luego el client_id/client_secret en Grafana → probar login

Apps que NO soportan OIDC nativo

Para apps sin soporte OIDC (Pi-hole, qBittorrent, dozzle, etc.) el patrón previsto es forward_auth de Caddy contra PocketID, no un OIDC client.

forward_auth no está en uso hoy (2026-07-12)

Hoy ningún bloque forward_auth está materializado en managed.caddy. El SSO real es OIDC nativo per-app (las apps con soporte OIDC apuntan directamente a pocketid.monxas.casa), y el borde lo cubre Cloudflare Access. El snippet de abajo es el patrón de referencia para apps sin OIDC nativo, pero no refleja configuración activa.

⚠️ PocketID (v2) NO expone endpoint forward-auth. /forward-auth, /verify y /api/oidc/forward-auth devuelven el SPA (HTTP 200) o 404 — un forward_auth de Caddy hacia PocketID es un no-op fail-open (deja pasar todo). Para proteger un servicio usa OIDC a nivel de app (un cliente propio, como el resto de servicios) o Cloudflare Access en el borde. No dependas de forward_auth.

Corregido 2026-08-01: no existe ninguna label de auth

Esta guía decía que el patrón se activaba con una label tunnel.auth=pocketid. Verificado leyendo homelab-ctl.py (el script real que procesa las labels tunnel.*): no existe ningún label de autenticación, ni tunnel.auth ni equivalente — el script no tiene ningún concepto de auth. En la práctica, para una app sin soporte OIDC hoy la única opción real es Cloudflare Access en el borde (ver Cloudflare Access + PocketID), no un label ni forward_auth (que además es no-op fail-open, ver arriba).

Rotación del client secret

# 1) Generar nuevo plain+hash (en VM 208, no via SSH inline)
# 2) UPDATE en la DB:
sqlite3 /home/monxas/stacks/pocket-id/data/pocket-id.db \
  "UPDATE oidc_clients SET secret = '<nuevo_hash>'  WHERE name = 'Grafana';"
# 3) Update el plain en SOPS + push
# 4) Update Grafana config con el nuevo plain
# 5) Restart Grafana