Au terme des deux premiers volets, deux faits sont prouvés cryptographiquement :
qui est chaque workload (volet 1 —
le SVID), et pour le compte de qui, via quelle chaîne, il agit
(volet 2 — la délégation et le
claim act). Le jeton qui arrive à l’outil porte donc « le chercheur, agissant
pour Alice, via le superviseur ». Tout cela est fiable. Et pourtant rien, encore,
ne dit si cet appel doit aboutir.
C’est la troisième question, et la dernière brique : (principal, chaîne d'acteurs, action, ressource) → autorisé, ou non ? L’autorisation. Prouver l’identité
et l’intention ne donne aucun droit — c’est précisément ce qu’on veut : une identité
fiable sur laquelle écrire une politique, séparée d’elle.
Le problème : l’intention prouvée ne donne pas le droit
Un agent qui prouve agir pour Alice ne devrait pas pour autant pouvoir faire tout
ce qu’Alice peut faire. Trois propriétés rendent l’autorisation agentique
spécifique — et fatale à un simple if dans le code :
- La décision dépend d’attributs, pas seulement d’un rôle figé : le principal et ses rôles, la sensibilité de la ressource, l’action, le contexte (heure, canal), et la chaîne d’agents qui a porté la requête. C’est de l’ABAC (attribute-based access control), pas du RBAC plat.
- L’agent ne doit jamais excéder le principal — et souvent il doit en faire moins. Un agent chercheur autorisé à lire les factures pour le compte d’un utilisateur ne devrait pas pouvoir lire ses données RH, même si l’utilisateur, lui, en a le droit. C’est le moindre privilège du chemin (on y revient).
- La politique change plus vite que le code. Coder les règles en dur, c’est un redéploiement à chaque ajustement, et aucune traçabilité de pourquoi un accès a été accordé. Une politique d’agents évolue en continu ; elle doit être modifiable, versionnée et auditable sans toucher aux agents.
Ce qu’on veut : une décision externalisée, écrite dans un langage dédié,
testable, qui prenne en entrée toute la richesse du jeton de délégation et
réponde allow / deny — assortie au besoin d’obligations.
Séparer la décision de l’exécution : PEP / PDP
Le patron est ancien et normalisé (NIST SP 800-207, déjà cité au volet 1) : on sépare l’endroit qui applique la décision de celui qui la prend.
- PEP — Policy Enforcement Point. Là où l’action a lieu : l’outil, ou la passerelle devant lui. Il intercepte l’appel, en extrait le contexte, demande une décision, puis l’applique (laisse passer, refuse, caviarde…).
- PDP — Policy Decision Point. Le moteur qui évalue la politique et répond.
Il ne sait rien de l’outil ; il reçoit un
input, renvoie une décision.
Cette séparation est ce qui rend la politique as code : le PDP est interrogeable, versionné, testé à part, et partagé par tous les outils — une seule source de vérité pour « qui a le droit de quoi ».
La politique comme code : OPA & Rego
Le PDP de référence, vendor-neutral et gradué à la CNCF, est OPA (Open
Policy Agent). On y écrit la politique en Rego, on l’interroge en HTTP avec un
input JSON, on récupère une décision. Le code des agents n’embarque aucune règle :
il pose une question.
L’input est construit à partir du jeton de délégation du volet 2 — c’est là que
les volets se rejoignent :
{
"principal": { "id": "alice@deep-5.com", "roles": ["finance"] },
"chaine": [
"spiffe://agentique.world/agent/researcher",
"spiffe://agentique.world/agent/supervisor"
],
"action": "search",
"ressource": { "index": "factures", "sensibilite": "confidentiel" }
}
Le principal et la chaine ne sont pas déclarés par l’agent : ils sont extraits
du jeton vérifié (sub et claim act). Le modèle ne peut pas mentir sur l’identité
de l’utilisateur ni se réinventer une chaîne — c’est tout le bénéfice des volets
précédents qui se monnaie ici.
Voici une politique Rego qui combine les trois exigences — rôle du principal, chaîne d’agents habilitée, et obligations :
# policy/autorisation.rego
package agentique.autorisation
import rego.v1
# Refus par défaut : rien n'est permis sans règle explicite (zero-trust).
default allow := false
# 1. Un des rôles du principal couvre-t-il la ressource demandée ?
principal_ok if {
some role in input.principal.roles
role in data.acces_role[input.ressource.index]
}
# 2. CHAQUE agent du chemin est-il habilité à opérer cette action pour autrui ?
chaine_ok if {
every agent in input.chaine {
agent in data.delegation_permise[input.action]
}
}
allow if {
principal_ok
chaine_ok
}
# Obligation : caviarder les champs sensibles d'une ressource confidentielle.
obligations contains {"type": "redact", "champs": data.champs_sensibles[input.ressource.index]} if {
input.ressource.sensibilite == "confidentiel"
}
# Obligation : exiger une validation humaine pour une action à risque.
obligations contains {"type": "approbation_humaine"} if {
input.action in data.actions_a_risque
}
# La décision exposée au PEP : autorisation + obligations à appliquer.
decision := {"allow": allow, "obligations": obligations}
Les faits (rôles, délégations permises, ressources sensibles) vivent dans un document de données séparé, modifiable sans toucher aux règles :
// policy/data.json
{
"acces_role": { "factures": ["finance"], "rh": ["rh"] },
"delegation_permise": {
"search": [
"spiffe://agentique.world/agent/supervisor",
"spiffe://agentique.world/agent/researcher"
]
},
"champs_sensibles": { "factures": ["iban", "montant"] },
"actions_a_risque": ["delete", "refund"]
}
Hands-on : autoriser un appel d’outil dans le graphe
Branchons OPA en PDP, écrivons le PEP côté outil, câblons-le au graphe LangGraph du volet 2, puis testons la politique.
Lancer OPA avec la politique
OPA tourne en serveur local, charge la politique et les données, et expose une API de décision.
# Récupérer le binaire depuis les releases du projet, puis :
opa run --server --addr localhost:8181 policy/
# La décision est alors interrogeable sur :
# POST http://localhost:8181/v1/data/agentique/autorisation/decisionÉcrire le PEP côté outil
Le PEP valide le jeton (volets 1 & 2), construit l’input, interroge OPA, puis
applique la décision. On réutilise valider_jwt et chaine_acteurs du volet 2.
import httpx
OPA = "http://localhost:8181/v1/data/agentique/autorisation/decision"
def decider(token: str, action: str, ressource: dict) -> dict:
"""Construit l'input à partir du jeton de délégation et interroge le PDP."""
p = valider_jwt(token, audience="spiffe://agentique.world/tool/search") # volet 2
entree = {
"principal": {"id": p["sub"], "roles": roles_de(p["sub"])},
"chaine": chaine_acteurs(p), # extrait du claim act — volet 2
"action": action,
"ressource": ressource,
}
r = httpx.post(OPA, json={"input": entree})
r.raise_for_status()
return r.json()["result"] # {"allow": ..., "obligations": [...]}
def appeler_outil(token: str, requete: str):
"""Le PEP : décide, refuse ou applique les obligations, puis exécute."""
d = decider(token, action="search",
ressource={"index": "factures", "sensibilite": "confidentiel"})
if not d["allow"]:
raise PermissionError("Action refusée par la politique")
# Obligations bloquantes d'abord (ex. validation humaine).
for o in d["obligations"]:
if o["type"] == "approbation_humaine" and not approbation_obtenue(token):
raise PermissionError("Validation humaine requise avant exécution")
resultat = recherche(requete)
# Obligations transformatrices ensuite (ex. caviardage).
for o in d["obligations"]:
if o["type"] == "redact":
resultat = caviarder(resultat, o["champs"])
return resultatCâbler le PEP dans le nœud LangGraph
Le nœud chercheur du volet 2 n’appelle plus l’outil directement : il passe par le PEP, qui peut refuser avant tout effet de bord.
def chercheur(etat: Etat) -> Etat:
valider_jwt(etat["jeton"], audience="spiffe://agentique.world/agent/researcher")
svid = svid_jwt(audience=STS)
jeton = echanger(etat["jeton"], svid,
audience="spiffe://agentique.world/tool/search") # volet 2
# La politique tranche ici ; un refus lève et coupe la branche du graphe.
etat["resultat"] = appeler_outil(jeton, etat["requete"])
return etatTester la politique (opa test)
Une politique non testée est une faille en attente. Rego se teste comme du code — chaque règle, chaque cas limite.
# policy/autorisation_test.rego
package agentique.autorisation_test
import rego.v1
import data.agentique.autorisation as az
# Cas nominal : un chercheur lit les factures pour un utilisateur « finance ».
test_chercheur_lit_factures if {
az.allow with input as {
"principal": {"roles": ["finance"]},
"chaine": [
"spiffe://agentique.world/agent/researcher",
"spiffe://agentique.world/agent/supervisor",
],
"action": "search",
"ressource": {"index": "factures", "sensibilite": "interne"},
}
}
# Un agent absent du registre de délégation est refusé, même principal valide.
test_agent_inconnu_refuse if {
not az.allow with input as {
"principal": {"roles": ["finance"]},
"chaine": ["spiffe://agentique.world/agent/pirate"],
"action": "search",
"ressource": {"index": "factures", "sensibilite": "interne"},
}
}
# Le rôle du principal ne couvre pas la ressource → refus.
test_role_insuffisant if {
not az.allow with input as {
"principal": {"roles": ["finance"]},
"chaine": ["spiffe://agentique.world/agent/researcher"],
"action": "search",
"ressource": {"index": "rh", "sensibilite": "interne"},
}
}opa test policy/ -v
# PASS: 3/3 Le moindre privilège du chemin : l’atténuation
C’est la propriété la plus spécifiquement agentique, et la plus oubliée. Le pouvoir effectif d’un appel n’est pas celui du principal : c’est ce que le principal peut faire, rétréci par chaque agent du chemin. Une chaîne ne peut qu’atténuer, jamais amplifier.
Dans la politique Rego ci-dessus, l’atténuation est déjà là, implicite : allow
exige à la fois principal_ok et chaine_ok — un ET logique, donc une
intersection. On peut la rendre explicite quand on raisonne en ensembles de
portées (quelles ressources, quelles actions) :
def scope_effectif(principal: set[str], chemin: list[set[str]]) -> set[str]:
"""Pouvoir effectif = droits du principal ∩ max délégué à CHAQUE agent.
Jamais plus que le principal, jamais plus que le plus restrictif du chemin."""
effectif = set(principal)
for portee_agent in chemin:
effectif &= portee_agent # intersection : chaque maillon peut réduire
return effectif
# Alice : finance + rh ; superviseur délègue large ; chercheur borné à finance.
scope_effectif(
principal={"factures", "rh", "contrats"},
chemin=[{"factures", "rh", "contrats", "tickets"}, # superviseur
{"factures", "tickets"}], # chercheur
) # -> {'factures'} : ni rh (chercheur l'ignore), ni tickets (Alice ne l'a pas)
Obligations & human-in-the-loop
Une décision d’autorisation n’est pas binaire. Le PDP renvoie souvent un allow
conditionné par des obligations que le PEP doit honorer :
- Caviardage / filtrage — autoriser l’accès mais masquer l’IBAN, ou ne retourner que les lignes du périmètre du principal.
- Validation humaine (human-in-the-loop) — pour une action à risque (
delete,refund), la politique n’autorise que sous réserve d’une approbation. L’escalade vers l’humain devient une sortie de la politique, pas un cas codé en dur.
C’est exactement ce que renvoie notre règle decision (obligations) et ce
qu’applique appeler_outil : on sépare les obligations bloquantes (à honorer
avant l’effet de bord) des obligations transformatrices (à appliquer au
résultat). Le human-in-the-loop n’est plus un if égaré dans un agent — c’est une
décision de politique, centralisée et auditable.
Alternatives & standards
OPA/Rego est un excellent défaut, mais ce n’est pas le seul outil — et l’interface PEP↔PDP se normalise :
- OPA / Rego (CNCF, gradué) — PDP généraliste, idéal pour de l’ABAC et des obligations. Notre choix ici.
- Cedar (open source, AWS) — langage de politique typé, avec schéma et analyse statique (on peut prouver qu’une politique n’autorise jamais X). Plus sûr à grande échelle, un peu moins souple que Rego.
- OpenFGA / Google Zanzibar — modèle ReBAC (relationship-based) : « qui est relié à quoi ». Imbattable pour des graphes d’autorisations massifs (partages, hiérarchies d’objets) ; complémentaire d’un PDP ABAC.
- AuthZEN (OpenID Foundation) — standardise l’API de décision entre PEP et PDP (format de la requête et de la réponse), pour rendre les moteurs interchangeables. Le pendant, côté autorisation, de ce que RFC 8693 est à la délégation.
L’invariant, quel que soit l’outil, reste la séparation PEP/PDP et la politique comme code testable — pas le langage choisi.
Bonnes pratiques & pièges
- Séparer décision et exécution (PEP/PDP) : la politique vit hors du code des agents, partagée et versionnée.
- Écrire la politique comme du code testable (Rego + opa test) : chaque règle, chaque cas limite couvert.
- Construire l'input depuis le jeton vérifié (sub + chaîne act, volets 1 & 2), jamais depuis ce que l'agent déclare.
- Imposer l'atténuation : pouvoir effectif = principal ∩ max délégué à chaque agent du chemin.
- Refus par défaut (default allow := false) : on autorise par règle explicite, jamais l'inverse.
- Renvoyer des obligations (caviardage, validation humaine) et journaliser chaque décision pour l'audit.
- Coder les règles en dur dans les agents : redéploiement à chaque changement, aucune traçabilité.
- Autoriser sur le seul principal en ignorant la chaîne : l'agent hérite de tous les droits de l'utilisateur.
- Laisser un agent dépasser le principal : une injection devient une escalade de privilèges.
- Faire confiance à des attributs déclarés par l'appelant plutôt qu'extraits du jeton vérifié.
- Politique sans tests : une faille silencieuse qui ne se voit qu'en production.
- Oublier les obligations : un allow binaire fait fuiter ce qu'un caviardage aurait masqué.
Ce qu’il faut retenir
- Prouver l’identité (volet 1) et l’intention (volet 2) ne donne aucun droit :
l’autorisation est une décision distincte, sur
(principal, chaîne, action, ressource). - On sépare décision et exécution (PEP/PDP, NIST 800-207) et on écrit la politique comme du code testable — ici OPA & Rego, refus par défaut.
- L’
inputde décision se construit depuis le jeton vérifié des volets précédents — le principal et la chaîne ne sont jamais auto-déclarés. - L’atténuation est la règle agentique cardinale : pouvoir effectif = intersection des droits du principal et du maximum délégué à chaque agent du chemin. Jamais plus.
- La décision porte des obligations (caviardage, human-in-the-loop) et chaque appel au PDP devient une ligne d’audit — pont vers l’observabilité de sécurité.
Avec ces trois volets, un système agentique sait qui se parle (identité), pour le compte de qui (délégation) et ce que chacun a le droit de faire (autorisation). Le triptyque zero-trust est complet. Reste à le voir vivre — détecter l’anomalie d’identité, l’escalade silencieuse, la chaîne inhabituelle : l’observabilité de sécurité, prochain volet de la série.