Skip to main content

« Agent Ready » : onboarder une équipe sur une plateforme agentique, sans ticket et avec des droits bornés

Updated 12 min readMarkdown
Part of seriesIA agentique : vers Azure et au-delà !
« Agent Ready » : onboarder une équipe sur une plateforme agentique, sans ticket et avec des droits bornés

Troisième article de la série tirée du talk « IA agentique : vers Azure et au-delà ! » (Genève, 29 octobre 2026). Code complet dans aloizeau/agentic-ai-azure-and-beyond-geneve-2026, dossiers terraform/modules/agent-ready et terraform/teams.

Une équipe de développement qui veut son agent l'obtient en une exécution de pipeline approuvée : deux identités sans secret, un stockage, un namespace avec quota, une clé de passerelle et un dépôt GitHub prêt à déployer. C'était la démo B du talk. Automatiser la création est simple ; le plus dur est de donner au pipeline juste les droits nécessaires.

Deux mécanismes bornent ces droits : une condition ABAC sur la délégation de rôles Azure, et une ValidatingAdmissionPolicy CEL côté Kubernetes.

Deux équipes, deux pipelines

Équipe IA (plateforme) Équipe de développement
Déclenche agent-ready.yml (workflow_dispatch) un push sur main de son dépôt agent-<équipe>
Approbation environnement GitHub ai-team, relecteur obligatoire aucune, elle ne touche que son namespace
Exécute Terraform terraform/teams, un état par équipe « 1 · Image » (build vers ACR) puis « 2 · Déploiement »
Identité id-<base>-ai-team, fédérée sur l'environnement ai-team id-<équipe>-pipeline, fédérée sur la branche main de son dépôt

Les workflows de l'équipe de dev font une dizaine de lignes et appellent les workflows réutilisables de l'équipe IA (agent-image.yml, agent-deploy.yml). L'équipe IA change la façon de construire et de déployer à un seul endroit.

Ce montage tient grâce à un comportement d'OIDC : quand un dépôt appelle un workflow réutilisable d'un autre dépôt, le sujet du jeton reste le dépôt et la branche de l'appelant. La fédération de l'identité d'équipe peut donc viser repo:<owner>/agent-<équipe>:ref:refs/heads/main, même si le code exécuté vient du dépôt de la plateforme.

Ce qu'une équipe reçoit en un apply

Tout est dans terraform/teams/main.tf.

Couche Ressources Usage
Azure identité du pipeline, fédérée GitHub pousser l'image et les documents, déployer
Azure identité du serveur MCP, fédérée AKS (system:serviceaccount:<équipe>:docs-mcp) lire les documents
Azure compte de stockage shared_access_key_enabled = false et conteneur documents aucune clé de compte à faire fuiter
Azure 4 attributions de rôle : Blob Data Contributor, Blob Data Reader, AcrPush, AKS Cluster User les droits des deux identités
Kubernetes namespace agentic.io/team et agentic.io/agent-ready, ResourceQuota, LimitRange périmètre et limites de capacité
Kubernetes clé virtuelle vk-<équipe> et sa copie gateway-key accès à /agents/v1 (article 1)
Kubernetes RoleBinding agentic-team-deployer pour l'identité du pipeline déployer dans son namespace
GitHub dépôt privé agent-<équipe> créé depuis le modèle, variables Actions, deux pipelines partir d'un squelette qui déploie déjà

Chaque équipe a son état Terraform (teams/<équipe>.tfstate). Pour retirer une équipe, un terraform destroy sur cet état supprime tout ce qu'elle a reçu sans toucher aux autres.

Borner Azure : la délégation de rôles sous condition ABAC

Pour attribuer quatre rôles aux identités qu'il vient de créer, le pipeline a besoin de Role Based Access Control Administrator. Sans condition, ce rôle lui permettrait de s'attribuer Owner. Azure permet de contraindre la délégation par une condition ABAC, et le module agent-ready n'autorise que quatre rôles, vers des principaux de service uniquement.

