Free playbooks in your inbox
Hands-on tutorials for people who want to build with AI.

Build a Credential Broker for Your AI Agent

Credential brokering stops your AI agent from holding a steal-able secret. Build a 30-line broker that mints scoped 5-minute tokens with a runnable test.

From the youcanbuildthings catalog ▸ Build-tested 9 min read

Summary:

  1. Why a scoped credential is still a steal-able secret, and brokering removes it entirely.
  2. A dependency-free 30-line broker you can run today.
  3. The request/response contract that ties every token to the action that needed it.
  4. An acceptance test that proves the agent holds nothing worth stealing.

Credential brokering is the fix for a problem hiding in plain sight: your agent is holding a secret it doesn’t need to hold. Probably several. They sit in its environment, full-access and long-lived, and any injection clever enough to make the agent read out its own context walks away with them. Scoping the agent’s permissions shrank what it can do. It did nothing about what it carries.

You can’t guard your way out of this. The agent has to read its own secrets to use them, which means anything that controls the agent can read them too. The real fix is stranger and stronger: make sure the agent never holds the secret at all. As one developer put it, cutting straight to the design, “The agent doesn’t need your full keyring. It needs a small, scoped one.” I’d add one word: a small, scoped, borrowed one, returned before anyone can steal it.

What is credential brokering?

Credential brokering is a pattern where the agent asks a broker for a short-lived, narrowly-scoped token at the moment it needs one, uses it, and the token expires before it’s worth stealing. The agent never holds a long-lived secret, so there’s nothing durable in its context for an injection to exfiltrate.

In pseudocode, the difference between the trap and the fix is stark:

# The trap: a long-lived, full-access secret living in the agent's context
agent.env = { GMAIL_TOKEN: "ya29.AfullAccessTokenGoodForMonths..." }   # no expiry
# An injection that prints env, or passes secrets to a tool, walks away with this.

# The fix: borrow a narrow, short-lived token at the moment of use
token = broker.request(identity="inbox-agent", scope="gmail.send", ttl="5m")
gmail.send(token, reply)        # used by the tool layer, never pasted into the prompt
# 5 minutes later the token is dead. Nothing durable was ever in context to steal.

This is the same reframe that runs through real agent hardening: a permission set is an API key, not a user account. An API key has a scope, it expires, and it’s revocable without nuking your own session. The answer to “how does brokering stop prompt injection from stealing my keys” is structural, not behavioral. It doesn’t depend on the model resisting the injection. It depends on there being nothing valuable to take. You cannot steal a secret that was never in the room.

Credential broker flow: the trap is a long-lived GMAIL_TOKEN with no expiry sitting inside the agent context, reaching gmail.googleapis.com; the fix is a broker outside the agent that holds the durable signing key and hands the agent a scoped gmail.send token good for 5 minutes

What broke: the “just for testing” token that became production

Here’s how the agent got its Gmail token, because it’s how every agent gets every credential. You were building it, you needed it to actually read mail to test the triage logic, so you grabbed your own token, pasted it into the agent’s environment, and moved on. It worked. You meant to swap it for something scoped before this went anywhere real. Then the triage logic was good, the agent was useful, and somewhere around week two “just for testing” had quietly become production. The token never rotated. It’s a long-lived secret with broad access, sitting in plaintext, and you’ve stopped thinking about it.

Don’t tell yourself you’ll just rotate it regularly. You won’t, because manual rotation is a chore with no deadline. The real-world codexui-android supply-chain attack targeted exactly this: a long-lived refresh token in ~/.codex/auth.json that had never been rotated, read straight off disk by malicious code running as the user. File permissions stop other users. They do nothing against code running as you, which is what a compromised agent is. The right number of long-lived secrets in an agent’s environment is zero, because zero is the only count you can’t forget to maintain.

Build a broker you can read in thirty lines

“Broker” sounds like infrastructure. At its core it’s a request/response contract and about thirty lines of code. Here’s a complete, dependency-free local broker, the kind you’d run beside an agent on your laptop. It mints a signed, scope-limited, time-boxed token. The one durable secret, the signing key, lives in the broker and never touches the agent:

# broker.py — a minimal local credential broker. No external dependencies.
# The durable secret (the signing key) lives HERE; the agent holds only minted tokens.
import os, hmac, hashlib, json, time, base64

SIGNING_KEY = os.environ["BROKER_SIGNING_KEY"].encode()   # broker-side only

# What each identity may request, and the MAX ttl it may have (seconds).
POLICY = { "inbox-agent": {"gmail.send": 300, "ticket.create": 300} }

def request(agent_identity, scope, ttl_seconds, reason):
    allowed = POLICY.get(agent_identity, {})
    if scope not in allowed:                          # scope not on this identity's list
        raise PermissionError(f"{agent_identity} may not request {scope}")
    ttl = min(int(ttl_seconds), allowed[scope])       # clamp to the policy ceiling
    claims = {"sub": agent_identity, "scope": scope,
              "exp": int(time.time()) + ttl, "reason": reason}
    body = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode()
    sig  = hmac.new(SIGNING_KEY, body.encode(), hashlib.sha256).hexdigest()
    return f"{body}.{sig}"                             # token = claims + signature

def verify(token, required_scope):
    body, sig = token.rsplit(".", 1)
    expect = hmac.new(SIGNING_KEY, body.encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expect):          # forged or tampered
        return False
    claims = json.loads(base64.urlsafe_b64decode(body))
    return claims["scope"] == required_scope and claims["exp"] > time.time()

