From 93253560a29e9f5237bfce4b3c6d1732c73d5699 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Sun, 18 Oct 2026 16:53:56 +0000 Subject: [PATCH] docs: Google APIs with a federated credential A mesh member calls Gemini on Vertex AI and an agent on Agent Engine through an egress node with Workload Identity Federation: the pool and the provider that trust the control plane as an OIDC issuer, the service account, the policy with oidc_federation and an http grant narrowed to the model and engine paths, the call, the refusals, and what the node's audit line and Google's audit log show. No service account key exists. Behind a TLS-terminating proxy the control plane does not infer its issuer scheme from forwarded headers; the guide says to set the public URL (--external-url on agentmesh-one). Run on a Cloud Run hub with nodes on GKE, from an OpenShell sandbox and a Substrate actor. --- .../docs/guides/google-cloud-credentials.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 site/content/docs/guides/google-cloud-credentials.md diff --git a/site/content/docs/guides/google-cloud-credentials.md b/site/content/docs/guides/google-cloud-credentials.md new file mode 100644 index 00000000..4426906b --- /dev/null +++ b/site/content/docs/guides/google-cloud-credentials.md @@ -0,0 +1,185 @@ +--- +title: "Google APIs with a federated credential" +linkTitle: "Google Cloud credentials" +weight: 3 +description: "Give mesh members access to Google APIs through an egress node that exchanges the control plane's token at Google STS. No Google credential is stored anywhere." +--- + +A member of the mesh calls a Google API: Gemini on Vertex AI, an agent on +Agent Engine, BigQuery, or Cloud Storage. This guide puts an `agentmesh-node` +in front of the API as the enforcement point. Workload Identity Federation +supplies the credential. + +The node authorizes each request on the caller, the method and the path. +Then the node mints a short-lived token at the control plane, exchanges it +at Google STS, and injects the Google access token. No service account key +exists. The member holds no Google credential. + +How it works: + +```text +member POST /mesh//egress/aiplatform.googleapis.com/v1/.../gemini-2.5-flash:generateContent + X-Mesh-Authentication: ****** member's Biscuit> +node authorizes: role grants egress://aiplatform.googleapis.com, POST, this path + POST /sts/token -> ES256 JWT: iss=, sub=, aud= + POST https://sts.googleapis.com/v1/token -> federated token + POST iamcredentials ...:generateAccessToken (impersonate) -> access token + POST https://aiplatform.googleapis.com/v1/.../gemini-2.5-flash:generateContent + Authorization: ****** token> +``` + +You need a control plane that Google can reach over HTTPS, at +`/.well-known/openid-configuration` and `/jwks`. You need a node that +serves the destination. You need a Google Cloud project where you can +create a Workload Identity Pool and a service account. The [Cloud Run +guide](/docs/guides/cloud-run/) gives you such a control plane. + +## 1. Trust the control plane in Google Cloud + +The control plane is an OIDC issuer. Its issuer URL must use `https`. +Behind a TLS-terminating proxy such as Cloud Run, the control plane does +not take the scheme from forwarded headers. Give it the public URL: +`--external-url=https://.run.app` on `agentmesh-one`, or +`--sts-issuer-url` on `agentmesh-control-plane`. Check the issuer with: + +```bash +curl -sS https://.run.app/.well-known/openid-configuration | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["issuer"], d["jwks_uri"])' +``` + +Create the pool, the provider and the service account. The allowed +audience of the provider is its own resource name. The control plane puts +that name in `aud`. + +```bash +PROJECT= +PN=$(gcloud projects describe $PROJECT --format='value(projectNumber)') +HUB=https://.run.app +gcloud services enable sts.googleapis.com iamcredentials.googleapis.com aiplatform.googleapis.com --project $PROJECT + +gcloud iam workload-identity-pools create agentmesh --project $PROJECT --location global +AUD="//iam.googleapis.com/projects/$PN/locations/global/workloadIdentityPools/agentmesh/providers/hub-cp" +gcloud iam workload-identity-pools providers create-oidc hub-cp --project $PROJECT --location global \ + --workload-identity-pool agentmesh --issuer-uri "$HUB" --allowed-audiences "$AUD" \ + --attribute-mapping "google.subject=assertion.sub,attribute.actor=assertion.act.sub" + +gcloud iam service-accounts create mesh-gemini --project $PROJECT +SA=mesh-gemini@$PROJECT.iam.gserviceaccount.com +gcloud projects add-iam-policy-binding $PROJECT --member serviceAccount:$SA --role roles/aiplatform.user --condition None +gcloud iam service-accounts add-iam-policy-binding $SA --project $PROJECT --role roles/iam.workloadIdentityUser \ + --member "principalSet://iam.googleapis.com/projects/$PN/locations/global/workloadIdentityPools/agentmesh/*" +``` + +The last binding lets every identity that the control plane authenticates +impersonate the service account. The mesh policy decides which members +reach the destination, and with which methods and paths. To narrow the +binding on the Google side as well, map a claim to an attribute +(`attribute.role=assertion.mesh_roles[0]`). Then bind +`principalSet://.../attribute.role/` instead of `*`. + +The `sub` claim in the token is the principal of the member: a SPIFFE ID +for a workload, or `user:` for a person. The `act.sub` claim is the +peer ID of the node that served the request. Google STS maps `sub` to +`google.subject`; the attribute mapping above keeps `act.sub` as +`attribute.actor`. + +## 2. Write the policy + +The `egress` entry names the destination, the nodes that serve it, and the +federation. The role lists the destination and narrows it to the method +and the path: + +```json +{ + "roles": [ + {"name": "mesh:role:node", "allowed_services": ["*"], "allowed_publish": ["*"], "allowed_targets": ["*"], "allowed_labels": ["*"]}, + {"name": "mesh:role:router", "allowed_services": ["*"], "allowed_targets": ["*"]}, + {"name": "sandbox", + "allowed_services": ["system://mesh.catalog", "egress://aiplatform.googleapis.com"], + "allowed_targets": ["*"], + "http": [{"service": "egress://aiplatform.googleapis.com", "methods": ["POST"], + "paths": ["/v1/projects//locations/us-central1/publishers/google/models/*"]}]} + ], + "bindings": [ + {"role": "sandbox", "members": ["user:spiffe://openshell.local/openshell/sandbox/*"]} + ], + "egress": [ + {"name": "aiplatform.googleapis.com", + "served_by": ["site=office"], + "broker": {"oidc_federation": { + "token_endpoint": "https://sts.googleapis.com/v1/token", + "audience": "//iam.googleapis.com/projects//locations/global/workloadIdentityPools/agentmesh/providers/hub-cp", + "impersonate": "mesh-gemini@.iam.gserviceaccount.com", + "scopes": ["https://www.googleapis.com/auth/cloud-platform"]}}} + ] +} +``` + +- `served_by` selects the nodes by role or label. Each selected node logs + `[Egress] Serving egress://aiplatform.googleapis.com` when it takes the + assignment. +- APIs that do not accept the federated principal directly need + `impersonate`. Vertex AI is one of them. +- `paths` is a prefix match under the destination. The grant above allows + every publisher model in one region. It allows no other path on the host. +- One hostname can serve more than one product. `aiplatform.googleapis.com` + serves Gemini (`publishers/google/models/*`) and Agent Engine + (`reasoningEngines/*`). A role has one `http` entry per service. To grant + both, put both paths in the `paths` list of that entry. + +Push the policy with `POST /policies` and the admin token, or through the +Cloud Run secret. The nodes log the assignment within their sync interval. + +## 3. Call the API + +Send the request from the node of a member, with the credential of that +member. Address the serving node by peer ID: + +```bash +PEER= +curl -sS -H "X-Mesh-Authentication: ******" -H "Content-Type: application/json" \ + "http://127.0.0.1:8080/mesh/$PEER/egress/aiplatform.googleapis.com/v1/projects//locations/us-central1/publishers/google/models/gemini-2.5-flash:generateContent" \ + -d '{"contents":[{"role":"user","parts":[{"text":"Answer with one word: what is 2+3?"}]}]}' +``` + +Gemini returns the answer. A `GET` on the same path returns `403` with +`Proxy-Status: agentmesh-node; error=http_request_denied`. A `POST` to +`/v1/projects//locations/us-central1/endpoints` returns the same +`403`. + +The serving node writes one line per decision: + +```text +Audit Traceability {"target": "egress://aiplatform.googleapis.com", "method": "POST", + "path": "/v1/projects//locations/us-central1/publishers/google/models/gemini-2.5-flash:generateContent", + "host": "aiplatform.googleapis.com", "port": 443, "decision": "allow", + "user": "spiffe://openshell.local/openshell/sandbox/0e4e35e4-…", "role": "sandbox", "actor_node": "12D3KooW…"} +Audit Traceability {"target": "egress://aiplatform.googleapis.com", "method": "GET", ..., "decision": "deny", + "reason": "biscuit: denied by no matching policies"} +``` + +The control plane logs the `/sts/token` request. The Google audit log shows +one `GenerateAccessToken` on the service account for each new token, with +`lifetime: 300s`. That entry names the service account. The identity of +the member is in the audit line of the node. + +## 4. From a sandbox + +An OpenShell sandbox or a Substrate actor reaches the destination through +its edge node (see [OpenShell or Substrate](/docs/guides/openshell-and-substrate/)). +The enforcement point of the runtime injects the Biscuit of the member. The +edge node forwards the request to the serving node. The allowlist of the +runtime must permit the path `/mesh/**` on the edge node. The examples +permit it. `make gemini NAME= GCP_PROJECT=` in each +example runs the three requests above from inside the sandbox or the actor. + +## Limits + +- The node caches the Google token per caller credential. An entry is valid + for at most the lifetime of the token that the node minted at the control + plane, 5 minutes by default. Then the node performs the exchange again. A + change of the role of a member takes effect at the next mesh credential of + that member, as for any destination. +- A `TaskAuthorizationRule` on the Biscuit of the caller narrows `scopes`, + methods and paths further. It cannot widen them. +- The destination is one hostname. See [Egress + destinations](/docs/guides/egress-destinations/) for the general limits.