Fichier : terraform/modules/agent-ready/main.tf

locals {
  delegable = join(", ", [
    local.role_ids.storage_blob_data_contributor, # pipeline d'équipe → ses documents
    local.role_ids.storage_blob_data_reader,      # serveur MCP d'équipe → ses documents
    local.role_ids.acr_push,                      # pipeline d'équipe → registre
    local.role_ids.aks_cluster_user,              # pipeline d'équipe → kubeconfig Entra ID
  ])

  delegation_condition = <<-EOT
    (
     (
      !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
     )
     OR
     (
      @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegable}}
      AND
      @Request[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEqualsIgnoreCase {'ServicePrincipal'}
     )
    )
    AND
    (
     (
      !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
     )
     OR
     (
      @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegable}}
      AND
      @Resource[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEqualsIgnoreCase {'ServicePrincipal'}
     )
    )
  EOT
}

resource "azurerm_role_assignment" "ai_team_delegation" {
  for_each           = local.delegation_scopes   # rg-<base>-equipes, le registre, le cluster
  scope              = each.value
  role_definition_id = local.role_def.rbac_administrator
  principal_id       = azurerm_user_assigned_identity.ai_team[0].principal_id
  principal_type     = "ServicePrincipal"
  condition_version  = "2.0"
  condition          = local.delegation_condition
}

La condition a deux blocs symétriques. Le premier n'autorise l'écriture d'une attribution que si le rôle demandé (@Request) fait partie des quatre GUID et si le bénéficiaire est un principal de service ; le pipeline ne peut donc donner aucun droit à une personne ni à un groupe. Le second n'autorise la suppression que sur une attribution existante (@Resource) du même type. Sans lui, le pipeline pourrait supprimer l'attribution Owner d'un administrateur.

La portée compte autant que la condition. Le rôle n'est posé que sur le groupe de ressources des équipes, le registre et le cluster, et le pipeline n'est Contributeur que sur rg-<base>-equipes.

Pour vérifier, ajoutez ces commandes à une exécution du workflow agent-ready.yml, qui tourne sous l'identité de l'équipe IA, et tentez une attribution interdite. Elle doit échouer avec une erreur d'autorisation liée à la condition :

$rg  = az group show -n rg-<base>-equipes --query id -o tsv
$sp  = az identity show -g rg-<base>-equipes -n id-equipe-docs-pipeline --query principalId -o tsv

# Autorisé : un des quatre rôles, vers un principal de service
az role assignment create --assignee-object-id $sp --assignee-principal-type ServicePrincipal `
  --role "Storage Blob Data Reader" --scope $rg

# Refusé : Contributor ne fait pas partie des rôles délégables
az role assignment create --assignee-object-id $sp --assignee-principal-type ServicePrincipal `
  --role "Contributor" --scope $rg

Borner Kubernetes : RBAC pour les verbes, CEL pour le périmètre

Dans le cluster, le pipeline crée des namespaces, des quotas, des Secrets et des RoleBindings. RBAC sait autoriser la création de Secrets, mais pas la limiter aux namespaces d'équipe ou aux objets qui portent un label donné. La ClusterRole donne donc seulement les verbes :

Fichier : terraform/modules/platform/teams.tf

resource "kubernetes_cluster_role" "team_provisioner" {
  metadata { name = "agentic-team-provisioner" }
  rule {
    api_groups = [""]
    resources  = ["namespaces", "resourcequotas", "limitranges", "secrets"]
    verbs      = ["get", "create", "update", "patch", "delete"]
  }
  rule {
    api_groups = ["rbac.authorization.k8s.io"]
    resources  = ["rolebindings"]
    verbs      = ["get", "create", "update", "patch", "delete"]
  }
  rule {
    # Sans bind, Kubernetes refuse un RoleBinding vers des droits
    # que le pipeline ne détient pas lui-même (contrôle d'escalade).
    api_groups     = ["rbac.authorization.k8s.io"]
    resources      = ["clusterroles"]
    verbs          = ["bind"]
    resource_names = ["agentic-team-deployer"]
  }
}

