From Credential Sprawl to Unified Identity

Every agent you add is another key on the pile. What actually replaces credential sprawl — and which controls are real rather than aspirational.

Diyan BogdanovDiyan Bogdanov3 min read
#thought-leadership#credentials#security

Credential sprawl is not a tidiness problem. It is the reason nobody can answer the only question that matters after an incident: which agent used that key, and what did it touch?

Five agents, each handed its own set of provider keys, produces a system with no central audit trail, manual rotation across a dozen .env files, and no way to narrow what a key can reach once it is issued. Compromise one agent's environment and the attacker inherits everything that agent was ever given.

The fix is not a better secrets manager. It is to stop giving agents secrets.

Don't scope the credential — remove it from the agent#

The strongest control available is not a tighter permission on a key the agent holds. It is the agent never holding the key.

useCredential has the platform make the outbound request and inject the credential in transit:

import { Anima } from "@anima-labs/sdk";
 
const anima = new Anima({ apiKey: process.env.ANIMA_API_KEY! });
 
const response = await anima.vault.useCredential("cred_google_cloud", {
  agentId: agent.id,
  method: "GET",
  url: "https://cloudresourcemanager.googleapis.com/v1/projects",
});

Two things are enforced on that call. The URL's host must be on that credential's allowlist, so a prompt-injected agent cannot redirect the secret at an endpoint it chooses. And any Authorization header the caller sets is discarded and replaced — the agent cannot smuggle its own credential through the same path.

Set the credential's reveal policy to brokered and there is no plaintext read path at all. Reveal and export are refused for every key type, including the organization's master key. Recovery from a lost brokered credential is rotation, because there is nothing left to recover.

That is a different category of guarantee from "the key is encrypted." A secret the agent never receives cannot leak from the agent's context window, its logs, or its model provider.

What per-agent vault policy actually enforces#

Here is where it is worth being precise, because the honest answer is narrower than the marketing instinct.

Two booleans are enforced against an agent's own key:

SettingEffect
vault.blockedRefuses every vault operation
vault.readOnlyAllows reads; refuses create, update and delete

The gate binds agents, not admins

Both constrain only the agent's own ak_ key. Org master keys and internal service keys keep access on purpose: setting vault.blocked on a compromised agent must not lock the org admin out of the very vault they are trying to secure.

A violation is recorded as a BLOCKED security event before the refusal, so a locked-down agent that keeps trying is visible rather than silent.

Time-of-day rules are not enforced

A richer policy module exists in the codebase — allowed hours, URI patterns, credential types — and nothing on the request path calls it. Access windows like "this credential only between 9 and 5" are not enforced today, and an earlier version of this post said they were.

If you need a time window, enforce it in the code that decides whether to make the call. Do not assume the vault is doing it.

Revocation that lands everywhere at once#

An agent has three states: ACTIVE, SUSPENDED, DELETED.

await anima.agents.update(agent.id, { status: "SUSPENDED" });

The status is read during authentication, where the principal is minted, rather than on each individual operation. That placement is the point: a suspended agent does not merely lose vault access, it stops doing anything at all — across REST, the voice WebSocket, the browser extension bridge and MCP, because every one of them resolves through the same authentication path.

One write, and the agent is contained everywhere. That is the property a pile of provider keys can never give you, because revoking a shared key takes down every agent that shares it — which is why, in practice, nobody revokes it.

The audit trail is the actual deliverable#

Every credential operation writes an audit entry against the agent identity that performed it — vault.credential.read, vault.credential.accessed, vault.credential.created — and a denied useCredential is audited too, so a blocked egress attempt leaves a record rather than a silence.

These are audit entries, not webhook events

They appear in the audit log, not in the webhook catalogue. The only vault event you can subscribe a webhook to is vault.credential.refresh_failed. Subscribing to a name that is not on the webhook list is accepted and then never fires, so reach for audit.list here rather than an endpoint.

for await (const entry of anima.audit.list(orgId, { agentId: agent.id })) {
  console.log(entry);
}

This is the part that repays the migration. Not that access is narrower — though it is — but that "which agent did this?" has an answer. With five agents sharing provider keys, the honest answer is a shrug and a rotation that takes the whole fleet down with it.

Where to start#

You do not need to migrate everything. The order that pays off fastest:

  1. Move the credential with the largest blast radius — usually the payment or cloud key — into the vault and switch its call sites to useCredential.
  2. Mark it brokered once nothing needs the plaintext. That closes the read path permanently rather than relying on nobody calling it.
  3. Give each agent its own identity so the audit log distinguishes them. This is what makes the first two worth having.

Credential sprawl is quiet right up until the moment it is not. The goal is not zero secrets — it is that the ones you keep have a name attached, a record of every use, and a single write that turns them off.

Stay Updated

Get the latest on AI agent identity, delivered weekly.