5.1 Identity Across Clouds¶
Inside one Google Cloud organization, identity is easy to take for granted. When cx-mortgage-orchestrator calls ai-coe-mortgage-specialist, the Central Agent Gateway sees an attested SPIFFE principal:// identity, and IAM sees a Google-issued token. Both were minted by the same platform that checks them.
Now picture a new caller: an underwriting agent that a risk team runs on another cloud. It has an identity there (an IAM role, a managed identity, a Kubernetes service account), but Google Cloud did not issue it. Before that agent may touch Julian Sterling's file, Esmeralda needs to answer three questions:
- Can the identity be verified cryptographically, without a shared secret or a long-lived key?
- Which team does it belong to, so the call can be authorized and billed?
- What happens when either answer is missing?
The short answers: federated, signed tokens verified at the gateway; the catalog; and the call is denied.
1. The Rule: Attribution Comes from Authentication¶
Every request can carry two kinds of information about its caller:
| Kind | Example | Trusted For |
|---|---|---|
| Verified identity | A JWT signed by the caller's cloud and validated against that issuer's published keys; a federated principal minted by Google's Security Token Service; a client certificate chained to a trusted CA. | Authorization and chargeback. |
| Declared context | A header or A2A metadata field saying cost_center: cc-underwriting-example or agent_name: underwriter. |
Telemetry and reporting only, and only when the catalog allows that value for the verified caller. |
Esmeralda never bills or authorizes on declared context alone. Self-asserted headers (such as an agent name in X-API-Key, or a SPIFFE ID copied into a custom header) stay telemetry-only. This is what makes cross-team, and now cross-cloud, chargeback impossible to fake.
2. Four Ways a Workload on Another Cloud Proves Who It Is¶
flowchart TB
subgraph Caller["Workload on another cloud"]
W["Underwriting agent"]
end
W -->|"A. cloud OIDC token"| WIF["Workload Identity Federation<br/>(Google STS token exchange)"]
W -->|"B. cloud OIDC token"| GWJWT["Gateway verifies JWT<br/>(issuer JWKS, audience)"]
W -->|"C. SPIFFE SVID"| SPF["SPIFFE federation<br/>(exchanged trust bundles)"]
W -->|"D. client certificate"| MTLS["mTLS at a gateway<br/>(trusted CA)"]
WIF --> P1["principal://iam.googleapis.com/.../subject/SUB"]
GWJWT --> P2["ISSUER#SUBJECT"]
SPF --> P3["spiffe://TRUST_DOMAIN/PATH"]
MTLS --> P4["certificate subject / SAN"]
P1 & P2 & P3 & P4 --> CAT["Catalog caller<br/>(identity → team)"]
| Pattern | How It Works | Verified Principal | Best Fit |
|---|---|---|---|
| A. Workload Identity Federation | The workload exchanges a token from its own cloud (for example an AWS or Azure workload credential) with Google's Security Token Service through a workload identity pool and provider. It then calls Google Cloud APIs with a short-lived federated token, either directly or by impersonating a dedicated service account. | principal://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL>/subject/<SUBJECT> |
Callers that need Google Cloud IAM directly (Cloud Run invoker, Vertex AI, BigQuery, the Telemetry API). |
| B. OIDC verified at the gateway | The workload sends a JWT from its cloud's OIDC issuer. The gateway looks the issuer up in an allowlist, fetches its JWKS, and verifies signature, issuer, audience and expiry. | <issuer>#<subject> |
Inbound agent calls through Apigee (the cross-cloud best path), or Envoy with JWT verification. |
| C. SPIFFE federation | Both sides run SPIFFE-compatible identity and exchange trust bundles, so each side can verify the other's SVIDs. | spiffe://<trust-domain>/<path> |
Organizations that already standardized on SPIFFE across clouds and clusters. |
| D. mTLS | The caller presents a client certificate at a gateway that terminates TLS and validates the chain against a trusted CA. | Certificate subject or SAN | Legacy or partner systems that cannot mint OIDC tokens. |
Tip
Prefer keyless patterns (A, B, C). They rely on short-lived tokens signed by an identity provider you already operate, so there is no long-lived secret to rotate or leak. Use mTLS (D) when a system can only present certificates, and terminate it at a gateway rather than inside agent code.
Why issuer#subject and Not Just subject¶
A JWT sub claim is only unique within its issuer. Two clouds, or two tenants of the same cloud, can both issue a token with sub: underwriter. If the gateway keyed callers on sub alone, a workload from the wrong issuer could inherit another team's grants and bill another team's cost centre.
Esmeralda therefore keys OIDC callers on the pair: the gateway composes caller.identity = <iss>#<sub> from the verified claims, and the catalog registers the same (issuer, subject) pair. Workload Identity Federation principals already encode both the pool (the trust relationship) and the subject in one string, so they need no extra composition.
3. Verifying an OIDC Caller at Apigee: The Cross-Cloud Shared Flow¶
When governance.capabilities.cross_cloud = "apigee", the Layer 5 Apigee cross-cloud unit manages an allowlist of trusted external issuers in an Apigee KeyValueMap (kvm-cross-cloud-issuers). Each entry maps a short issuer slug to three values: the issuer URI, the JWKS URI, and the expected audience:
# Illustrative: trusted issuers for the Apigee cross-cloud unit (synthetic values)
trusted_issuers = {
example-cloud = {
issuer_uri = "https://oidc.example.com"
jwks_uri = "https://oidc.example.com/.well-known/jwks.json"
audience = "https://partner.esmeralda.internal"
}
}
The cross-cloud shared flow turns that allowlist into a fail-closed verification step that the partner proxies call before anything else is trusted:
sequenceDiagram
autonumber
participant C as Workload on another cloud
participant P as Apigee partner proxy<br/>(partner-agents / partner-mcp)
participant SF as cross-cloud shared flow
participant KVM as kvm-cross-cloud-issuers
participant T as Target agent or MCP server
C->>P: Request + x-api-key + x-cross-cloud-issuer: example-cloud + x-cross-cloud-token: JWT
Note over P: AM-Strip-Verified-Headers removes any client-supplied x-verified-* headers
P->>P: VerifyAPIKey (app, developer, product)
P->>SF: FlowCallout (header present, or product/app requires cross-cloud)
SF->>SF: Missing issuer or token? → 401
SF->>KVM: Look up issuer_uri, jwks_uri, audience for the slug
SF->>SF: Unknown issuer? → 403
SF->>SF: VerifyJWT (signature via JWKS, issuer, audience, expiry)
SF->>SF: Invalid token, or empty iss / sub? → 401
SF->>P: caller.identity = iss#sub, raw token removed
P->>P: App owner_identity ≠ caller.identity? → 403
P->>T: Forward (Apigee's own Google credential), x-verified-identity set by the gateway
P-->>P: ML-interaction record with caller_identity + caller_issuer
Three details make this path trustworthy:
- Spoof-proof headers. The proxy strips every client-supplied
x-verified-*header first, so the onlyx-verified-identitythat can reach a target is the one the gateway set after verification. - Key-to-identity binding. A partner API key alone is not enough. When the developer app carries an
owner_identityattribute, the proxy rejects the call (403) unless it matches the verifiediss#sub. A leaked key is useless without a valid token from the right workload. - The raw token stops at the gateway. The shared flow removes the cross-cloud JWT before forwarding. Downstream services receive the verified identity, never the credential.
Info
The target behind Apigee (for example, an agent on Agent Runtime) authenticates Apigee's service identity, not the original caller's. That is by design: the gateway is the verification point, and the gateway's ML-interaction record is the source of truth for attribution on this path. Targets should not try to re-derive the caller from forwarded headers.
4. Registering a Federated Caller in the Catalog¶
Verification answers "is this token genuine?" The catalog answers "who is this, and what may they do?" Every non-workload identity (a VM, a human group, a partner Apigee app, a SPIFFE ID, or a workload on another cloud) is declared under callers, mapped to the team that pays for its calls.
A workload on another cloud is declared with identity type federated and its (issuer, subject) pair. In the team-owned catalog/ layout, such entries live under catalog/external/clouds/, which platform and security own:
# catalog/external/clouds/example-cloud-underwriter.yaml (synthetic)
schema: esmeralda.catalog
schema_version: 1
teams:
example-cloud-underwriting:
description: Synthetic underwriting team running on another cloud.
cost_centers: [cc-underwriting-example]
country: BR
external: true
callers:
aws-underwriter:
team: example-cloud-underwriting
description: Underwriting workload on another cloud (federated identity).
identity:
type: federated
issuer: https://oidc.example.com
subject: example-underwriter-role
The target owner then decides what that caller may do. The AI CoE grants it access to its specialist, through Apigee, under a specific API product and quota:
# catalog/teams/ai-coe/agents/ai-coe-mortgage-specialist.yaml (excerpt)
callers:
cx-mortgage-orchestrator: { cost_centers: [cc-cx-br, cc-cx-mx] }
aws-underwriter: { gateway: apigee, product: gold, quota: { tokens_per_minute: 50000 } }
The other identity types follow the same shape:
Identity type |
Required Fields | Example |
|---|---|---|
federated |
issuer, subject |
An OIDC workload on another cloud |
spiffe |
ref (unit output) or id |
spiffe://example.org/ns/risk/sa/underwriter |
apigee_app |
id (and optional product) |
A partner developer app |
service_account |
ref (unit output) or id |
A test VM or a platform workload |
group |
id |
An operator group |
Identities the platform creates are referenced from Terraform unit outputs (ref: layer-3-security:<output>), never pasted by hand. Identities owned elsewhere, such as an external issuer, are written as literals and reviewed by platform and security through CODEOWNERS.
Note
The team-owned catalog/ folder, the federated identity type and esmeralda catalog resolve / show are on main (2.6 Service Catalog). Today only agents and service_account callers are compiled into enforcement (IAM grants); federated callers are resolved and shown, and compiling them into Apigee products and apps is planned. See 5.5 Status & Roadmap.
5. Fail Closed, at Every Step¶
Every way a cross-cloud call can go wrong ends in a denial, never in an "allowed but unattributed" request:
| Situation | Result | Where It Is Enforced |
|---|---|---|
| No credentials at all | 401 |
Every hop: IAM, IAP, Apigee keys |
| Issuer header or token missing where cross-cloud is required (by header, product or app attribute) | 401 |
Apigee cross-cloud shared flow |
| Issuer not in the trusted allowlist | 403 |
Apigee cross-cloud shared flow |
Bad signature, wrong audience, expired token, or empty iss / sub |
401 |
Apigee cross-cloud shared flow |
Valid key but owner_identity does not match the verified iss#sub |
403 |
Apigee partner proxy |
Client tries to inject x-verified-identity |
Header stripped; the binding check then fails | Apigee partner proxy |
| Authenticated caller not in the catalog | Denied (policy.attribution.unmapped_caller: deny) |
Gateway grants compiled from the catalog |
| Declared cost centre not allowed for the caller | Rejected or billed to the caller's own team (policy.attribution.declared_mismatch) |
Attribution views |
The last two rows describe the catalog policy:
# catalog/policy.yaml (excerpt)
policy:
attribution:
unmapped_caller: deny # deny | flag (allow + alert, e.g. a dev sandbox)
declared_mismatch: reject # reject | bill_own_team
Because enforcement is compiled from the catalog, unmapped_caller: deny is a property of the build: a caller absent from the catalog never receives a gateway grant, an API product, or an IAM binding in the first place. And because being listed is what makes a caller billable, the "unattributed" count in the usage views has an expected value of zero, with an alert when it is ever greater (5.3).
6. The Outbound Direction: Esmeralda Agents Calling Other Clouds¶
Identity also matters when ai-coe-mortgage-specialist calls out to a service on another cloud. The remote side wants proof of who is calling, and it should not have to trust a static API key.
The keyless pattern mirrors pattern A in reverse:
- The agent obtains a Google-signed OIDC ID token for its own service identity, with the remote service as the audience.
- The remote cloud trusts Google as an OIDC identity provider and exchanges that token for its own short-lived credential (for example, assuming a role with a web identity, or a federated credential on an application registration). The trust policy pins the expected audience and the agent's subject.
- The agent calls the remote service with the remote credential.
The Central Agent Gateway governs every one of those calls: the remote service and the remote cloud's token-exchange endpoint must both be registered in the Agent Registry, or the gateway blocks them. Tokens travel as ordinary HTTP headers, so they pass through the gateway's inspection unchanged.
Tip
Give each agent its own outbound identity. When several agents share one service account for outbound calls, no callee, on any cloud, can tell them apart, and per-agent attribution is lost. Per-agent identities are a prerequisite for verified attribution and are on the roadmap (5.5).
Next Subsection: The caller is verified. Now, which path does its request take between clouds, and which gateway sits on each hop? Continue to 5.2 Cross-Cloud Connectivity & Protocols.