Cloud federation
For: cluster operators.
Configure the cloud's trust before using a federated provider or tool. The edge exchanges the calling agent's identity for a short-lived cloud credential.
AWS, Google Cloud and Microsoft Entra can trust an outside OIDC issuer instead of a key you store. Each tenant has one on the edge (identity for outside services), so a cloud can trust the tenant's agents directly:
- A model call or a tool call needs the cloud. The edge mints the calling agent's workload token for the cloud's
token service (five minutes, one audience, the agent as
sub, never a person). - The edge exchanges it at the cloud: AWS STS
AssumeRoleWithWebIdentity, Google Cloud's security token service (and, if you name one, the service account it impersonates), or Entra's token endpoint with the token as the client assertion of an app registration's federated credential. - The cloud checks the token through the issuer's discovery document and key set, against the trust you set up below, and answers a credential that lives an hour.
- For a model call, the edge signs the call with it and nothing else sees it. For a tool call, the credential goes to the node sealed to it, and only the node's egress proxy opens it: it signs the tool's AWS requests or puts the Google or Entra token on, while the tool holds a placeholder.
Nothing is stored: the edge keeps a credential in memory for the job's next calls until a few minutes before it ends.
A credential lives as long as the cloud says (expires_in, or AWS's Expiration), at most an hour: an answer that
gives no lifetime, or one that has already ended, is an error, never a credential. When the edge's memory for
credentials is full of live ones, a new call's credential serves that call alone, and no other job's is dropped for it.
A cloud's refusal (a trust policy, an audience: a 4xx answer) is told apart from an outage (no answer, a 5xx, 408 or
429), which may pass: the refusal names the cloud's own error code, and an outage is audited as a failed exchange
(token.exchange_refused, marked unavailable) with its cause.
The audit log records each exchange as token.exchanged (the cloud, the role, pool or application it reached, the
agent and its subject, the job), never a token or a credential.
Only the agent's own plugin's tools get its cloud credentials: a tool of another plugin that the agent was granted gets none.
Nothing is sent to a cloud until a tenant admin sets up a federated provider or installs a plugin with a federated connection.
What the clouds see
Issuer (iss) | https://<edge>/oidc/<tenant>, from oz0 tenant oidc show <tenant> |
Subject (sub) | oz0:agent:<tenant>:<agent>:<install generation>: it survives upgrades of the plugin and changes when the plugin is removed and installed again |
Audience (aud) | AWS: https://sts.amazonaws.com; Google: https://iam.googleapis.com/ and the pool provider's name; Entra: api://AzureADTokenExchange. A provider or connection may name another (audience) |
| AWS session tag | oz0-agent: the agent's name, in the token's https://aws.amazon.com/tags claim |
| AWS session name | oz0- and the job's ID, so CloudTrail names the job |
The clouds' endpoints are fixed: sts.<region>.amazonaws.com, sts.googleapis.com and
iamcredentials.googleapis.com, login.microsoftonline.com. A manifest or a tenant names the role, the pool or the
application, never where the agent's token goes. An operator can replace an endpoint, for a sovereign cloud, with the
server's --cloud-endpoint aws-sts=<URL> (also gcp-sts, gcp-iam, entra).
AWS
- An IAM OIDC identity provider for the issuer: in IAM, add an OpenID Connect provider with the provider URL set to
the tenant's issuer and the audience
https://sts.amazonaws.com. AWS fetches the discovery document over the internet; for a private cluster, publish it first. - A role whose trust policy names the agent, by
audandsub, and allows the session tag:trust-policy.json{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Federated": "arn:aws:iam::123456789012:oidc-provider/oz0-edge.acme.example/oidc/acme"}, "Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"], "Condition": { "StringEquals": { "oz0-edge.acme.example/oidc/acme:aud": "https://sts.amazonaws.com", "oz0-edge.acme.example/oidc/acme:sub": "oz0:agent:acme:triage:42" } } }] }sts:TagSessionis required: every token carries the agent as a session tag, and AWS refuses the role without it.
The subject ends in the agent's install generation (42here; every exchange's audit entry, refused ones too, names the full subject:oz0 audit --action token.), soStringEqualson it trusts one install of one agent. After a reinstall the agent has a new subject, and you update the policy on purpose.
Do not match the generation with a wildcard (StringLikeonoz0:agent:acme:triage:*): any later agent namedtriagein the tenant, from any plugin and any publisher, would then assume the role, which is what the generation in the subject is there to prevent. For the same reasonaws:PrincipalTag/oz0-agent(the agent's name alone) may narrow a policy but never replaces the subject. - A model provider for Bedrock:Terminal
oz0 provider set bedrock --preset bedrock-federated --base-url https://bedrock-runtime.eu-north-1.amazonaws.com \ --aws-role arn:aws:iam::123456789012:role/oz0-agents --aws-region eu-north-1 --region eu
or a connection for tools (in this guide).
Google Cloud
- A workload identity pool and an OIDC provider for the issuer: the issuer URI is the tenant's issuer; map
google.subjecttoassertion.suband, if you like,attribute.agenttoassertion.sub; keep the default allowed audience (https://iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>). Google can take the key set as an upload instead of fetching it:oz0 tenant oidc jwks <tenant> > jwks.json, then give the file to the provider (--jwk-json-pathingcloud), and upload it again after every rotation. - Access for the agent, one of the following, always by the full subject (with its install generation): a
principalSetover an attribute without the generation, such asattribute.agentmapped from the agent's name, trusts every later agent of that name in the tenant.- grant roles to the principal
principal://iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/subject/oz0:agent:acme:triage:<generation>directly, or - grant it
roles/iam.workloadIdentityUseron a service account, and name that service account: the edge then exchanges the federated token for the service account's own (generateAccessToken, an hour).
- grant roles to the principal
- A model provider for Vertex AI:Terminal
oz0 provider set vertex --preset vertex-federated --base-url https://europe-west1-aiplatform.googleapis.com --region eu \ --gcp-workload-provider //iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/oz0/providers/acme \ --gcp-service-account [email protected]
Microsoft Entra
- An app registration (or a user-assigned managed identity) with a federated credential: issuer = the
tenant's issuer, subject identifier = the agent's
sub, audience =api://AzureADTokenExchange. Entra matches all three exactly and takes no wildcards, so each agent needs a federated credential of its own, and an app registration or managed identity holds at most 20: one app serves at most 20 agents (use more apps for more). Entra fetches the discovery document over the internet.Microsoft supports workload identity federation only with issuers that sign their tokens with RS256; tokens signed with other algorithms "may work, but haven't been tested". The tenant's issuer signs with ES256, which AWS and Google document as supported. Entra's path is tested here only against a stand-in of Entra's token endpoint and has not yet run against Microsoft's: treat it as unproven until it has. - Access for the app: a role on the Azure OpenAI or AI Foundry resource (Cognitive Services OpenAI User, for example).
- A model provider:Terminal
oz0 provider set foundry --preset azure-foundry-federated --base-url https://acme.services.ai.azure.com/anthropic \ --entra-directory acme.onmicrosoft.com --client-id 11112222-bbbb-3333-cccc-4444dddd5555 --region eu
(azure-openai-federatedfor Azure OpenAI's v1 API.)
Where a provider's token may go
A Google or Entra token is a bearer token: whoever receives it can call the cloud as the agent until it ends. So a
federated provider's --base-url must be a host of its cloud's own model API, on https's port:
| Cloud | Hosts |
|---|---|
| Google Cloud | aiplatform.googleapis.com, <region>-aiplatform.googleapis.com, generativelanguage.googleapis.com |
| Entra | None of its own: only the resources the operator names, below |
provider set refuses any other address, for the record and for each pool member, and the edge checks the
address again at every call, before it asks the cloud for a token; the call follows no redirect. A host
outside the list (a sovereign cloud's, a private endpoint's, a test's fake) is one the operator names for the cloud
on the servers: --cloud-endpoint entra-models=https://aoai.private.acme.example (or gcp-models=...), repeatable.
A record matches such a host only with the same scheme, host and port, so http://aoai.private.acme.example:443 is
not https://aoai.private.acme.example and is refused: the token never goes out in clear text.
Azure's model hosts (<resource>.openai.azure.com, <resource>.services.ai.azure.com,
<resource>.cognitiveservices.azure.com) are names any Azure customer can register, so such a name says Microsoft runs
the host, not that the resource is yours: someone who may write a provider record could otherwise point a federated
provider at a resource of their own.
So an Entra record needs its host named on the servers, on management and on the
edges alike: --cloud-endpoint entra-resources=https://acme-ai.openai.azure.com, repeatable. An Entra token goes to
those and to entra-models hosts, and to no other host. With neither named, provider set refuses the record, the edge
refuses every call of one already saved, and the error names the flag to set.
An Entra record's auth.scope must be a scope of Azure's model API (https://cognitiveservices.azure.com/.default or
https://ai.azure.com/.default, or a sovereign cloud's), so a record cannot ask for a token of another service; a host
named with entra-models answers for itself, and takes the scope its record names.
An AWS session signs each request for the host it reaches, which no other host can use, so a Bedrock record's address is not limited this way.
In a plugin
A tool reaches a cloud through a connection of kind federated, and runs under bindings of it: its credential reaches
the node only sealed, so a connection without bindings, or a remote tool, gets nothing.
egress: ["*.s3.eu-north-1.amazonaws.com", "storage.googleapis.com"]
requires:
inject:
aws:
- {host: "*.s3.eu-north-1.amazonaws.com", sigv4: {service: s3, region: eu-north-1}}
gcs:
- {host: storage.googleapis.com, header: Authorization, format: "Bearer {secret}"}
connections:
- name: aws
description: The tenant's AWS account, by the agent's own identity
tools: [files]
params:
- {name: role_arn, description: "The IAM role the agents assume"}
- {name: region, description: "The role's region", default: eu-north-1}
token:
kind: federated
federated: {cloud: aws, role_arn: "{params.role_arn}", region: "{params.region}"}
pass: {region: "{params.region}"}
scopes:
- functions: [files.put, files.get]
- name: gcs
description: The tenant's Google Cloud project
tools: [files]
params:
- {name: provider, description: "The workload identity pool provider"}
token:
kind: federated
federated: {cloud: gcp, workload_provider: "{params.provider}"}
scopes:
- functions: [files.copy_to_gcs]
federated | |
|---|---|
cloud | aws, gcp or entra |
role_arn, region | AWS: the role and the region of its STS endpoint |
workload_provider, service_account | Google Cloud: the pool provider's resource name, and a service account to impersonate |
directory, client_id | Entra: the directory and the application with the federated credential |
scope | What a Google or Entra token is for (Google's default: cloud-platform) |
audience | The workload token's audience, when the cloud's trust names another than the default |
Each field is a value or a template of the connection's params; a tenant admin sets the params, never a secret.
plugin lint refuses a federated AWS connection bound without sigv4, a Google or Entra one bound with it, and a
connection with no bindings at all.
In the tool, the call's _meta["oz0/connections"]["aws"]["token"] is a placeholder access key ID. Give it to your
AWS SDK as the access key ID, with any secret access key and no session token; the node's egress proxy takes the SDK's
signature off and signs the request again with the session (SigV4 signing).
For Google and Entra, the token is a placeholder bearer token, put on as any bound connection's token.
Private clusters
AWS and Entra fetch the issuer's discovery document and key set over public HTTPS; Google can take an uploaded key set. A cluster whose edges the internet cannot reach:
- Starts its edges with
--public-edge-url https://oidc.acme.example: the issuer becomeshttps://oidc.acme.example/oidc/<tenant>, a place you control. - Publishes the documents there:Terminal
oz0 tenant oidc publish acme --to /srv/www/oidc.acme.example # a folder a web server serves oz0 tenant oidc publish acme --to 's3://acme-oidc?region=eu-north-1' # a bucket behind your https name
It reads the documents from an edge (--from https://<edge>:<port>, by default the CLI context's), checks them, and writesoidc/<tenant>/.well-known/openid-configurationandoidc/<tenant>/jwks.jsonwith an hour's cache. For ans3://target the credentials come from theAWS_*variables or--credentials-file. - Publishes again after every rotation: the key set holds the next key ahead of a rotation, so tokens keep verifying until the following one; after an emergency rotation, publish at once, since its new key is in no published set.
When it fails
| What you see | Why |
|---|---|
412 oz0_key_not_bound: "its cloud's token goes only to the cloud's own model API" | The record's address is not a host of its cloud's model API (see above), so no token was asked for and nothing was sent |
403 oz0_federation: "called only by an agent's model call" | A call with no agent behind it (an operator's, or an older node's that names no job): there is no agent to name in the token. Use a provider with a key for such calls |
AccessDenied from AWS STS | The trust policy does not match aud or sub, or does not allow sts:TagSession |
InvalidIdentityToken | AWS could not fetch or match the issuer's documents: the provider URL, or an unpublished private cluster |
invalid_target from Google | The pool provider's name, or its allowed audiences |
AADSTS70021 from Entra | No federated credential matches the issuer, the subject and the audience exactly |
| A tool's upload is refused for "chunk by chunk" | The SDK signs each chunk of the body with its own key; configure it for unsigned payloads with a trailing checksum (its default over HTTPS) or a hash of the whole body |
Limits
- "Bound to the call" holds inside Orchestrator Zero. The credential a cloud issues is a bearer credential at that cloud for its hour; it reaches only the edge and, sealed, the node's egress proxy.
- Workload tokens only: a cloud never receives a person's identifier from this path.
- Entra: Microsoft supports only RS256-signed issuer tokens, and the issuer signs ES256, so the Entra exchange runs outside what Microsoft has tested; an app registration or managed identity holds at most 20 federated credentials, one per agent.
- The fakes these features are tested against are shaped from the clouds' documentation; see integration maturity.