Le verbe bind, limité à une seule ClusterRole, est peu connu. Il permet au pipeline de lier agentic-team-deployer sans en détenir lui-même les droits, et l'empêche de lier cluster-admin.

Le périmètre vient d'une ValidatingAdmissionPolicy (Kubernetes 1.30 et plus). Elle ne s'applique qu'à l'identité du pipeline et vérifie l'objet avant et après chaque modification :

Fichier : terraform/modules/platform/manifests/agent-ready-admission.yaml.tftpl

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: agent-ready-perimetre
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE", "DELETE"]
        resources: ["namespaces", "secrets", "resourcequotas", "limitranges"]
      - apiGroups: ["rbac.authorization.k8s.io"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE", "DELETE"]
        resources: ["rolebindings"]
  matchConditions:
    - name: pipeline-agent-ready
      expression: request.userInfo.username == '${principal_id}'
  variables:
    - name: objets            # le nouvel objet ET l'existant
      expression: >-
        request.operation == 'CREATE' ? [object] :
        (request.operation == 'DELETE' ? [oldObject] : [object, oldObject])
    - name: equipe            # le namespace cible est-il un namespace Agent Ready ?
      expression: >-
        request.resource.resource != 'namespaces' &&
        has(namespaceObject.metadata.labels) &&
        'agentic.io/agent-ready' in namespaceObject.metadata.labels &&
        namespaceObject.metadata.labels['agentic.io/agent-ready'] == 'true'
  validations:
    - expression: >-
        request.resource.resource != 'namespaces' ||
        variables.objets.all(o, has(o.metadata.labels) &&
          'agentic.io/agent-ready' in o.metadata.labels && o.metadata.labels['agentic.io/agent-ready'] == 'true' &&
          'agentic.io/team' in o.metadata.labels && o.metadata.labels['agentic.io/team'] == o.metadata.name)
      message: "namespace hors périmètre Agent Ready"
    - expression: >-
        request.resource.resource != 'secrets' || variables.equipe ||
        (request.namespace == 'agentgateway-system' &&
         variables.objets.all(o, o.metadata.name.startsWith('vk-') && has(o.metadata.labels) &&
           'agentic.io/virtual-key' in o.metadata.labels && o.metadata.labels['agentic.io/virtual-key'] == 'true' &&
           'agentic.io/agent-ready' in o.metadata.labels && o.metadata.labels['agentic.io/agent-ready'] == 'true'))
      message: "Secret hors périmètre : namespace Agent Ready, ou clé virtuelle vk-<équipe> créée par Agent Ready"
    - expression: >-
        request.resource.resource in ['namespaces', 'secrets'] || variables.equipe
      message: "quota, limites et RoleBinding : uniquement dans un namespace Agent Ready"

La vérification de oldObject empêche un détournement simple. Sans elle, le pipeline pourrait poser agentic.io/agent-ready=true sur le namespace kagent ou agentgateway-system, puis y créer ce qu'il veut. Comme l'objet existant doit déjà être dans le périmètre, ce réétiquetage échoue. La deuxième validation protège les clés de la plateforme : dans agentgateway-system, le pipeline n'atteint que les Secrets vk-* marqués par Agent Ready.

kubectl auth can-i ne teste que RBAC, pas l'admission. Pour tester le périmètre, faites un dry-run côté serveur en vous faisant passer pour l'identité du pipeline, avec le kubeconfig admin. Sur AKS avec Entra ID, Kubernetes connaît une identité managée par son object ID :

$oid = az identity show -g <rg-plateforme> -n id-<base>-ai-team --query principalId -o tsv

# Doit passer : un namespace d'équipe correctement marqué
@'
apiVersion: v1
kind: Namespace
metadata:
  name: equipe-test
  labels: { agentic.io/team: equipe-test, agentic.io/agent-ready: "true" }
'@ | kubectl apply --dry-run=server --as=$oid -f -

# Doit être refusé : « namespace hors périmètre Agent Ready »
kubectl label namespace kagent agentic.io/agent-ready=true --dry-run=server --as=$oid

# Doit être refusé : Secret de la plateforme
kubectl -n agentgateway-system create secret generic vk-platform --from-literal=x=y `
  --dry-run=server --as=$oid

Dans les pipelines, affichez kubectl auth whoami. C'est le moyen le plus rapide de confirmer que Kubernetes voit bien l'object ID attendu par la matchCondition et par le RoleBinding.

Ce que l'équipe déploie

Dans son namespace, l'équipe reçoit la ClusterRole agentic-team-deployer : Deployments, Services, ConfigMaps, NetworkPolicies et les ressources kagent agents, remotemcpservers et modelconfigs. Elle n'a accès ni aux Secrets, ni au RBAC, ni aux namespaces.

Pour l'équipe, ajouter un agent revient à ouvrir une pull request. Le ModelConfig pointe vers la passerelle, avec la clé créée par Agent Ready :

Fichiers : terraform/teams/template/agent/modelconfig.yaml et agent.yaml

apiVersion: kagent.dev/v1alpha2
kind: ModelConfig
metadata:
  name: passerelle-llm
spec:
  provider: OpenAI
  model: llm                     # ignoré : la passerelle choisit le déploiement
  apiKeySecret: gateway-key
  apiKeySecretKey: key
  openAI:
    baseUrl: http://agentgateway-proxy.agentgateway-system.svc.cluster.local/agents/v1
---
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
  name: assistant-docs
spec:
  description: Répond aux questions de l'équipe à partir de ses documents
  type: Declarative
  declarative:
    modelConfig: passerelle-llm
    systemMessage: |
      Réponds en français, uniquement à partir des documents de l'équipe :
      cherche d'abord avec « rechercher », lis avec « lire_document »,
      et cite toujours le document source.
    tools:
      - type: McpServer
        mcpServer:
          apiGroup: kagent.dev
          kind: RemoteMCPServer
          name: docs-mcp
          toolNames: [lister_documents, lire_document, rechercher]

Le serveur MCP de l'équipe lit son stockage par Workload Identity, sans clé :

Fichier : terraform/teams/template/mcp/server.py

container = ContainerClient(
    account_url=f"https://{ACCOUNT}.blob.core.windows.net",
    container_name=CONTAINER,
    credential=DefaultAzureCredential(exclude_interactive_browser_credential=True),
)
mcp = FastMCP("docs-equipe", host="0.0.0.0", port=8080, stateless_http=True)

@mcp.tool()
def lire_document(nom: str) -> str:
    """Renvoie le contenu d'un document (nom exact, tel que listé)."""
    docs = _documents()                      # cache de 60 s sur le conteneur
    if nom not in docs:
        return f"Document introuvable : {nom}. Documents : {', '.join(sorted(docs))}"
    return docs[nom][:MAX_CHARS]

Pour publier ce serveur sur la passerelle, l'équipe n'ouvre aucun ticket. Il suffit d'un label, d'un appProtocol et de deux annotations sur le Service :

Fichier : terraform/teams/template/deploy/docs-mcp.yaml

apiVersion: v1
kind: Service
metadata:
  name: docs-mcp
  labels:
    agentic.io/publier-mcp: "true"
  annotations:
    agentgateway.dev/mcp-path: /mcp
    agentgateway.dev/mcp-target-name: docs-equipe-docs
spec:
  selector: { app: docs-mcp }
  ports:
    - name: mcp
      port: 8080
      targetPort: mcp
      appProtocol: agentgateway.dev/mcp

Côté plateforme, un seul backend découvre ces Services dans tous les namespaces d'équipe :

Fichier : gateway/deploy/backend-mcp.yaml

apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
  name: mcp-equipes
spec:
  mcp:
    prefixMode: Always      # docs-equipe-docs_rechercher : on voit quelle équipe sert l'outil
    failureMode: FailOpen   # le serveur en panne d'UNE équipe ne coupe pas les autres
    targets:
      - name: equipes
        selector:
          namespaces:
            matchExpressions:
              - { key: agentic.io/team, operator: Exists }
          services:
            matchLabels:
              agentic.io/publier-mcp: "true"

Dans LibreChat, les utilisateurs voient sur /mcp-equipes les outils de toutes les équipes, préfixés par le nom de l'équipe, avec leur jeton Entra ID. Le label agentic.io/team posé par Agent Ready sert à la fois au périmètre d'admission et à cette découverte.

Le modèle de dépôt rendu par Terraform

Le provider GitHub crée le dépôt agent-<équipe> depuis le dossier terraform/teams/template. Les variables y sont des marqueurs __EQUIPE__, __STORAGE__ ou __CLIENT_ID__ plutôt que ${}, pour ne pas entrer en collision avec ${{ }} des workflows ni avec ${VAR} du shell.

Deux règles de cycle de vie évitent les mauvaises surprises. Avec lifecycle { ignore_changes = [content] }, les fichiers appartiennent à l'équipe une fois créés, et relancer Agent Ready n'écrase ni son agent ni ses documents. Les workflows sont écrits en dernier (depends_on sur le contenu et les variables), pour qu'aucun run ne parte pendant que le dépôt se remplit.

Les limites que j'assume

Le pipeline lit les Secrets. Terraform relit ce qu'il gère, d'où le verbe get, et l'admission ne filtre pas les lectures ; le pipeline voit donc les Secrets des namespaces d'équipe.

L'état Terraform contient les clés. Un état par équipe limite l'exposition, mais l'accès au stockage d'état doit rester réservé à l'équipe IA.

AcrPush est global au registre, si bien qu'une équipe peut pousser une image sous le nom d'une autre. Les permissions par dépôt d'ACR, ou un registre par équipe, ferment ce point.

Un rôle tout juste attribué peut mettre quelques minutes à s'appliquer ; les pipelines réessaient au lieu d'échouer. Enfin, l'authentification Entra ID d'AKS ne se désactive plus une fois activée. Terraform et le présentateur gardent le kubeconfig admin, et les équipes passent par kubelogin.

Le code

Le prochain article présente un agent d'exploitation qui s'appuie sur les MCP d'entreprise, avec approbation humaine.

About the author

  • Ayant eu de multiples expériences dans le monde du conseil, j'ai pu acquérir une expertise dans la conception et la construction de services de collaboration d'entreprise. Je suis passionné par les technologies Microsoft et surtout Azure. Aujourd'hui je possède une solide expérience en méthodologie de développement et j'ai mené des équipes de développement technique au succès. Je possède également une solide connaissance de l'infrastructure qui fait de moi une ressource efficace pour mettre en œuvre la transformation numérique vers le cloud Microsoft. Je suis un professionnel efficace et honnête qui aime relever les challenges. Aimant partager mes connaissances; je suis à l'aise en tant que Technical Leader et en tant que membre d'une équipe. Mes compétences techniques sont les suivantes: Azure, DevOps, Architecture Applicative, Développement de solution Cloud Native, écosystème Microsoft... et bien d'autres. Pour voir mes certifications Microsoft : https://www.youracclaim.com/users/antoine-loizeau

No comments yet. Be the first to comment.

IA agentique : vers Azure et au-delà !

Part 3 of 5

Série d'article tirée du talk « IA agentique : vers Azure et au-delà ! » (Chalet Azure Romandie, Genève, 29 octobre 2026). Tout le code est dans le dépôt aloizeau/agentic-ai-azure-and-beyond-geneve-2026.

Up nextGarde-fous IA portables : NeMo Guardrails derrière agentgateway, en fail-closedDeuxième article de la série tirée du talk « IA agentique : vers Azure et au-delà ! » (Genève, 29 octobre 2026). Code complet dans aloizeau/agentic-ai-azure-and-beyond-geneve-2026, dossier guardrails/