Google Cloud Agent Identity 解讀:綁定 token 防重放與重部署身份陷阱
A Stolen Agent Token Is Useless Here: Inside Google Cloud Agent Identity, Bound Tokens, and the Redeploy Trap
Google Cloud Agent Identity 為每個代理提供與承載資源綁定的身份,並以 X.509 憑證將存取 token 綁定至代理,降低遭竊 token 在代理執行環境外被重放的風險。
The old problem: agents running as service accounts
For most of the last two years, the standard way to give an AI agent access to Google Cloud looked like this:
- Create a service account.
- Grant it a pile of roles so the agent "just works."
- Attach it to the runtime. Sometimes, sadly, download a JSON key.
That service account is usually shared by several agents, can be impersonated by anyone with the right role, and hands out bearer tokens. A bearer token works like a hotel key card. Whoever holds it gets in. If an agent leaks its token through a prompt injection, a debug log, or a tool response, the attacker can replay it from anywhere until it expires.
Agents make this worse than normal workloads. They read untrusted text all day, they call tools, and they can be talked into printing things they should not print.
Google Cloud's answer is Agent Identity, a new IAM identity type built specifically for agents. In this post I will cover how it works under the hood, why its tokens are not useful to a thief, and one lifecycle trap that most teams will hit in their first month.
What Agent Identity actually is
Every agent gets its own identity based on the SPIFFE standard. The identity is tied to the resource that hosts the agent, and it looks like this:
spiffe://agents.global.org-123456789012.system.id.goog/resources/aiplatform/projects/9876543210/locations/us-central1/reasoningEngines/my-test-agent
When you use it in IAM, you swap spiffe:// for principal://:
principal://agents.global.org-123456789012.system.id.goog/resources/aiplatform/projects/9876543210/locations/us-central1/reasoningEngines/my-test-agent
The pieces:
| Part | Meaning |
|---|---|
agents.global.org-ORG_ID.system.id.goog |
Trust domain for projects inside an organization |
agents.global.proj-PROJECT_NUMBER.system.id.goog |
Trust domain for projects with no organization |
aiplatform or discoveryengine
|
The service hosting the agent |
| The rest | The full resource path of the agent |
Three things make this different from a service account, according to the docs:
- It is not shared across workloads by default. One agent, one identity.
- It cannot be impersonated.
- You cannot create long-lived keys for it.
Today the supported hosts are Agent Runtime, Gemini Enterprise, and Cloud Run.
Granting access to an agent
Because the agent itself is the principal, you grant roles straight to it. No service account in the middle.
ORG_ID="123456789012"
PROJECT_NUMBER="9876543210"
PROJECT_ID="my-project"
AGENT_PATH="projects/${PROJECT_NUMBER}/locations/us-central1/reasoningEngines/my-test-agent"
AGENT_PRINCIPAL="principal://agents.global.org-${ORG_ID}.system.id.goog/resources/aiplatform/${AGENT_PATH}"
gcloud storage buckets add-iam-policy-binding gs://invoices-raw \
--member="${AGENT_PRINCIPAL}" \
--role="roles/storage.objectViewer"
Notice what this gives you: least privilege per agent. Your invoice agent can read the invoices bucket. Your HR agent cannot, even though both run in the same project. With a shared service account, that separation was very hard to keep.
One small gotcha from the docs: you cannot grant agent identities the legacy bucket roles like storage.legacyBucketReader. Use the modern roles/storage.* roles instead.
How bound tokens work (the part that matters)
This is the core idea, so let's go slowly.
When an agent with Agent Identity is deployed, Google Cloud gives it two things:
- A SPIFFE identity.
- An X.509 certificate, valid for 24 hours and renewed automatically.
When the agent needs to call a Google Cloud API, it asks for an access token. That token is cryptographically bound to the agent's certificate. The token alone is not enough. To use it, the caller must also prove it holds the private key behind that certificate.
The proof happens in two ways, depending on where the call goes:
| Path | Binding method | What it means |
|---|---|---|
| Agent to Google Cloud APIs | mTLS | The TLS connection itself proves the agent holds the certificate |
| Agent through Agent Gateway and beyond | mTLS to the gateway, plus DPoP | Each request carries a signed proof that only the key holder can produce |
DPoP stands for Demonstrating Proof of Possession. Think of it as signing every request with a key the attacker does not have. Google calls the combination "double-bound credentials."
On top of that, a Google-managed Context-Aware Access policy is applied by default. It enforces the mTLS and DPoP binding so that a bound token cannot be replayed outside its trusted runtime.
Walking through a token theft
Let's replay the scary scenario from the start of the post with Agent Identity in place.
- An attacker plants a prompt injection in a document.
- The agent reads it and is tricked into echoing its access token into a tool response.
- The attacker copies the token and calls a Google Cloud API from their laptop.
With a service account bearer token, step 3 works. The attacker has your data.
With a bound token, step 3 fails. The attacker's laptop cannot complete the mTLS handshake with the agent's certificate, and it cannot sign a DPoP proof with the agent's private key. That key never leaves the agent's runtime. The token is just a string with no power.
This does not make prompt injection harmless. The agent itself can still be tricked into misusing its own access while it is running. What bound tokens remove is the attacker's ability to take the access and leave with it. That shrinks the blast radius from "anything, anywhere, until the token expires" to "whatever the agent can be talked into right now, inside its runtime."
That is exactly why the next two sections matter.
Limiting what a tricked agent can reach: Principal Access Boundary
Bound tokens stop theft. They do not stop an agent from misusing permissions it legitimately has. For that, you combine two controls:
- Least-privilege IAM grants per agent, shown above.
- Principal Access Boundary (PAB) policies, which cap what an agent can ever reach, no matter what roles it collects later.
PAB policies on an agent identity are checked on outbound calls. If the agent tries to touch a resource outside its boundary, the call is denied even if some role somewhere would allow it. That protects you from the most common real-world failure: someone grants a broad role "temporarily" during an incident and forgets to remove it.
A practical pattern:
- One PAB boundary per business domain (finance, HR, support).
- Every agent in that domain gets bound to that boundary at deploy time.
- Fine-grained IAM roles inside the boundary.
Agent identities can also be used as principals in VPC Service Controls ingress and egress rules, so you can let a specific agent cross a perimeter without opening it to a whole service account.
The redeploy trap
Now the part I have not seen anyone write about yet.
An agent's identity comes from the resource ID of whatever hosts it. For Agent Runtime, that is the reasoningEngines resource ID. This has two consequences that the docs state plainly but that are easy to miss:
1. Redeploying creates a brand new identity.
If you delete an agent and deploy a replacement, even with the same name, code, and config, the new agent gets a new resource ID and therefore a new principal. None of the old IAM grants apply to it. Your "same" agent suddenly gets permission denied everywhere.
2. Deleting an agent leaves its grants behind.
Deleting the agent does not remove IAM bindings that reference its principal. They stay in your policies as inactive grants until someone removes them by hand.
Put those together and picture a team that redeploys by delete plus create every sprint. After six months, every bucket, dataset, and secret the agent touched carries a stack of dead principals. Nobody knows which ones are live. Your IAM policies get longer, your audits get noisier, and you are one step away from someone "fixing" an outage by granting a broad role to the wrong principal.
How to avoid the trap
Update in place instead of delete plus create. Agent Runtime supports managing revisions and traffic on an existing agent. As long as the hosting resource stays the same, its identity stays the same. Make "never delete to redeploy" a team rule.
Derive the principal in your IaC, never hardcode it. If you use Terraform, build the principal string from the agent resource's output, so a replacement automatically gets fresh bindings and the old ones are destroyed with the old resource.
Run an orphaned grant audit. That is the script below.
Finding orphaned agent grants
This script searches IAM policies across your organization for agent principals, checks whether each Agent Runtime agent still exists, and reports the dead ones.
It uses Cloud Asset Inventory, so the caller needs permission to search IAM policies at the organization level (for example roles/cloudasset.viewer).
# find_orphaned_agent_grants.py
import json
import os
import re
import subprocess
import google.auth
import requests
from google.auth.transport.requests import Request
ORG_ID = os.environ["ORG_ID"]
PRINCIPAL_RE = re.compile(
r"principal://agents\.global\.[^/]+/resources/aiplatform/"
r"(projects/[^/]+/locations/([^/]+)/reasoningEngines/[^/\s]+)"
)
def search_agent_bindings():
out = subprocess.run(
[
"gcloud", "asset", "search-all-iam-policies",
f"--scope=organizations/{ORG_ID}",
"--query=policy:agents.global",
"--format=json",
],
check=True, capture_output=True, text=True,
)
return json.loads(out.stdout)
def agent_exists(session, resource_path, location):
url = f"https://{location}-aiplatform.googleapis.com/v1/{resource_path}"
r = session.get(url)
if r.status_code == 404:
return False
r.raise_for_status()
return True
def main():
creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
creds.refresh(Request())
session = requests.Session()
session.headers["Authorization"] = f"Bearer {creds.token}"
cache = {}
orphans = []
for result in search_agent_bindings():
resource = result.get("resource")
for binding in result.get("policy", {}).get("bindings", []):
for member in binding.get("members", []):
match = PRINCIPAL_RE.search(member)
if not match:
continue
path, location = match.group(1), match.group(2)
if path not in cache:
cache[path] = agent_exists(session, path, location)
if not cache[path]:
orphans.append((resource, binding["role"], member))
for resource, role, member in orphans:
print(f"ORPHAN {role}\n on: {resource}\n member: {member}\n")
print(f"Total orphaned agent grants: {len(orphans)}")
if __name__ == "__main__":
main()
A few notes on the script:
- It only checks Agent Runtime agents (
aiplatformandreasoningEngines). Gemini Enterprise agents usediscoveryenginepaths, so add a similar check for those if you use them. - It reports, it does not delete. Review the list before removing anything. Removing a binding is easy; finding out at 2 a.m. that it was still needed is not.
- Run it on a schedule and treat a growing count as a signal that your team is redeploying by delete plus create.
Seeing who really did what: audit logs
One more benefit that SRE and security teams will like. When an agent acts on behalf of a user through delegated OAuth, the audit logs show both the agent's identity and the user's identity.
With a shared service account, an audit log line told you "the service account did it." With Agent Identity, it tells you "this specific agent did it, for this specific user." During an incident review, that difference saves hours.
There are also dedicated audit logs for the Agent Identity API and the Agent Identity Credentials API, so you can track when identities and credentials are issued.
Quick migration checklist
If you have agents on shared service accounts today, here is a sensible order of work:
- Inventory which agents share which service accounts.
- Turn on Agent Identity for one low-risk agent first.
- Grant that agent's principal only the roles it actually used. Check recent audit logs to find them.
- Add a PAB boundary for its business domain.
- Remove the agent's access to the old shared service account.
- Change your deploy process to update in place, then repeat for the next agent.
- Schedule the orphaned grant audit.
Limitations and caveats
- Agent Identity is supported on Agent Runtime, Gemini Enterprise, and Cloud Run today. Agents hosted elsewhere still need another approach.
- Legacy Cloud Storage bucket roles cannot be granted to agent identities.
- Bound tokens stop token theft and replay. They do not stop a tricked agent from misusing its own access while it is running. You still need least privilege, PAB, input filtering such as Model Armor, and human approval for risky actions.
- DPoP binding applies to traffic across Agent Gateway. Direct calls to Google Cloud APIs rely on mTLS binding.
- Trust domains differ for projects with and without an organization. Hardcoded principal strings will break if a project moves into an organization.
- My audit script assumes the Cloud Asset Inventory query and the
reasoningEnginesREST path work in your environment. The platform has been renamed recently, so test it in a sandbox first. - Features in this area are moving fast. Check the current docs before relying on any detail here.
Wrapping up
Agent Identity fixes a real weakness. Agents read untrusted input all day, so their credentials need to be useless outside the agent itself. Bound tokens do exactly that.
But identity tied to a resource ID brings a new kind of operational debt. Every delete plus create leaves dead grants behind and breaks the "same" agent's access. Update in place, derive principals in code, and audit for orphans, and you get the security benefits without the mess.
If you run the audit script against your org, I would love to hear how many orphaned grants it finds.
References
- Agent Identity overview: https://docs.cloud.google.com/iam/docs/agent-identity-overview
- Identity delegation architecture: https://docs.cloud.google.com/iam/docs/identity-delegation-architecture
- Authenticate to Google Cloud using an agent's own identity: https://docs.cloud.google.com/iam/docs/auth-agent-own-identity
- Agent Gateway overview: https://docs.cloud.google.com/gemini-enterprise-agent-platform/govern/gateways/agent-gateway-overview
- SPIFFE concepts: https://spiffe.io/
來源:DEV Community · dev.to