As of November 2026. All identifiers in this post are examples.
We built a proof of concept with two AI agents. Agent A runs on Azure, in Microsoft Foundry. Agent B runs on Google Cloud, in Agent Runtime. They talk to each other over the A2A protocol, in both directions.
Neither agent stores a password, a service account JSON key or a client secret. This post shows how that works, with the actual requests.
The problem with the usual approach
The quick way to connect agents across clouds looks like this:
- The Google Cloud agent gets an Azure client secret, so it can ask Entra for a token.
- The Azure agent gets a Google service account key, so it can call Google APIs.
Now each cloud holds a long-lived credential for the other one. Someone has to rotate them. Each one can leak through a log, a repository or a container image. And with a hundred agents you have a few hundred secrets.
We wanted a design where there is nothing to rotate, because nothing is stored.
The idea
Two rules carry the whole design.
- Each cloud proves who the workload is. The agent never holds a credential. The platform it runs on vouches for it.
- One authority decides what the workload may do. We chose Microsoft Entra. People sign in with Entra ID. Agents get identities in Entra Agent ID.
The piece that joins the two rules is workload identity federation. Entra is told: "trust tokens signed by this Google issuer, for exactly this subject". If you have connected GitHub Actions to a cloud without a secret, this is the same pattern.
What makes it work for agents is that Google gives every agent in Agent Runtime its own identity, expressed as a SPIFFE ID:
spiffe://agents.global.org-123456789012.system.id.goog/resources/aiplatform/ projects/987654321098/locations/europe-west1/reasoningEngines/7300112233445566778 That ID names one agent, in one project, in one region. Google signs tokens that carry it.
The Entra side: a blueprint and one federated credential
In Entra Agent ID, an agent identity is created from a blueprint. The blueprint is where credentials live. The agent identity itself cannot hold one, which is a useful property: nobody can attach a password to it later.
On the blueprint we add a single federated identity credential:
{ "name": "gcp-agent-spiffe", "issuer": "https://sts.googleapis.com/v1/organizations/123456789012/locations/global/workloadIdentityPools/agents.global.org-123456789012.system.id.goog", "subject": "spiffe://agents.global.org-123456789012.system.id.goog/resources/aiplatform/projects/987654321098/locations/europe-west1/reasoningEngines/7300112233445566778", "audiences": ["api://AzureADTokenExchange"] } Entra will accept a token as proof of identity only if the issuer, the subject and the audience match this record exactly, and the signature checks out against Google's public keys. There are no wildcards.
The exchange, hop by hop
Hop 1: Google proves who the agent is
Inside the agent, one call to the Google auth library:
from google.auth.transport.requests import Request from google.oauth2 import id_token google_jwt = id_token.fetch_id_token(Request(), "api://AzureADTokenExchange") The agent asked for nothing secret. The runtime returns a JWT signed by Google, with the SPIFFE ID as its subject and the audience we requested.
Hop 2: the blueprint vouches for the agent
The agent sends that JWT to Entra in place of a client secret:
POST https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token grant_type=client_credentials &client_id=<BLUEPRINT_APP_ID> &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=<google_jwt> &scope=api://AzureADTokenExchange/.default &fmi_path=<AGENT_IDENTITY_ID> Entra finds the federated credential, validates the Google signature, and checks that the agent identity in fmi_path belongs to this blueprint. It returns an intermediate token that is only good for the next hop.
Hop 3: the agent gets a token for one gateway
POST https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token grant_type=client_credentials &client_id=<AGENT_IDENTITY_ID> &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=<intermediate token> &scope=api://a2a-azure-gateway/.default This is where authorization starts. The gateway application in Entra requires a role assignment. If the agent has no role there, Entra answers AADSTS501051 and issues nothing.
If it has one, the access token looks like this:
{ "iss": "https://login.microsoftonline.com/<TENANT_ID>/v2.0", "aud": "<a2a-azure-gateway app id>", "azp": "<AGENT_IDENTITY_ID>", "roles": ["A2A.Invoke.agent-a"] } One audience, one role, valid for about an hour. The agent caches it and calls the gateway with it.
Each token in the chain is narrower than the one before it:
In Python this is about twenty lines of plain HTTP. We did not use MSAL, because it does not expose fmi_path.
Authorization: three locks
Authenticating is not enough to get in. A request has to pass three independent checks.
Lock 1, Entra. Each target agent has its own role on the gateway application, such as A2A.Invoke.agent-a. The role is assigned only to the agents allowed to call that target. No role, no token.
Lock 2, the gateway. API Management on Azure and Apigee on Google Cloud both validate the signature, the tenant, the audience and the expiry. Then they check that the token carries the role for the route being called, that the caller is on the allow-list for that route, and that it is not on the deny-list. The Apigee version of that check is small:
var ok = has(roles, cfg.role) // the role for THIS target && has(cfg.allowed_callers, azp) // the caller is expected here && !isolated; // and nobody pulled the plug A role for agent X gives no access to agent Y behind the same gateway.
Lock 3, the backend. The gateway drops the caller's token and calls the agent with its own identity. On Azure, only the gateway's managed identity may invoke the Foundry agent. On Google Cloud, only the Apigee service account may query the Agent Runtime engine. A call that goes around the gateway bounces, even with a valid Entra token.
The other direction
Agent A calling Agent B is the same design with less visible plumbing. Foundry already holds a federated credential for its agents, so the code is one line:
token = DefaultAzureCredential().get_token("api://a2a-gcp-gateway/.default") Apigee validates that Entra token with the same three checks, then calls Agent B with its own service account.
Both directions end up symmetrical: an Entra token for the gateway of the other cloud, and a gateway that is the only door to its agent.
Pulling the plug
Entra access tokens live for about an hour, so removing a role does not stop an agent right away. For that there is a deny-list in both gateways. It holds the agent's ID as a caller and its name as a target, so an isolated agent can neither call nor be called.
In our rehearsals the first refusal arrived in under a second. The worst case is bounded by a ten-second cache in the gateway.
What we learned the hard way
- Never delete and recreate the Google agent. Its engine ID is part of the SPIFFE ID. A new agent has a new subject, and the federated credential stops matching. Update it in place.
-
AADSTS70021means the federated credential does not match. Check the issuer, subject and audience character by character, including a trailing slash. Also wait a little: new credentials take time to propagate. - Authorize on
azpandroles. Tokens also carry a claim for the parent blueprint. Do not use it for access decisions. - Quotas shape the design. An application accepts at most 20 federated credentials, and a blueprint at most 250 agent identities. Subject patterns are not available for the Google issuer, so each agent needs its own exact credential.
- Automate onboarding. In our setup an agent is one entry in an inventory file. A pipeline creates the blueprint, the federated credential, the agent identity, the roles and the gateway allow-lists. With the runtime already deployed, that run took 22.8 seconds.
What this design asks of you
No stored secret does not mean no responsibility. Two things take the place of the secrets.
The control planes become the credential. Whoever can update the agent's code on Google Cloud can act as the agent. Whoever can edit the blueprint in Entra can add a federated credential of their own. Keep both permissions with very few identities, and alert on changes to federated credentials.
Use one blueprint per agent. The federated credential lives on the blueprint, and a workload that authenticates as the blueprint can ask for a token for any agent identity under it. With one blueprint per agent, a compromised agent can only ever be itself.
Wrapping up
The result is a small set of moving parts:
- The platform proves the workload.
- One federated credential connects that proof to Entra.
- One role per target says who may call whom.
- The gateway enforces it, and the backend trusts only the gateway.
Nothing in that list has to be rotated.
In part 2 we put a model gateway on top of this: one LiteLLM per cloud, where agents use the same Entra token to reach Gemini and Azure OpenAI, again without API keys.





