Skip to main content

Short-Lived Credentials for AI Agents on Kubernetes: Designing Out the Long-Lived-Secret Failure Mode

8 min readDora NodaDora Noda
Share

On March 24, 2026, a threat actor calling itself TeamPCP published two backdoored releases of LiteLLM — a routing library with roughly 3.4 million downloads a day — to PyPI. The credential-stealing payload was live for about 40 minutes before PyPI quarantined it. In that window it reached AWS tokens, GCP credentials, SSH keys, Kubernetes configs, and database passwords sitting on every machine that pulled the update.

Forty minutes sounds like a small window. It wasn't the window that mattered. Every one of those credentials was long-lived, so the attacker's real deadline wasn't 40 minutes — it was however long it took each victim to notice and rotate. For teams still running static AWS keys and unrotated kubeconfigs, that's days, sometimes months. The incident is a supply-chain story on the surface, but the damage ceiling underneath it was set entirely by credential lifetime.


Why a Kubernetes Secret is the failure mode, mechanically

"Kubernetes configs" among the stolen assets usually means exactly what it sounds like: a Secret object, or a kubeconfig pointing at one. It's worth being precise about why that object is the multiplier, not just the AI angle:

  • Base64 is encoding, not encryption. A kubectl get secret -o jsonpath and one base64 -d recovers the plaintext. Anyone who assumes a Secret is "encrypted at rest" by default is wrong unless the cluster operator has explicitly configured encryption at rest for etcd.
  • etcd stores it in a retrievable form. Without that encryption-at-rest configuration, anyone with read access to etcd — a backup, a snapshot, a misconfigured admin binding — can pull every Secret in the cluster in plaintext.
  • RBAC over-shares by default. In a lot of real cluster configurations, anyone authorized to create a Pod or Deployment in a namespace can read any Secret in that namespace, including indirectly — by mounting it into a pod they control, even without an explicit get secrets grant.

GitGuardian's 2025 State of Secrets Sprawl report put a number on the slow-burn version of this: 28.6 million new secrets exposed in public GitHub commits in 2025, up 34% year over year, and 64% of secrets confirmed leaked back in 2022 were still active and exploitable as of January 2026. Humans are already bad at rotating credentials once they're out.

Now hand an AI agent write access to deploy pods in that namespace. It doesn't just inherit the deploy permission — it inherits read access to every Secret sitting next to the workloads it manages, and it can act on that access at machine speed, not human speed. The blast radius was always there; agents are just fast enough to actually use all of it before anyone notices.


What short-lived actually looks like

The fix in the TODO spec — Vault or an External Secrets Operator/CSI pull, keyed to Kubernetes service account tokens — has a concrete mechanical shape, and it starts below the secrets manager, at the token Kubernetes itself hands out.

Since Kubernetes v1.22, the recommended way to identify a pod isn't a static, long-lived service account token stored as a Secret — it's a bound service account token obtained through the TokenRequest API:

  • Audience-bound: the token names the specific service it's meant for; a recipient that isn't that audience should reject it.
  • Object-bound: the token is tied to the pod that requested it and is invalidated the moment that pod is deleted.
  • Time-bound: expirationSeconds defaults to one hour, and the kubelet automatically requests a new one once the token passes 80% of its TTL or turns 24 hours old — no separate rotation job to babysit.

That bound token becomes the identity an external secrets manager authenticates against — not a shared platform credential. From there you have two parallel delivery patterns, and they deserve equal billing rather than treating one as the "real" answer and the other as a footnote:

  1. Vault + External Secrets Operator. The pod's bound SA token authenticates to Vault's Kubernetes auth method. Vault verifies the token against the live API server (a deleted pod's token is rejected outright) and issues a dynamic secret — say, database credentials with a 15-minute TTL — instead of a static one. ESO syncs that into a Kubernetes Secret the pod mounts. It's still an etcd-resident Secret, but now it's short-lived and scoped to one workload instead of static and shared.
  2. Secrets Store CSI Driver. The Vault CSI provider skips the Kubernetes Secret object entirely: it mounts the dynamic secret straight into the pod as a CSI volume via a SecretProviderClass, keyed to the pod's own service account. The secret never touches etcd, and it can rotate without a pod restart.

Either path turns "credential valid until someone notices" into "credential valid until the task that needed it is over." The design choice that actually matters is scoping the TTL to the task, not the convenience of a long-lived session — a 15-minute dynamic secret for a database migration job, not a 90-day static key that happens to also work for migrations.


MCP as the credential broker for the agent's own actions

Everything above secures the platform's identity layer. It says nothing about the credentials an agent hands to the tools it calls mid-task — and that's a separate, and currently worse, problem.

Astrix Security's analysis of more than 5,200 open-source MCP server implementations found that 88% require credentials of some kind, but 53% of those rely on exactly the pattern this whole piece is arguing against: long-lived static secrets — API keys and personal access tokens — with no expiry. 79% of servers store those keys directly in environment variables, readable by anything with process access.

The fix at this layer looks like AWS Bedrock AgentCore Identity's approach: a central token broker holds the actual OAuth credentials, and the MCP server or gateway never touches them directly. The agent's request for a tool call goes through the broker, which issues a short-lived, scoped token for that specific call and lets it expire when the call returns — the same "identity in, time-boxed credential out" shape as the Vault/Kubernetes flow, just applied one layer up, at the point where the agent picks up a tool instead of where a pod picks up a workload identity.

Self-hosting that pattern doesn't require AgentCore specifically — the same Vault Kubernetes auth method already described can back an MCP gateway, issuing a scoped, short-TTL token per tool call instead of handing the MCP server a standing API key at startup.


Designing this into an agent-operated PaaS

None of this is optional once "the agent can deploy and roll back your app" is a platform feature rather than a lab demo. An agent that can trigger a real deploy needs a credential layer built around a specific checklist, not the default Kubernetes Secret behavior every cluster ships with:

  • One Vault role per agent identity, not a shared platform credential. If every agent action authenticates through the same service credential, you can't tell which agent did what, and you can't revoke one agent's access without cutting off all of them.
  • TTL scoped to the task, not the session. A deploy operation might take 90 seconds. The credential backing it should expire in roughly that window — not persist for the agent's entire multi-hour session because that was easier to configure once.
  • Audience-bind each token to its actual target. A token scoped to the deploy webhook shouldn't also be valid against the database or the container registry. Without that binding, a credential stolen from one action is replayable against every other system the platform touches.
  • Stop building a bespoke revocation system. Bound service account tokens are already invalidated the instant their pod is deleted — that's free. Layering a custom revocation path on top usually means someone forgot the free version exists, or reintroduced a long-lived credential somewhere the bound-token model doesn't reach.
  • Prefer CSI-mounted dynamic secrets over syncing into a Kubernetes Secret object when the tooling allows it. ESO's sync-to-Secret path is a real improvement over static Secrets, but it still leaves a plaintext-recoverable object sitting in etcd. The CSI Driver path avoids reintroducing the exact etcd/RBAC exposure this piece opened with, even after everything else is "short-lived."

This is what separates a platform that merely rotates credentials on a schedule from one that has actually designed the failure mode out: an agent that deploys your app should hold a credential that is scoped to that one deploy, valid against that one target, and already dead by the time anyone would think to revoke it.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with AI agents as first-class operators instead of an afterthought bolted onto a human-shaped API. Star the repo on GitHub and see how the credential layer looks when it's built for agents from day one.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide