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.
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, ohandlesi aquel está vacío) coincide exactamente con una entrada deDefaultServiceKeyAliases, el allowlist lo trata como infraestructura en todos los caminos. Reproducido con un miembroccordovaque tiene una sola key en LiteLLM, la suya:Y lo que lo hace serio:
revoke.collectKeysy el reconciler también descartan ese alias, así que esas keys sobreviven al churn indefinidamente. Es la fuga deENTITLEMENT-HARDENING.md§1, esta vez en modo autoservicio.Por qué el vector es real
discord_userlo 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:Estado actual
members).ensureEntitledsigue exigiendo suscripción viva para llegar aCreateKey, así que no es acceso gratuito: es un miembro de pago que se vuelve invisible al churn.mainel 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_aliasdeTestCreateKey_NotBlockedByAServiceKeyOnTheSameAxiscodifica 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_ALIASESreemplaza 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.