To run it, set BROKER_SIGNING_KEY in the broker’s environment first (in production, load it from your secret manager). Then walk the three properties that matter, each one a line you can point at:

  1. Scope. request refuses any scope not in POLICY for that identity. An injection that talks the agent into asking for gmail.delete gets a PermissionError, not a token.
  2. TTL. The exp claim is set at mint time and verify rejects anything past it, so a stolen token is dead in minutes. Match the ceiling to how dangerous the action is: a send token can live 5 minutes, a ticket token 15, a read-only token 60.
  3. Revocation without nuking yourself. Rotate SIGNING_KEY and every token in flight is instantly invalid, while your own credentials are untouched.

That’s the whole game. Ask, check the policy, mint short and scoped, expire automatically.

The request/response contract

The agent and broker agree on a tiny message shape: the agent says who it is, what scope it wants, for how long, and why; the broker answers with a token reference, an expiry, and the one action it permits.

{
  "request": {
    "agent_identity": "inbox-agent",
    "scope": "gmail.send",
    "ttl_seconds": 300,
    "reason": "send_reply:msg-8821"
  },
  "response": {
    "token_ref": "broker-token-abc123",
    "expires_at": "2026-01-01T12:05:00Z",
    "allowed_action": "gmail.send"
  }
}

The reason field is the one people skip and shouldn’t. It ties every minted credential to the specific action that needed it, so your audit log can later say not just “a send token was issued” but “a send token was issued to reply to message 8821.” A credential with no stated reason is a credential you can’t investigate.

Prove it holds

Don’t assume the broker has these properties. Run the acceptance test. Each checked box is a property you confirmed, not one you hoped the design had:

[ ] env dump contains no GMAIL_TOKEN and no ticket API key
[ ] context dump (the model's visible prompt) contains no durable secret
[ ] broker issues a token with TTL <= 5 minutes
[ ] an expired token is rejected by verify()
[ ] a token forged with the wrong key is rejected by verify()
[ ] a scope the identity isn't allowed (e.g. gmail.delete) is refused at request()
[ ] rotating the signing key invalidates every token already issued

The unchecked box is your hole.

You don’t need Vault, but Vault shows you the shape

The 30-line broker is the illustrative floor. The production ceiling is a system like HashiCorp Vault’s dynamic secrets, and it’s worth seeing because every other broker is a variation on it:

How Vault brokers credentials. Vault secrets engines “connect to other services and generate dynamic credentials on demand” rather than storing static ones. Two lifecycle properties make this a broker: issued credentials are bound to a lease with a TTL, and “when a secrets engine is disabled, all of its secrets are revoked.” The Databases engine, for example, generates short-lived database credentials per request.

Source: HashiCorp Vault docs, Secrets engines. If the agent gets compromised at minute fifty-nine of a sixty-minute lease, the attacker has a login that works for sixty seconds. Cloud-native brokers do the same job: AWS STS AssumeRole hands out temporary scoped credentials, OIDC workload-identity federation avoids a static secret entirely, and 1Password Service Accounts inject scoped secrets at runtime. The decision tree is short. Cloud-native resource, use the cloud’s temporary credentials. Cross-environment, use OIDC. Everything else, broker through Vault or your secret manager. There’s no branch where the right answer is “paste a long-lived token into the agent.”

What should you actually do?

  • If your agent runs on your laptop → run the 30-line broker beside it, or use your secret manager’s runtime injection. Both APIs the inbox agent touches issue scoped tokens you can keep short-lived.
  • If your agent runs in the cloud → lean on STS-style temporary credentials for anything cloud-native and OIDC for cross-environment hops. The infrastructure to mint short-lived scoped credentials is already running.
  • If you can’t rip out long-lived secrets today → at least confirm the brokered token is used by the tool layer, not pasted into the model’s prompt where the next injection can read it. The token passes through the plumbing around the model, never through its text.
  • Before you call it done → dump the agent’s environment and context mid-run. If you find a durable secret sitting there, you’re not done.

The bottom line

  • A scoped secret is still a secret. Scoping limits the damage; brokering removes the bomb. Shrinking the blast radius is not the same as taking away the explosive.
  • Short TTLs are how you stop rotating by hand forever. A five-minute token rotates itself every five minutes by being reissued, and the chore with no deadline disappears.
  • The goal was never “unhackable.” It’s making the prize worthless: an injection that fully owns the agent walks away with a token that does one narrow thing for five minutes, then turns to dust.
Why trust this? Every youcanbuildthings guide is pulled from a build-tested book: code that ran in production before it was written down.

Frequently Asked Questions

What is credential brokering for AI agents?+

A pattern where the agent asks a broker for a short-lived, narrowly-scoped token at the moment it needs one, uses it, and the token expires before it's worth stealing. The agent never holds a durable secret.

How does credential brokering stop prompt injection from stealing keys?+

It doesn't rely on the model resisting the injection. It removes the prize. An injection that drains the agent's context finds only a token that does one narrow thing for a few minutes, or nothing at all.

Do I need HashiCorp Vault to broker credentials?+

No. Vault dynamic secrets are the canonical version, but AWS STS, OIDC federation, 1Password, or a 30-line local broker all do the same job: mint short-lived scoped tokens on demand.