Secrets Handling for Agent Workflows: Vaults, Scopes, and Rotation
Keeping credentials out of model context: OWASP LLM risks, the tool broker pattern, scoped per-tool keys, Vault dynamic secrets and leases, rotation and revocation, and MCP OAuth 2.1 authorization.
Published on • October 4, 2026
AI Assistant

Secrets Handling for Agent Workflows: Vaults, Scopes, and Rotation
A classic service holds one long-lived API key in an environment variable, rotates it twice a year, and never logs it. An agent breaks every clause. Its context window is copied into prompts, cached, streamed, and often forwarded to third-party model providers; transcripts are retained for debugging; and tools are invoked from model output, so an attacker who controls an input can aim them at anything the credential allows.
Two design rules follow. Secrets never enter the model’s context — a component the model cannot read injects them at tool-execution time. And whatever the tools reach must be short-lived and narrowly scoped, so a leak costs minutes and one resource instead of months and everything.
Key technologies: MCP authorization (OAuth 2.1, RFC 8707 resource indicators, RFC 8414 / 9728 / 7591 metadata), HashiCorp Vault dynamic secrets and leases, OWASP Top 10 for LLM Applications, tool broker / sidecar pattern.
Why Agents Are Worse at Secret Hygiene
The OWASP Top 10 for LLM Applications, now part of the OWASP GenAI Security Project, names the relevant risks across its two published editions:
| Risk | 2025 | 2023/24 | How it hits agent secrets |
|---|---|---|---|
| Prompt injection | LLM01:2025 | LLM01 | Injected instructions steer tool calls toward exfiltration paths |
| Sensitive information disclosure | LLM02:2025 | LLM06 | A key echoed in a tool result or error reaches whoever sees the transcript |
| Excessive agency | LLM06:2025 | LLM08 | A broad credential “to be safe” makes the blast radius the whole account |
| Insecure plugin design | not carried over | LLM07 | A tool accepts an arbitrary string — a URL, a token field — as an exfiltration channel |
The structural problem is that a model is not a principal. It cannot be audited like one, it cannot hold a credential like one, and it does not forget. Anything in the prompt is visible to the model, likely present in logs and traces, and reachable by whatever it does next. The fix is architectural, not procedural.
Keep Secrets Out of the Model’s Context
The tool broker (or sidecar) pattern: the model sees tool names, descriptions, and JSON schemas — never the credential. A trusted process matches the request against policy, fetches a credential with a minutes-long lifetime, calls the upstream API, redacts secret material from the response, and returns the sanitized result.
const POLICY: Record<string, { path: string; ttl: string }> = {
"crm.contacts.search": { path: "kv/data/crm/read", ttl: "5m" },
"payments.refund": { path: "kv/data/payments/refund", ttl: "2m" },
"warehouse.lookup": { path: "kv/data/warehouse/read", ttl: "5m" },
};
export async function executeTool(name: string, args: unknown, runId: string) {
const policy = POLICY[name];
if (!policy) throw new Error(`tool ${name} is not brokered`);
const lease = await vault.read(policy.path, { ttl: policy.ttl, runId });
try {
const result = await upstream.call(name, args, lease.data); // secret never leaves this scope
return redact(result, Object.values(lease.data));
} finally {
await vault.revoke(lease.id).catch(() => {});
}
}
Three properties matter. The credential exists only inside executeTool, so it is absent from the conversation, the tool result, and the trace attributes. Each tool gets its own path, so payments.refund cannot read customer records. And the lease is revoked at the end of the call instead of left to expire. Keep the tool allowlist in the broker: the model asks, the broker decides whether the ask is answerable.
Scopes and Lifetime: Choosing the Credential
| Credential | Typical lifetime | Scope | Where it lives | Rotation trigger |
|---|---|---|---|---|
| Anything model-visible | none — must not exist | none | n/a | n/a |
| Vault dynamic cred | lease TTL, seconds to minutes | single path + policy | Vault, injected at execution | expiry, renewal, or revoke |
| OAuth access token (MCP) | short-lived | audience + scopes | client, Authorization header | refresh on expiry |
| Refresh token | long | original consent scope | OS keychain / secure storage | rotated on use (required for public clients) |
| Long-lived API key | months | one tool, least privilege | broker or sidecar only | schedule + incident |
Two rules of thumb: one key per tool function rather than one master key per agent, and read scopes by default, with write scopes granted only where the workflow demands them. A leaked credential that can search a warehouse for five minutes is a bad afternoon, not a breach.
Vault Dynamic Secrets, Leases, and Revocation
Vault’s documentation summarizes its job as centralizing secret management, rotating old credentials, generating credentials on demand, and auditing client interactions. Behind “generated on demand” is the lease: for every dynamic secret (and every service type token), Vault creates a lease with a duration and renewability, and promises the data is valid for that TTL.
vault read database/creds/agent-crm
# lease_id database/creds/agent-crm/9f3c...
# lease_duration 60m
vault lease renew -increment=600 database/creds/agent-crm/9f3c... # counted from now
vault lease revoke -prefix database/creds/agent- # kill the whole tree
Key details:
- Every dynamic secret requires a lease, even data meant to last forever — forcing the consumer to check in routinely, which makes audit logs more valuable and key rolling easier.
- Revocation is immediate. With the AWS secrets engine, access keys are deleted from AWS the moment the lease is revoked.
- Revoking a token revokes all leases created under it — end the agent session’s token and its credentials go with it.
- Prefix revocation (
vault lease revoke -prefix aws/) kills trees of secrets by path: your response to one compromised system. - The KV backend does not issue leases. Static secrets give storage and rotation, not expiry — exactly why the broker reads them and the model never does.
Cloud secret managers keep the same invariant: a scoped, expiring credential the model cannot negotiate with.
MCP Authorization: OAuth 2.1, Resource Indicators, Scopes
For MCP servers over HTTP transports, the protocol’s authorization spec defines the token path. It builds on the OAuth 2.1 IETF draft plus RFC 8414 (authorization server metadata), RFC 7591 (dynamic client registration), RFC 9728 (protected resource metadata), and RFC 8707 (resource indicators). The MCP server is an OAuth 2.1 resource server, the client is an OAuth 2.1 client, and an authorization server issues the tokens.
# 1. Unauthenticated request -> metadata pointer
curl -i https://mcp.example.com/mcp
# HTTP/1.1 401 Unauthorized
# WWW-Authenticate: resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
# 2. Protected Resource Metadata (RFC 9728) names the authorization server(s)
curl https://mcp.example.com/.well-known/oauth-protected-resource
# 3. Authorization Server Metadata (RFC 8414); optional RFC 7591 registration
curl https://auth.example.com/.well-known/oauth-authorization-server
# 4. Authorization and token requests both carry the resource indicator (RFC 8707)
curl https://auth.example.com/token \
-d grant_type=authorization_code -d code=... -d code_verifier=... \
-d redirect_uri=https://127.0.0.1/callback \
-d resource=https://mcp.example.com
# 5. Every MCP request carries the token in a header
curl https://mcp.example.com/mcp -H "Authorization: Bearer eyJhbGciOi..."
Non-negotiables from the spec:
- The
resourceparameter MUST appear in both the authorization request and the token request, naming the canonical URI of the MCP server — sent whether or not the authorization server advertises support. This binds the token to its audience. - Tokens travel in the
Authorization: Bearerheader and MUST NOT appear in the query string. - Servers MUST validate audience and reject tokens issued elsewhere. Token passthrough is forbidden: an upstream call makes the server an OAuth client to that API, which MUST NOT receive the client’s token.
- PKCE is mandatory, redirect URIs are registered and validated, and
stateshould be checked. - Short-lived access tokens are the recommendation, and authorization servers MUST rotate refresh tokens for public clients.
- Errors: 401 for missing or invalid authorization, 403 for invalid scopes, 400 for a malformed request.
- STDIO servers SHOULD NOT follow this spec — they take credentials from the environment, another reason to keep that environment free of model-visible secrets.
Scope design follows: request only what the workflow needs, align server-side scope checks with the broker’s per-tool policy, and treat a 403 as a signal to narrow the workflow, not widen the grant.
Rotation, Revocation, and Audit Logging
- Rotate on schedule and on events. Calendar rotation catches drift; event-driven rotation (session end, suspected leak) catches incidents.
- Revoke first, investigate second. Prefix and token revocation work in seconds; pair them with a break-glass procedure so revoking is never the scariest option.
- Audit every access. Vault’s audit logging records client interactions; your broker should log run ID, tool, lease path, and policy version — never the secret. On the MCP side, 401/403 responses and token issuance events are the trail.
- Scan your transcripts for token-shaped strings after any suspected leak — knowing beats guessing.
Best Practices
- Never write a secret into a prompt, system message, tool description, or tool result. In context means disclosed by design.
- Broker every tool call: allowlist, fetch per call, redact on return, revoke on completion.
- Scope per tool, not per agent. One master key turns a single injection into total compromise.
- Prefer dynamic credentials with leases over static keys. Expiry is enforced by the platform, not by discipline.
- Bind tokens to audiences. On MCP, send
resourceon both requests, validate audience server-side, never pass a client token upstream. - Rotate refresh tokens; keep access tokens short, stored where the model cannot inspect them.
- Version tool-to-credential policy like code and record the version in audit logs.
- Design for revocation. Assume any credential will leak; make the kill path a single command.
Wrapping Up
Agent secret hygiene fails when credentials are treated as configuration instead of capability. Keep them out of the model’s context with a broker in front of every tool; give each tool a scoped, short-lived credential minted on demand and revoked on completion; use leases and prefix revocation so expiry and kill switches are platform-enforced; and on MCP, follow the OAuth 2.1 flow with PKCE, resource indicators, and strict audience validation. The model reasons about the work. It should never hold the keys.