AI
AgentOps : empêcher le transfert de jetons MCP avant les actions de production
Un runbook de production pour prouver audience, délégation, scopes et identité runtime dans une chaîne MCP avant de rétablir les actions avec effet.
Un agent interne d’exploitation peut lire correctement un incident tout en agissant avec la mauvaise autorité. Un utilisateur lui demande d’inspecter un seul service, l’agent appelle un serveur MCP, puis l’API cible enregistre une identité applicative trop large ou un jeton destiné à une autre ressource. La requête fonctionne, mais la chaîne d’autorisation ne prouve plus qui a demandé l’action, ce qui a été approuvé ni pourquoi l’outil pouvait atteindre cette cible.
Le cas fil rouge est un assistant d’incident connecté à Microsoft Foundry. Il appelle un serveur MCP interne pour lire la santé d’un service et préparer un redémarrage borné. Les lectures utilisent le contexte délégué de l’appelant ; les changements d’état exigent une identité workload dédiée et une approbation humaine. Après une modification d’authentification, les traces ne concordent plus sur l’audience du jeton et le principal effectif. Ce runbook doit mener à une décision : restaurer la délégation prévue, maintenir les écritures désactivées derrière des outils read-only, ou rollbacker la release d’authentification.
Figer une chaîne d’autorisation
Ne commencez pas par ajouter un rôle. Capturez une requête depuis la frontière utilisateur jusqu’à l’API cible et conservez des identifiants, jamais les bearer tokens. Une chaîne complète distingue l’humain, le runtime de l’agent, le serveur MCP et la ressource aval.
incident: inc-20260913-006
request:
intent_id: intent-7f21
trace_id: 8cf4b7d6a2d14ef1
user_object_id: <user-oid>
approved_action: prepare_restart
approved_target: api-orders-prod / instance-03
agent:
runtime: ops-assistant-prod
release: 2026.09.13.1
mcp:
server: mcp-operations-prod
tool: prepare_service_restart
tool_call_id: call-31c8
downstream:
api: operations-control-api
expected_audience: api://operations-control
expected_mode: workload-identity-after-human-approval
preserve:
- token_claims_redacted
- approval_record_and_fingerprint
- tool_arguments_and_schema_version
- downstream_request_id
- identity_and_policy_release_timeline Ne placez jamais d’access token, d’en-tête Authorization ni de refresh token dans le dossier d’incident. Ne gardez que les claims minimaux nécessaires pour expliquer la décision et limitez l’accès à l’investigation.
Nommer l’autorité à chaque saut
« L’agent est autorisé » reste trop vague. Chaque saut doit nommer un sujet, une audience, un mode d’authentification et une action permise. L’accès délégué et l’accès applicatif répondent à deux questions différentes : le premier transporte un contexte utilisateur ; le second représente un workload agissant avec ses propres permissions.
Saut 1 Utilisateur -> runtime agent
Sujet : object ID utilisateur et tenant
Audience : application de l'agent
But : soumettre une demande d'exploitation
Saut 2 Runtime agent -> serveur MCP
Sujet : utilisateur délégué ou identité workload de l'agent
Audience : serveur MCP
But : appeler un outil déclaré
Saut 3 Serveur MCP -> API aval
Sujet : échange délégué explicite ou identité workload MCP dédiée
Audience : API aval
But : lire la santé ou préparer une action bornée
Raccourcis interdits
Transférer le bearer token initial sans valider son audience
Réutiliser un jeton applicatif large sur plusieurs API sans rapport
Confondre visibilité d'un outil et droit de l'exécuter
Transformer une demande utilisateur en écriture applicative sans approbation Cette carte représente le contrat attendu. L’incident est l’écart entre ce contrat et ce que le runtime a réellement émis.
Inspecter les claims sans exposer les secrets
Inspectez un jeton uniquement dans un chemin de diagnostic contrôlé. Le service récepteur doit valider sa signature et sa policy ; un décodage local facilite le triage, mais ne prouve pas l’authenticité. Journalisez une empreinte à sens unique du jeton et une petite liste de claims autorisés : la même credential devient corrélable entre les sauts sans transformer les logs en preuve rejouable.
import { createHash } from 'node:crypto';
export function diagnosticEnvelope(rawToken, verifiedClaims) {
return {
tokenFingerprint: createHash('sha256').update(rawToken).digest('hex').slice(0, 16),
iss: verifiedClaims.iss,
aud: verifiedClaims.aud,
tid: verifiedClaims.tid,
oid: verifiedClaims.oid,
azp: verifiedClaims.azp ?? verifiedClaims.appid,
scp: verifiedClaims.scp?.split(' ') ?? [],
roles: verifiedClaims.roles ?? [],
iat: verifiedClaims.iat,
exp: verifiedClaims.exp
};
} À chaque récepteur, vérifiez issuer, tenant, audience, durée de vie et forme d’autorisation attendue. Un jeton délégué expose généralement des scopes, tandis qu’un jeton applicatif expose généralement des app roles. N’acceptez aucune forme parce que le claim existe : comparez-la à la policy explicite de l’endpoint.
Corréler le caller vu par la cible
Les traces de l’agent prouvent ce que l’orchestrateur a tenté. L’API aval prouve quelle identité est arrivée. Joignez les deux vues avec trace_id, tool_call_id et le request ID aval. Si la cible voit une identité applicative inattendue, élargir son rôle ne ferait que conserver le défaut.
let StartTime = datetime(2026-09-13T06:00:00Z);
let EndTime = datetime(2026-09-13T07:00:00Z);
let TraceId = "8cf4b7d6a2d14ef1";
union isfuzzy=true
(AppTraces
| where TimeGenerated between (StartTime .. EndTime)
| where tostring(Properties.trace_id) == TraceId
| project TimeGenerated, Source="agent-or-mcp", Message,
Tool=tostring(Properties.tool),
ToolCallId=tostring(Properties.tool_call_id),
Audience=tostring(Properties.token_aud),
Caller=tostring(Properties.caller_oid),
ClientApp=tostring(Properties.client_app_id),
Result=tostring(Properties.result)),
(AppRequests
| where TimeGenerated between (StartTime .. EndTime)
| where tostring(Properties.trace_id) == TraceId
| project TimeGenerated, Source="downstream-api", Name,
Tool="", ToolCallId=tostring(Properties.tool_call_id),
Audience=tostring(Properties.token_aud),
Caller=tostring(Properties.caller_oid),
ClientApp=tostring(Properties.client_app_id),
Result=tostring(ResultCode))
| order by TimeGenerated asc Le nom des propriétés dépend du contrat de télémétrie. Définissez-les dans le middleware d’authentification et le wrapper d’outil plutôt que de parser des logs en texte libre. L’absence de ligne aval est aussi une information : l’appel peut avoir échoué avant l’authentification, être bloqué par une policy ou suivre un autre chemin réseau.
Séparer délégation et action privilégiée
Un pattern utile consiste à séparer diagnostic et exécution. Les outils read-only peuvent utiliser une identité déléguée bornée lorsque l’API cible le permet. Un outil qui change l’état doit exiger un enregistrement d’approbation, une cible exacte et une identité workload dédiée dont les permissions correspondent à l’opération. Le serveur MCP ne doit jamais promouvoir silencieusement un mode vers l’autre.
{
"tool": "prepare_service_restart",
"schemaVersion": "3.2",
"mode": "prepare_only",
"required": {
"intentId": "intent-7f21",
"target": "api-orders-prod/instance-03",
"expectedState": "unhealthy",
"approvalFingerprint": "sha256:<fingerprint>",
"expiresAt": "2026-09-13T07:15:00Z"
},
"identityPolicy": {
"acceptedAudience": "api://operations-control",
"acceptedClient": "<mcp-workload-client-id>",
"requiredApplicationRole": "ServiceRestart.Prepare",
"delegatedScopesAccepted": false
},
"rejectWhen": [
"target_differs_from_approval",
"approval_expired",
"caller_or_audience_unexpected",
"execution_requested_in_prepare_only_mode"
]
} Le schéma de l’outil contraint les arguments ; l’autorisation de l’API contraint les effets. Les deux sont nécessaires. Masquer un outil ne désactive pas une permission, et un jeton valide ne vaut pas approbation pour tous les paramètres.
Contenir au périmètre le plus étroit
Si la chaîne reste ambiguë, retirez la capacité d’écriture sans désactiver le diagnostic. Réappliquez la configuration d’authentification précédente lorsqu’elle est connue saine, ou forcez les outils sensibles en prepare_only. Ne révoquez la credential concernée qu’après avoir identifié ses autres dépendances : une révocation aveugle peut masquer le caller d’origine et casser des lectures sans rapport.
Confinement immédiat
Désactiver le mode execute des outils MCP qui changent l'état
Conserver les sources approuvées et les health checks read-only
Refuser tout jeton dont issuer, tenant ou audience n'est pas exact
Refuser les jetons délégués sur les endpoints application-only
Préserver une trace en échec et une trace saine
À ne pas faire pendant le triage
Ajouter un rôle API large à l'identité actuellement observée
Journaliser les bearer tokens pour faciliter la comparaison
Accepter temporairement plusieurs audiences
Réactiver les écritures parce que la même requête réussit manuellement
Faire tourner toutes les credentials avant de cartographier les consommateurs L’objectif est d’arrêter l’autorité injustifiée tout en conservant assez d’observabilité pour réparer le contrat.
Tester les chemins positifs et négatifs
Rejouez d’abord avec des identités synthétiques et des cibles hors production. Testez le chemin prévu et des violations volontaires : mauvaise audience, mauvais tenant, scope ou rôle absent, approbation expirée, cible modifiée, empreinte d’approbation réutilisée et tentative de transmettre un jeton délégué à une opération application-only.
cases:
- id: delegated-health-read
token: delegated / Health.Read / correct audience
tool: get_service_health
expect: allowed
- id: wrong-audience
token: valid signature / different audience
tool: get_service_health
expect: denied_before_tool_execution
- id: delegated-write
token: delegated / correct audience
tool: execute_service_restart
expect: denied_application_role_required
- id: stale-approval
token: workload / ServiceRestart.Execute
approval: expired
expect: denied_before_downstream_call
- id: target-substitution
token: workload / ServiceRestart.Execute
approved_target: instance-03
requested_target: all-instances
expect: denied_target_mismatch
promotion_gate:
unexpected_callers: 0
raw_tokens_in_logs: 0
negative_cases_allowed: 0
trace_chain_complete: true Passez ensuite en canari sur un chemin de lecture et une action seulement préparée. L’exécution réelle arrive en dernier, sur une cible réversible, avec l’API cible enregistrant le client, le rôle, l’empreinte d’approbation et les identifiants de corrélation attendus.
Décider restauration, mode dégradé ou rollback
Ne restaurez les outils avec effet que si chaque saut accepte l’audience et l’identité prévues, si la cible impose le bon scope ou rôle, si l’approbation est liée à l’action exacte et si les tests négatifs échouent en mode fermé. Maintenez l’agent en read-only ou prepare_only lorsque la chaîne reste utile mais ne prouve pas encore le droit d’exécuter.
Rollbackez la release d’authentification si le contrat précédent est connu, compatible et plus étroit. Faites tourner ou révoquez une credential lorsqu’elle a été exposée ou utilisée hors policy, après avoir cartographié ses consommateurs. Si le design dépend du transfert d’un même jeton entre plusieurs services sans contrat de délégation explicite, arrêtez le rollout et redessinez ce saut au lieu de documenter l’ambiguïté comme une exception.
Conclusion
Un appel d’outil MCP n’est pas un unique événement d’autorisation. C’est une chaîne de récepteurs, audiences, sujets, policies et effets. Diagnostiquez-la en comparant la carte d’autorité attendue aux claims vérifiés et à l’identité observée par l’API aval.
La sortie doit être opérationnelle : restaurer une délégation explicite, utiliser une identité workload dédiée pour les écritures approuvées, maintenir les outils sensibles dans un mode sans exécution, ou rollbacker la release. L’incident est clos seulement quand l’appel attendu réussit, qu’une autorité substituée est refusée, que les logs ne contiennent aucun token brut et que l’action entière reste attribuable.