Skip to content

Un miembro cuyo discord_user colisione con un alias del allowlist puede acuñar keys invisibles e irrevocables #29

Description

@sre-helmcode

Salido de la revisión del PR nan-cloud-api#119. No bloquea ese PR (que mejora el estado respecto a main), pero es un agujero real que conviene cerrar.

El problema

Si el alias resuelto de un miembro (discord_user, o handle si aquel está vacío) coincide exactamente con una entrada de DefaultServiceKeyAliases, el allowlist lo trata como infraestructura en todos los caminos. Reproducido con un miembro ccordova que tiene una sola key en LiteLLM, la suya:

GET    -> 200 {"exists":false}          <- tiene key, pero el portal dice que no
CREATE -> 201 generated=1               <- acuña otra, y otra en cada pulsación
DELETE -> 404 "no API key to delete"    <- no puede limpiarlas (rotate igual)

Y lo que lo hace serio: revoke.collectKeys y el reconciler también descartan ese alias, así que esas keys sobreviven al churn indefinidamente. Es la fuga de ENTITLEMENT-HARDENING.md §1, esta vez en modo autoservicio.

Por qué el vector es real

discord_user lo elige el propio miembro: es su nombre de usuario de Discord. Y la lista contiene nombres humanos plausibles además de otros triviales de adivinar:

gatus-synthetic-monitor, gatus-synthetic-monitor-premium, test-key,
devops-bench, nan-discord-bot-opensource, ci-api-tests,
api-suite-smoke-test, classifier-teacher, ccordova, cgutierrez-test

Estado actual

  • Ningún miembro tiene hoy ninguno de esos 10 alias (verificado contra la tabla members).
  • ensureEntitled sigue exigiendo suscripción viva para llegar a CreateKey, así que no es acceso gratuito: es un miembro de pago que se vuelve invisible al churn.
  • En main el mismo miembro conseguía algo peor: su rotate borraba la service key de verdad.

Arreglo propuesto

Fallar cerrado en resolveUser (internal/handlers/keys.go): si el alias resuelto está en el allowlist, devolver un error explícito en vez de acuñar una key invisible e irrevocable. Algo como "tu usuario de Discord colisiona con una clave de servicio, cámbialo".

Alternativa complementaria: reservar un prefijo para las service keys (svc-…) y validar que ningún alias de miembro pueda empezar por él, que elimina la colisión por construcción en vez de por lista.

Ojo al tocar esto

El subtest key_alias de TestCreateKey_NotBlockedByAServiceKeyOnTheSameAxis codifica el comportamiento actual como deseado: usa un miembro cuyo alias ES el del allowlist y exige que se acuñe la key. Si se arregla lo anterior, ese subtest hay que replantearlo — el eje alias se cubre mejor con un miembro de alias normal cuya service key comparte el eje, no con un miembro cuyo alias es el de la lista.

Nota relacionada

LITELLM_SERVICE_KEY_ALIASES reemplaza la lista por defecto, no la extiende (reconciler.go:44-46). Hoy no está puesta en el Deployment de prod, así que aplican los 10 del código. Si alguien la define en el futuro con una lista parcial, ahora afecta a cuatro handlers además del reconciler.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions