c1eaead5d2
First of the six agents to need an identity. It exchanges the end user's
token for one narrowly-scoped token per MCP server, so `azp` names the
agent while `sub` stays the human — which is what makes the audit trail
able to answer "which agent asked, on whose behalf".
Deliberately narrower than the librechat client it is modelled on:
* no browser flow at all. standardFlowEnabled, implicitFlow and
directAccessGrants are off, because the agent never logs a user in —
it receives an already-issued token over A2A. Any of those three
would be a way to obtain a token it has no business obtaining.
* no service account. It has no standing access of its own; everything
it can reach, it reaches on behalf of a user who asked.
* two optional scopes, mcp-catalog-audience and mcp-sales-audience, and
nothing else. librechat carries all of them because it fronts every
domain; an agent serving the product manager must not be able to mint
a payments token.
Adds classic-models-agent-catalogue-audience, the scope whoever issues
the user's token requests so the agent appears in that token's audience.
Keycloak's Standard Token Exchange refuses otherwise, with "Client is not
within the token audience" — which reads as a permissions problem on the
target rather than a missing audience on the source.
Verified against the running realm: catalog and sales exchange
successfully and return aud=[mcp-catalog] / aud=[mcp-sales] with
azp=classic-models-agent-catalogue, while payments is refused with
invalid_scope. The confinement is enforced by Keycloak, not by the
agent's own restraint.
Note this file is a bootstrap seed, not the live realm: Keycloak starts
with --import-realm under IGNORE_EXISTING and logs "Realm
'classic-models' already exists. Import skipped", so editing it changes
nothing on a restart. The client and scope were created through the admin
API and recorded here so a fresh install matches. That divergence is
worth closing separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Infra Keycloak (Kubernetes / CaC)
Déploiement de Keycloak 26.6.0 et PostgreSQL 16 en Configuration-as-Code (CaC) sous Kubernetes dans le namespace keycloak.
Structure du Répertoire
k8s/
├── kustomization.yaml # Manifeste Kustomize principal
├── namespace.yaml # Namespace keycloak
├── secret.yaml # Secrets (Admin, DB, OIDC secrets)
├── secret.example.yaml # Gabarit des secrets
├── postgresql/
│ ├── pvc.yaml # Stockage persistant PostgreSQL (2Gi)
│ ├── statefulset.yaml # Base de données PostgreSQL 16
│ └── service.yaml # Service PostgreSQL (port 5432)
├── keycloak/
│ ├── configmap-realm.yaml # Realm classic-models (Configuration-as-Code)
│ ├── deployment.yaml # Déploiement Keycloak 26.6.0 (--import-realm)
│ └── service.yaml # Service Keycloak (ports 8080, 9000)
└── ingress/
├── tls.yaml # Secret TLS wildcard (*.sttlab.local)
└── ingress.yaml # Ingress NGINX (keycloak.sttlab.local)
Fonctionnalités Clés
- Version Keycloak :
quay.io/keycloak/keycloak:26.6.0(Runtime Quarkus). - Import Automatique du Realm (CaC) : Le realm
classic-modelsest chargé au démarrage via l'option--import-realmdepuis leConfigMapmonté dans/opt/keycloak/data/import/. - Token Exchange RFC 8693 : Activé via
KC_FEATURES: "token-exchange,admin-fine-grained-authz". - Clients Configurés :
librechat: Client OIDC pour le login et le flux OAuth MCP.mcp-catalog: Audience du serveur MCP.kong-token-exchange-broker: Échangeur de jetons RFC 8693 de Kong Gateway.catalog-api&classic-models-api: Audiences des services backend.
- Utilisateurs & Rôles :
admin-user(admin)pm-user(product_manager)ro-user(read_only)customer-user(customer)support-user(support)
Déploiement
k8s/secret.yaml n'est pas versionné : il porte les mots de passe réels (base
PostgreSQL, admin Keycloak, secrets clients OIDC). Le créer une fois depuis le
gabarit, puis renseigner chaque <CHANGE_ME> :
cp k8s/secret.example.yaml k8s/secret.yaml
$EDITOR k8s/secret.yaml
k8s/kustomization.yaml le référence, donc le déploiement échoue tant qu'il
est absent.
kubectl apply -k k8s/