Un prompt qui ordonne à l'agent de ne rien supprimer n'est pas une frontière d'autorisation : seuls l'identité, le RBAC (contrôle d'accès basé sur les rôles) Azure, le contrat de l'outil et la politique d'exécution le sont, tous situés hors du modèle. Ce tutoriel applique cette idée à un agent Microsoft Foundry doté d'une identité d'agent Microsoft Entra (Entra Agent ID). Vous repartez avec un script d'affectation de rôle sur un seul conteneur, une politique deny-by-default en Python et une batterie de tests de refus.
Un prompt n'est pas une frontière : le contrat de capacité
Avant de brancher le moindre outil, écrivez ce que l'agent a le droit de faire, puis traduisez chaque ligne en contrôle technique. Voici un contrat type pour un agent de revue de factures.
| Décision | Exemple | Contrôle qui l'applique |
|---|---|---|
| Agent | Agent de revue de factures | Identité d'agent dédiée |
| Outil | Lire une facture approuvée | Schéma de l'outil à un seul identifiant métier |
| Ressource Azure | Un conteneur de stockage | Rôle RBAC au scope du conteneur |
| Opérations autorisées | Lecture | Rôle en lecture seule + politique applicative |
| Opérations interdites | Lister, téléverser, écraser, supprimer | Absence de rôle + refus par défaut |
| Limites d'exécution | 20 lectures par run, 5 Mo par objet | Vérification dans le code de l'outil |
| Preuves | ID agent, ID utilisateur, décision, version de politique, ID de trace | Traces OpenTelemetry |
La règle de validation est simple : si une exigence ne se traduit ni en affectation RBAC, ni en schéma d'outil, ni en vérification à l'exécution, elle n'est pas encore applicable. Elle vaut aussi pour le nommage des outils. Exposez get_invoice, check_order_status ou create_refund_request, pas run_sql, call_url ou execute_command : plus l'outil est étroit, plus le rayon d'impact d'une mauvaise décision du modèle ou d'une injection de prompt est réduit.
Choisir l'identité qui atteint l'outil
Une conception de production pose deux questions distinctes. L'agent a-t-il le droit d'invoquer l'outil ? L'appel résultant a-t-il le droit de toucher les données de cet utilisateur ou de cette charge de travail ? Les deux réponses ne passent pas par la même identité.
| Scénario | Identité d'exécution recommandée | Pourquoi |
|---|---|---|
| Agents de développement aux permissions temporaires identiques | Identité partagée du projet | Simplicité, périmètre non publié |
| Agent de production avec piste d'audit propre | Identité d'agent dédiée | Révocation, périmètre et enquête agent par agent |
| Traitement sans surveillance | Identité d'agent ou de charge de travail, permissions applicatives étroites | Aucun utilisateur connecté |
| Données propres à l'utilisateur | Identité déléguée ou connexion OAuth | L'appel respecte les droits de l'utilisateur |
| Point de terminaison public anonyme | Aucune authentification | Seulement si l'anonymat est voulu et validé |
La documentation Microsoft Learn précise le fonctionnement de l'identité d'agent : chaque agent Foundry reçoit un service principal créé sous un blueprint d'agent. Cette identité démarre sans aucune permission, et Foundry n'attribue aucun rôle RBAC automatiquement. C'est à vous d'accorder les droits via le modèle d'autorisation du service cible, avec l'object ID de l'identité comme bénéficiaire (Agent identity concepts in Microsoft Foundry).
Zéro permission par défaut : c'est voulu
Une identité d'agent neuve ne peut rien lire. Si votre outil échoue au premier appel, ne remontez pas le scope du rôle pour « débloquer » : vérifiez d'abord l'object ID utilisé comme assignee et le délai de propagation.
L'authentification se configure sur la connexion de l'outil, jamais dans le prompt ni dans le code de l'agent. Pour les outils MCP (Model Context Protocol) et A2A (agent à agent), Microsoft Learn liste les types none, custom-keys, oauth2, user-entra-token, project-managed-identity et agentic-identity, créés avec azd ai connection create (Connect agents to MCP server endpoints).
Deux nuances à connaître pour la délégation utilisateur :
- Le type
AgentOnBehalfOfUser(l'identité d'agent agissant avec l'utilisateur) est en préversion ;UserEntraTokenreste le chemin de compatibilité. - Pour Work IQ MCP, seul
AgentOnBehalfOfUserest pris en charge : niUserEntraToken, ni l'accès application seule.
Les Toolboxes Foundry offrent un levier supplémentaire. Elles regroupent des outils derrière un point de terminaison MCP géré qui centralise l'injection d'identifiants, le rafraîchissement des jetons et l'application des politiques, les agents s'y authentifiant avec des identifiants Entra. Les skills dans les toolboxes sont en préversion (What is Toolbox in Microsoft Foundry?).
Authentifier sans secret et lier la ressource côté serveur
En développement, DefaultAzureCredential fournit une chaîne d'identifiants locale pratique. En production sur Azure, utilisez explicitement ManagedIdentityCredential pour qu'aucun autre identifiant disponible ne soit sélectionné par surprise.
1import os2 3from azure.ai.projects import AIProjectClient4from azure.identity import DefaultAzureCredential, ManagedIdentityCredential5 6 7def build_credential():8 if os.environ.get('APP_ENV') == 'production':9 # AZURE_CLIENT_ID est facultatif pour une identité managée affectée au système.10 # Il désigne l'identité quand une identité affectée à l'utilisateur est employée.11 client_id = os.environ.get('AZURE_CLIENT_ID')12 if client_id:13 return ManagedIdentityCredential(client_id=client_id)14 return ManagedIdentityCredential()15 return DefaultAzureCredential()16 17 18project_client = AIProjectClient(19 endpoint=os.environ['FOUNDRY_PROJECT_ENDPOINT'],20 credential=build_credential(),21)Supprimer les secrets ne crée pas le moindre privilège : le principal doit encore recevoir un rôle minimal au scope le plus étroit possible, par exemple un conteneur plutôt qu'un groupe de ressources. Séparez aussi plan de contrôle et plan de données. L'identité qui déploie un agent n'a pas à lire les données métier de production, et l'identité d'exécution n'a pas à créer de projets, de connexions ou d'affectations de rôle.
Reste le piège classique : laisser le modèle fournir l'URL du compte, le conteneur et le chemin du blob. La conception sûre fixe le compte et le conteneur dans une configuration de confiance et n'accepte qu'un identifiant métier validé.
1import json2import os3import re4from dataclasses import dataclass5 6from azure.storage.blob import BlobServiceClient7 8INVOICE_ID = re.compile(r'^[A-Z]{3}-[0-9]{8}$')9MAX_BYTES = 5 * 1024 * 102410AGENT_OID = os.environ['INVOICE_AGENT_OBJECT_ID']11TENANT_ID = os.environ['PRODUCTION_TENANT_ID']12 13 14@dataclass(frozen=True)15class ToolContext:16 # Alimenté par l'identité vérifiée et les métadonnées de la requête,17 # jamais par des arguments générés par le modèle.18 agent_oid: str19 tenant_id: str20 user_oid: str | None21 run_id: str22 23 24@dataclass(frozen=True)25class ToolGrant:26 actions: frozenset27 tenants: frozenset28 require_user: bool29 30 31GRANTS = {32 AGENT_OID: ToolGrant(33 actions=frozenset({'invoice.read'}),34 tenants=frozenset({TENANT_ID}),35 require_user=True,36 )37}38 39 40def authorize(ctx: ToolContext, action: str) -> None:41 grant = GRANTS.get(ctx.agent_oid) # refus par défaut42 if grant is None:43 raise PermissionError('Identité d agent inconnue')44 if action not in grant.actions:45 raise PermissionError('Action non autorisée')46 if ctx.tenant_id not in grant.tenants:47 raise PermissionError('Tenant non autorisé')48 if grant.require_user and not ctx.user_oid:49 raise PermissionError('Contexte utilisateur vérifié requis')50 51 52class InvoiceReader:53 def __init__(self, credential, account_url: str, container: str):54 service = BlobServiceClient(account_url=account_url, credential=credential)55 self._container = service.get_container_client(container)56 57 def get_invoice(self, invoice_id: str, ctx: ToolContext) -> dict:58 authorize(ctx, 'invoice.read')59 if not INVOICE_ID.fullmatch(invoice_id):60 raise ValueError('Identifiant de facture invalide')61 # L'identité vérifiée détermine la partition : le modèle ne choisit pas le chemin.62 blob = self._container.get_blob_client(63 f'{ctx.tenant_id}/{ctx.user_oid}/{invoice_id}.json'64 )65 if blob.get_blob_properties().size > MAX_BYTES:66 raise ValueError('Facture au-dessus de la limite de 5 Mo')67 invoice = json.loads(blob.download_blob().readall())68 # Ne renvoyer que les champs utiles à l'agent.69 return {k: invoice[k] for k in ('invoice_id', 'supplier', 'amount', 'currency', 'status')}Le modèle choisit un identifiant de facture, rien d'autre : il ne peut ni changer de compte ou de conteneur, ni énumérer les objets, ni viser la partition d'un autre utilisateur. Utilisez des object IDs immuables dans la politique, jamais des noms d'affichage. Le contrôle applicatif et le RBAC sont complémentaires : la documentation Azure Storage sur l'attribution de rôles de données confirme que ces rôles peuvent être affectés au scope d'un conteneur.
Séparer lecture, proposition et validation
Un outil générique qui lit et écrit accorde mécaniquement trop de droits. Découpez-le en capacités aux politiques, voire aux identités, distinctes :
get_invoice(invoice_id): lecture seule, faible conséquence.propose_refund(invoice_id, amount, reason): crée une proposition révisable, sans déplacer d'argent.commit_refund(proposal_id, approval_token): forte conséquence, soumise à approbation, plafond strict et clé d'idempotence.
Un décorateur garde l'application de la politique uniforme :
1from collections.abc import Callable2from functools import wraps3 4 5def requires(action: str):6 def decorate(fn: Callable):7 @wraps(fn)8 def wrapped(*args, ctx: ToolContext, **kwargs):9 authorize(ctx, action)10 return fn(*args, ctx=ctx, **kwargs)11 return wrapped12 return decorate13 14 15@requires('refund.propose')16def propose_refund(invoice_id: str, amount: float, *, ctx: ToolContext) -> dict:17 if amount <= 0 or amount > 1000:18 raise ValueError('Montant hors du plafond de proposition de l agent')19 # On persiste une proposition uniquement ; un workflow approuvé distinct règle le remboursement.20 return {21 'state': 'pending_approval',22 'invoice_id': invoice_id,23 'amount': amount,24 'requested_by_agent': ctx.agent_oid,25 'run_id': ctx.run_id,26 }En production, le jeton d'approbation doit être de courte durée, lié à la proposition, à l'action et au montant approuvé, associé à un approbateur vérifié, validé par du code serveur de confiance, et à usage unique ou protégé par une clé d'idempotence. Une phrase générée par le modèle, du type « approuvé », ne constitue jamais une autorisation.
Mise en œuvre : attribuer un rôle en lecture sur un seul conteneur
Ce script accorde à l'identité d'agent un rôle de données Blob sur un conteneur, sans rien au-dessus. Il fonctionne en simulation par défaut et n'écrit qu'avec -Apply.
- Module :
Install-Module Az.Resources -Scope CurrentUser - Permission minimale : pouvoir écrire des affectations de rôle (
Microsoft.Authorization/roleAssignments/write) au scope du conteneur, par exemple via le rôle User Access Administrator ou Role Based Access Control Administrator limité à ce scope. - Sortie : la liste des affectations de l'agent hors du scope visé (signal de sur-privilège), puis l'affectation créée ou le message de simulation.
1#Requires -Modules Az.Resources2[CmdletBinding()]3param(4 [Parameter(Mandatory)][string]$SubscriptionId,5 [Parameter(Mandatory)][string]$ResourceGroup,6 [Parameter(Mandatory)][string]$StorageAccount,7 [Parameter(Mandatory)][string]$Container,8 # Object ID du service principal de l'identité d'agent (pas son appId)9 [Parameter(Mandatory)][string]$AgentObjectId,10 # Rôle de données en lecture seule ; vérifiez le nom exact dans votre environnement11 [string]$RoleName = 'Storage Blob Data Reader',12 # Sans -Apply, le script reste en simulation13 [switch]$Apply14)15 16$ErrorActionPreference = 'Stop'17 18if (-not (Get-AzContext)) { Connect-AzAccount | Out-Null }19Set-AzContext -SubscriptionId $SubscriptionId | Out-Null20 21# Scope le plus étroit : le conteneur lui-même22$scope = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Storage/storageAccounts/{2}/blobServices/default/containers/{3}' -f $SubscriptionId, $ResourceGroup, $StorageAccount, $Container23 24# Inventaire des droits actuels de l'agent25$all = @(Get-AzRoleAssignment -ObjectId $AgentObjectId)26$outside = @($all | Where-Object { $_.Scope -ne $scope })27if ($outside.Count -gt 0) {28 Write-Warning 'Affectations de l agent hors du conteneur visé : à justifier ou retirer.'29 $outside | Format-Table RoleDefinitionName, Scope -AutoSize30}31 32# Idempotence : ne rien faire si l'affectation existe déjà33$already = $all | Where-Object { $_.Scope -eq $scope -and $_.RoleDefinitionName -eq $RoleName }34if ($already) {35 Write-Host 'Affectation déjà présente sur le conteneur : rien à faire.'36 return37}38 39if (-not $Apply) {40 Write-Host ('[SIMULATION] Attribuerait {0} à {1} sur {2}' -f $RoleName, $AgentObjectId, $scope)41 return42}43 44New-AzRoleAssignment -ObjectId $AgentObjectId -ObjectType ServicePrincipal -RoleDefinitionName $RoleName -Scope $scope | Out-Null45 46# Vérification : l'affectation doit apparaître exactement à ce scope47Get-AzRoleAssignment -ObjectId $AgentObjectId -Scope $scope |48 Where-Object { $_.Scope -eq $scope } |49 Select-Object DisplayName, RoleDefinitionName, ScopeSimuler
Lancez le script sans -Apply et relisez la ligne [SIMULATION] ainsi que la liste des affectations hors scope.
Appliquer
Relancez avec -Apply. La création est idempotente : un second passage ne change rien.
Attendre la propagation
Les modifications d'attribution de rôle peuvent prendre jusqu'à 10 minutes à se propager. Un 403 juste après l'affectation n'est donc pas forcément une erreur de configuration.
Prouver le refus
Depuis l'identité de l'agent, vérifiez qu'une lecture dans le conteneur réussit, puis qu'un listage d'un autre conteneur, une écriture et une suppression échouent.
Publication : l'identité change, les droits ne suivent pas
Publier un agent crée une Agent Application dotée de sa propre identité d'agent et de son propre blueprint. Les permissions ne sont pas transférées depuis l'identité partagée du projet : il faut réassigner les rôles RBAC, sinon les appels d'outils qui fonctionnaient en développement échouent avec des erreurs d'autorisation (Publish your agent as an Agent Application). Relancez donc le script ci-dessus avec l'object ID de la nouvelle identité.
Appelants et appels sortants : deux contrôles distincts
Qui peut invoquer un agent publié se règle par Azure RBAC sur la ressource ARM Agent Application, avec le rôle Foundry User. L'identité Entra de l'agent ne sert qu'aux appels sortants. L'authentification par clé d'API n'est pas prise en charge pour invoquer une Agent Application.
Dépannage des refus d'accès
| Symptôme | Cause probable | Résolution |
|---|---|---|
| 403 quelques minutes après l'affectation du rôle | Propagation du RBAC, jusqu'à 10 minutes | Patienter puis relancer ; contrôler l'affectation avec Get-AzRoleAssignment |
| Outil fonctionnel en développement, 403 après publication | L'Agent Application a une nouvelle identité sans permissions | Réassigner les rôles RBAC à l'object ID de la nouvelle identité |
| 401 sur l'appel d'un outil | Audience incorrect ou service n'acceptant pas les jetons Entra | Vérifier l'audience configurée et le mode d'authentification du service cible |
| Work IQ MCP refuse l'authentification | Seul AgentOnBehalfOfUser est pris en charge | Utiliser ce type (préversion), pas UserEntraToken ni l'accès application seule |
| Un appelant ne parvient pas à invoquer l'agent publié | Rôle Foundry User absent sur la ressource Agent Application | Affecter Foundry User à l'appelant sur la ressource ARM |
| Invocation par clé d'API rejetée | Clé d'API non prise en charge pour une Agent Application | Passer à une authentification Entra |
Tracer chaque décision et tester les refus d'abord
Journalisez la décision d'autorisation avant l'appel en aval, avec des identifiants stables et la version de politique, mais sans jetons, secrets, prompts complets ni données métier. Connectez le projet Foundry à Application Insights et propagez le contexte de trace entre l'agent, la passerelle d'outils et l'appel aval.
1from opentelemetry import trace2 3tracer = trace.get_tracer('contoso.agent-tools')4 5 6def audited_authorize(ctx: ToolContext, action: str) -> None:7 with tracer.start_as_current_span('tool.authorization') as span:8 span.set_attribute('agent.id', ctx.agent_oid)9 span.set_attribute('tenant.id', ctx.tenant_id)10 span.set_attribute('tool.action', action)11 span.set_attribute('policy.version', '2026-10-01')12 span.set_attribute('run.id', ctx.run_id)13 try:14 authorize(ctx, action)15 span.set_attribute('authorization.decision', 'allow')16 except PermissionError:17 span.set_attribute('authorization.decision', 'deny')18 raiseCette piste applicative se double d'une piste côté plateforme. Selon Microsoft Learn, les identités d'agent Foundry apparaissent dans l'inventaire des agents du centre d'administration Microsoft Entra, les journaux de connexion Entra se filtrent par type d'agent et indiquent pour quel utilisateur un accès délégué a eu lieu. Microsoft Agent 365 synchronise automatiquement les agents Foundry publiés dans son registre.
Un test heureux prouve que l'outil marche ; seule une suite de refus prouve qu'il échoue proprement. Écrivez-la en premier :
1import pytest2 3 4@pytest.mark.parametrize(5 'ctx,action',6 [7 (ToolContext('unknown-agent-oid', TENANT_ID, 'u1', 'r1'), 'invoice.read'),8 (ToolContext(AGENT_OID, 'other-tenant-id', 'u1', 'r2'), 'invoice.read'),9 (ToolContext(AGENT_OID, TENANT_ID, None, 'r3'), 'invoice.read'),10 (ToolContext(AGENT_OID, TENANT_ID, 'u1', 'r4'), 'invoice.delete'),11 ],12)13def test_unauthorized_requests_are_denied(ctx, action):14 with pytest.raises(PermissionError):15 authorize(ctx, action)Completez par des tests d'intégration qui vérifient que l'identité Azure ne peut pas lister d'autres conteneurs, écrire ou supprimer, accéder à la partition d'un autre utilisateur ou tenant, être redirigée vers un point de terminaison non approuvé, appeler un outil à forte conséquence sans approbation, réutiliser un jeton d'approbation ou dépasser les limites de montant et de débit. Exécutez-les en intégration continue et après chaque changement d'identité, de rôle, de politique ou de connexion.
Pour prolonger ces contrôles côté plateforme, voyez notre article sur la sécurité MCP et la protection des outils à la source et notre guide du cycle de vie sécurisé des agents IA.
Par où commencer
Choisissez un agent et un outil, pas le catalogue entier. Remplacez l'outil générique par un contrat métier, donnez à l'agent une identité dédiée, limitez son rôle Azure à une ressource, puis placez une vérification deny-by-default avant l'appel aval.
Contrôles avant et après publication
- Chaque agent de production a une identité d'agent dédiée, adaptée à son niveau d'autorité et à ses besoins d'audit.
- L'authentification de l'outil est portée par la connexion, pas par le prompt ni le code.
- Les rôles sont affectés au scope le plus étroit et limités aux actions de données nécessaires.
- Le modèle ne peut fournir ni point de terminaison, ni scope, ni requête, ni nom de ressource arbitraire.
- Lecture, proposition et validation sont des capacités séparées ; les actions à forte conséquence exigent une approbation de confiance et une clé d'idempotence.
- Les rôles ont été réassignés à l'identité de l'Agent Application publiée.
- Les traces portent la preuve d'autorisation sans jeton ni donnée sensible.
- Les quatre refus de base (mauvais agent, mauvais tenant, utilisateur absent, action interdite) sont testés.
- Un interrupteur d'urgence désactive l'identité ou la connexion sans redéployer le modèle.
Si vos outils passent déjà par un point de terminaison MCP partagé, alors évaluez les Toolboxes pour centraliser identifiants et politiques, en gardant en tête que les skills y sont en préversion. Pour situer cette couche dans une défense plus large, consultez notre cadre de défense en couches pour agents IA.
Questions fréquentes
Foundry attribue-t-il automatiquement des rôles RBAC à l'identité d'un agent ?
Non. Chaque agent reçoit une identité d'agent Microsoft Entra qui démarre sans aucune permission, et Foundry n'assigne aucun rôle de lui-même. L'administrateur accorde les droits via le modèle d'autorisation du service cible, avec l'object ID de l'identité comme assignee.
Pourquoi mes outils échouent-ils après la publication de l'agent ?
Publier crée une Agent Application avec sa propre identité d'agent et son propre blueprint. Les permissions de l'identité partagée du projet ne sont pas transférées : il faut réassigner les rôles RBAC à la nouvelle identité.
Qui peut invoquer un agent publié ?
L'accès se contrôle par Azure RBAC sur la ressource ARM Agent Application, avec le rôle Foundry User. L'identité Entra de l'agent ne gouverne que ses appels sortants, et l'authentification par clé d'API n'est pas prise en charge pour l'invocation.
Combien de temps faut-il attendre après une affectation de rôle ?
Microsoft Learn indique que les changements d'attribution peuvent prendre jusqu'à 10 minutes à se propager. Un 403 immédiat peut donc disparaître seul, alors qu'un 401 évoque plutôt un audience incorrect ou un service qui n'accepte pas les jetons Entra.
Pour aller plus loin
- Agent identity concepts in Microsoft Foundry : identités et blueprints d'agent, échange de jetons, RBAC et surveillance via les journaux Entra.
- Publish your agent as an Agent Application : changement d'identité à la publication et dépannage des erreurs 401/403.
- Connect agents to MCP server endpoints : types d'authentification de connexion et commandes azd.
- What is Toolbox in Microsoft Foundry? : centraliser authentification et gouvernance des outils derrière un seul point de terminaison MCP.
