AI agents leak credentials. The 2026 headlines document it in detail: prompt injection, tool definition poisoning, and misconfigured service accounts all lead to the same outcome — some long-lived API key or database password lands in attacker hands. The standard security response is to rotate secrets more aggressively, lock down RBAC policies, and add alerting. All of that is correct and none of it changes the fundamental problem.
The fundamental problem is that a static credential is a bearer of standing authority. Whoever holds it can use it until a human revokes it. If your agent is holding it when it gets compromised, the attacker’s clock starts at the moment of exfiltration and runs until your on-call engineer wakes up.
Short-lived credentials change what a leaked secret is worth. A credential with a five-minute TTL that auto-revokes on lease expiry may already be dead by the time it reaches the attacker’s tooling. This post covers the concrete Kubernetes and Vault architecture that makes short-lived credentials the default for agent workloads, not a one-off for crown-jewel databases.
For the broader argument about why standard IAM frameworks fail for agents, see our AI Agent Credential Crisis guide. For the egress control layer that pairs with short-lived credentials to reduce blast radius further, see our agent egress control guide.
The Reframe: A Five-Minute Credential Is a Different Threat
In 2026, a widely documented incident involved an AI coding agent finding an overprivileged API token in a codebase, deleting a production database in nine seconds, then destroying all backups because they were stored in the same location. The agent was following instructions. The token was long-lived and had no scope boundary. Prompt injection or a bad instruction and the agent becomes a fast-moving threat with persistent access.
The structural answer is not a better prompt. It is a credential that is already dead before the attacker can use it.
gantt
title Credential Exposure Window: Static vs Dynamic
dateFormat HH:mm
axisFormat %H:%M
section Static Credential
Credential issued (valid indefinitely) :active, s1, 00:00, 12h
Agent compromised at t+2m :milestone, s2, 00:02, 0
Attacker exfiltrates at t+4m :milestone, s3, 00:04, 0
Attacker pivots with live credential :crit, s4, 00:04, 12h
Manual rotation (if it happens) :milestone, s5, 04:00, 0
section Dynamic Credential (5m TTL)
Credential issued :active, d1, 00:00, 5m
Agent compromised at t+2m :milestone, d2, 00:02, 0
Attacker exfiltrates at t+4m :milestone, d3, 00:04, 0
Vault revokes lease at t+5m :milestone, d4, 00:05, 0
Attacker reuse attempt fails :done, d5, 00:05, 5m
With a static credential, the attacker’s window opens at exfiltration and stays open until a human acts. With a five-minute dynamic credential, Vault revokes the lease automatically — the attacker’s window may already be closed before they can pivot.
This is the core reframe. Vault’s database secrets engine issues a per-request credential with a default_ttl and a max_ttl. When the lease expires, Vault deletes the database user. The credential stops working at the source, not just in the application. Set that TTL to minutes and you change what an exfiltrated credential is: not standing authority, but a receipt for access that may no longer exist.
Every issuance is also uniquely attributable. When a dynamic credential is used, the audit log records which agent, which Vault role, and which lease. That closes the permission-versus-pattern gap where a compromised service account looks indistinguishable from legitimate traffic.
The Bootstrap Problem: How Kubernetes Auth Removes the Last Static Secret
The obvious objection: how does the agent authenticate to Vault in the first place without a static Vault token?
The answer is the Vault Kubernetes auth method. The pod presents its Kubernetes ServiceAccount JWT to Vault’s /auth/kubernetes/login endpoint, along with a role name. Vault calls the Kubernetes TokenReview API to validate the JWT, confirming the token is current and the service account still exists. If both checks pass, Vault returns a short-lived token scoped to the policies bound to that role.
The trust anchor is the pod’s identity, not a shared secret.
sequenceDiagram
participant Pod as Pod (SA: agent-orders)
participant Vault as HashiCorp Vault
participant K8s as Kubernetes TokenReview API
Pod->>Pod: Mount projected SA token<br/>(audience=vault, 10m expiry)
Pod->>Vault: POST /auth/kubernetes/login<br/>{role: "agent-orders", jwt: <SA token>}
Vault->>K8s: TokenReview: is this JWT valid?
K8s-->>Vault: Valid, SA=agent-orders, NS=ai-agents
Vault-->>Pod: Short-lived Vault token<br/>(scoped to agent-orders-db policy)
Pod->>Vault: Read creds/agent-orders-db
Vault-->>Pod: {username, password, lease_id, ttl: 5m}
The pod authenticates to Vault using its projected service account token. No static bootstrap secret is stored anywhere in the cluster.
Projected (bound) service account tokens are the Kubernetes feature that makes this work cleanly. Available since Kubernetes v1.22, they are audience-scoped (the audience field must match the Vault role’s audience setting), time-bound via expirationSeconds and rotated automatically by the kubelet, and invalidated when the pod is deleted. A projected SA token with a 10-minute expiration and audience: vault is useful for exactly one thing: logging in to that Vault cluster. Nothing else can use it.
volumes:
- name: vault-token
projected:
sources:
- serviceAccountToken:
path: vault-token
audience: vault # must match the Vault role audience field
expirationSeconds: 600 # 10m bound token, auto-rotated by kubelet
On the Vault side, the role binds identity (a specific service account and namespace) to policy:
# Enable Kubernetes auth and point Vault at the cluster's TokenReview API
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
# Bind ONE service account to a Vault policy with a short token TTL
# one service account per agent role — no shared identity
vault write auth/kubernetes/role/agent-orders \
bound_service_account_names=agent-orders \
bound_service_account_namespaces=ai-agents \
policies=agent-orders-db \
ttl=15m \
audience=vault
The ttl=15m is the lifetime of the Vault token returned after login, not the credential TTL. The bound_service_account_names and bound_service_account_namespaces fields mean this role can only be claimed by one specific workload. An attacker who compromises a different pod in the cluster cannot claim agent-orders permissions because their service account will not match.
Dynamic Secrets: Credentials That Expire on Their Own
With the authentication chain in place, the next step is making the credentials themselves short-lived at the source.
Vault’s database secrets engine is the canonical example. Rather than storing a static database password, Vault connects to the database as an admin user. When a client reads from the engine’s creds/ path, Vault creates a fresh database user with a generated password, scoped to the role’s permissions, and hands back the credential with a lease.
vault secrets enable database
vault write database/roles/agent-orders-db \
db_name=orders-postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl=5m \
max_ttl=30m
Two TTL fields control the lifetime:
default_ttl— the initial lease duration when the credential is issued. The consumer can renew the lease before expiry up tomax_ttl.max_ttl— the hard ceiling. Even with renewals, the credential cannot live past this limit. Vault deletes the database user atmax_ttlregardless.
For agent workloads, tune these down aggressively for high-blast-radius backends. A default_ttl of five minutes and a max_ttl of thirty minutes means a long-running task can keep renewing, but an exfiltrated credential has a short window. Vault deletes the database user when the lease expires, not just revokes the password: the credential stops working at the source.
The same pattern extends to cloud credentials (AWS, GCP, Azure secrets engines) and PKI (short-lived TLS certificates via the PKI secrets engine). The mechanism is identical: generate on demand, lease with a TTL, auto-revoke at expiry.
Delivering Secrets to the Agent: ESO vs VSO
There are four paths for getting a Vault-managed secret into a running pod. They differ in where the secret materializes and how tightly they integrate with Vault’s lease lifecycle.
External Secrets Operator (ESO) — provider-agnostic sync
ESO reads from an external secret store and writes the value as a native Kubernetes Secret. Its stable API group is external-secrets.io/v1, which went GA from the older deprecated v1beta1. The core kinds are SecretStore (namespace-scoped backend config), ClusterSecretStore (cluster-scoped), ExternalSecret (what to pull and where to put it), and ClusterExternalSecret.
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: vault-backend
namespace: ai-agents
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "database"
version: v2
auth:
kubernetes:
mountPath: "kubernetes"
role: "agent-orders"
serviceAccountRef:
name: agent-orders
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: agent-orders-db
namespace: ai-agents
spec:
refreshInterval: "1m"
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: agent-orders-db-creds
dataFrom:
- extract:
key: creds/agent-orders-db
The refreshInterval of one minute means ESO re-reads from Vault every minute, picking up a rotated credential. Combined with a five-minute Vault TTL, the materialized Secret is at most one minute stale.
The caveat to be honest about: ESO writes a real Kubernetes Secret. It sits in etcd. Any pod with get secrets permissions in that namespace can read it. Combining a short refreshInterval with Vault dynamic secrets narrows the exposure window significantly, but it does not eliminate the materialized copy at rest. ESO’s model is sync-into-a-Secret, which is appropriate for rotating credentials but not for workloads where the secret must never touch a Secret object.
ESO is the right choice when your backend is not exclusively Vault: if you also read from AWS Secrets Manager, GCP Secret Manager, or Azure Key Vault, ESO’s provider-agnostic API is the cleaner abstraction.
Vault Secrets Operator (VSO) — Vault-native with lease management
VSO is HashiCorp’s own operator, designed around Vault’s lease model. Its API group is secrets.hashicorp.com/v1beta1. The key kinds are VaultAuth (how to authenticate), VaultDynamicSecret (a credential with a managed lease), VaultStaticSecret, VaultPKISecret, and SecretTransformation for templating outputs.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: agent-orders-auth
namespace: ai-agents
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: agent-orders
serviceAccount: agent-orders
audiences: ["vault"]
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: agent-orders-db
namespace: ai-agents
spec:
mount: database
path: creds/agent-orders-db
destination:
name: agent-orders-db-creds
create: true
rolloutRestartTargets:
- kind: Deployment
name: agent-orders
vaultAuthRef: agent-orders-auth
VaultDynamicSecret does something ESO does not: it tracks the Vault lease directly, renewing it before expiry, and when the lease rotates it can trigger a rolling restart of the target Deployment via rolloutRestartTargets. VSO also performs automatic secret drift detection — if the Secret in Kubernetes diverges from what Vault issued, VSO corrects it.
VSO is documented to support Kubernetes versions 1.32 through 1.36 (current as of publication) and has been tested on EKS, GKE, AKS, and OpenShift.
The same caveat applies: VSO still writes a Kubernetes Secret. The difference is that VSO manages the lease lifecycle natively rather than relying on a polling interval.
Zero-secret-at-rest: Agent Injector and direct API
For workloads where a materialized Kubernetes Secret is unacceptable, two paths avoid writing a Secret object entirely.
Vault Agent Injector uses a mutating webhook to inject an init container and sidecar into the pod. The init container authenticates to Vault using the pod’s projected SA token and renders the secret to a shared in-memory volume (backed by emptyDir: {medium: Memory}). The sidecar renews the lease while the pod is running. Nothing lands in a Kubernetes Secret or on persistent disk. The pod reads the credential from a tmpfs path.
Direct Vault API is the simplest model: the agent code authenticates to Vault at runtime using its projected SA token, reads the dynamic credential for one task, holds it only in process memory, and discards it when the task is complete. This gives the shortest possible exposure window but couples the agent code directly to Vault’s HTTP API.
Treat the injector and direct API patterns as the “crown-jewel” tier. Most teams land on ESO or VSO with short TTLs. Reserve zero-at-rest for the specific workloads where a materialized Secret is a compliance failure or unacceptable blast radius.
Comparison
| ESO | VSO | Agent Injector | Direct API | |
|---|---|---|---|---|
| API group | external-secrets.io/v1 | secrets.hashicorp.com/v1beta1 | N/A (sidecar) | N/A |
| Secret materialized? | Yes, Kubernetes Secret | Yes, Kubernetes Secret | No (tmpfs) | No (process memory) |
| Lease management | Poll on refreshInterval | Native lease track + renew | Sidecar renews | App manages |
| Rollout on rotation | No | Yes (rolloutRestartTargets) | No | No |
| Multi-backend | Yes | Vault only | Vault only | Vault only |
| Coupling to Vault | Low | Medium | High | Highest |
The zero-at-rest paths (injector, direct) eliminate the materialized Secret but add coupling and operational complexity. Most teams land on VSO for Vault-centric environments or ESO for multi-backend setups, with TTLs tuned to minutes.
Making Short-Lived the Default
Short-lived credentials are not useful as a one-off for the highest-sensitivity database. The goal is making them the default posture for everything an agent touches.
flowchart TD
A[New secret needed for agent workload] --> B{Multiple secret backends?}
B -->|Yes, multi-cloud or mix| C[ESO: external-secrets.io/v1\nSecretStore + ExternalSecret\nshort refreshInterval]
B -->|No, Vault-only| D{Secret must NEVER materialize\nin a Kubernetes Secret?}
D -->|No, materialized Secret OK| E[VSO: secrets.hashicorp.com/v1beta1\nVaultDynamicSecret\nrolloutRestartTargets]
D -->|Yes, zero-at-rest required| F{Agent code can call Vault API?}
F -->|Yes| G[Direct Vault API\nProjected SA token login\nHold in process memory only]
F -->|No| H[Vault Agent Injector\ntmpfs volume\nsidecar lease renewal]
C --> I[Set default_ttl to minutes\nfor high-blast-radius backends]
E --> I
G --> I
H --> I
I --> J[Per-workload ServiceAccount\none SA per agent role\nno shared identity]
J --> K[Pair with egress NetworkPolicy\nblock pod-to-internet except\napproved endpoints]
Start with whether you need multi-backend support, then work down to whether a materialized Secret is acceptable. At every leaf, tune TTLs down to minutes for production databases and cloud credentials.
Per-workload identity is non-negotiable. Each agent role gets its own Kubernetes ServiceAccount, and each ServiceAccount is bound to exactly one Vault role via bound_service_account_names. No shared service accounts across agent types. When an incident occurs, you know which workload the compromised credential belonged to, and you can revoke that specific role’s leases without touching anything else.
Aggressive TTLs on high-blast-radius backends. The default_ttl on a Vault database role defaults to around one hour. For an agent-touched production database or cloud credential, that is too long. Tune it to minutes. Use max_ttl to cap the ceiling even with renewals. For lower-sensitivity configuration data from KV, longer TTLs are fine.
Dynamic over static wherever a secrets engine exists. The database and cloud secrets engines cover most agent credential needs. Prefer them over KV static secrets for anything an agent touches directly. Reserve KV for configuration values that are not credentials.
Pair with egress control. A short-lived credential that cannot reach the internet from the pod is close to inert even if exfiltrated. A Kubernetes NetworkPolicy that restricts the agent pod to only the specific internal endpoints it legitimately calls means the attacker’s stolen credential is stranded. The agent egress control guide covers the specific NetworkPolicy and Cilium L7 policy patterns that enforce this.
Audit the lease trail. Dynamic credentials are uniquely attributable. Each issuance creates a distinct lease_id tied to the requesting Vault role and token. When an agent behaves unexpectedly, the Vault audit log tells you which workload, which role, what path, and when — without relying on the application to log the credential it used.
Version and API Reference
Current as of publication date (2026-07-15):
- HashiCorp Vault 2.0.3 — released 2026-06-17. The 2.x line went GA at 2.0.0 on 2026-04-14. The Vault 2.x release notes document native AI-agent support as a public beta capability for enterprise customers. This post’s architecture (Kubernetes auth, database secrets engine, ESO/VSO) is built on GA features that have been stable across the 1.x and 2.x lines.
- External Secrets Operator — stable API group
external-secrets.io/v1. Thev1beta1API group is deprecated; migrate any existing CRDs. Confirm the current operator release at github.com/external-secrets/external-secrets/releases. - Vault Secrets Operator — API group
secrets.hashicorp.com/v1beta1. Supported on Kubernetes 1.32-1.36, EKS, GKE, AKS, and OpenShift. - Projected service account tokens — available since Kubernetes v1.22; the
audienceandexpirationSecondsfields used in this post have been stable across subsequent releases.
The load-bearing facts in this post are the API mechanisms: the Kubernetes auth TokenReview flow, the dynamic-secret lease/TTL/revocation model, the external-secrets.io/v1 kind set, and the secrets.hashicorp.com/v1beta1 VSO kinds. These are all generally available and stable. Version numbers will drift; the architecture will not.
Frequently asked questions
How short should a Vault credential’s TTL be for an AI agent?
Short enough that the credential likely expires before an attacker can reuse it off-box. For high-blast-radius backends — production databases and cloud credentials — tune the role’s default_ttl to minutes and let Vault renew the lease for long-running tasks up to max_ttl. Vault deletes the underlying database user when the lease expires, so the credential stops working at the source, not just in the app. For lower-sensitivity configuration data stored in KV, a longer TTL is fine; the minutes-scale guidance applies specifically to dynamic credentials for externally callable resources.
How does an agent authenticate to Vault without a static bootstrap secret?
With the Kubernetes auth method. The pod presents its projected ServiceAccount token to /auth/kubernetes/login along with a role name. Vault validates the JWT against the Kubernetes TokenReview API, confirming the token is current and the service account still exists, then returns a short-lived Vault token scoped to the policies bound to that role. The role is bound to a specific service account and namespace via bound_service_account_names and bound_service_account_namespaces, so identity comes from the workload. There is no shared secret to leak, rotate, or store.
What is the difference between External Secrets Operator and Vault Secrets Operator?
ESO (external-secrets.io/v1) is provider-agnostic: it reads from Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, and many others, syncing values into a native Kubernetes Secret via SecretStore and ExternalSecret resources. VSO (secrets.hashicorp.com/v1beta1) is HashiCorp’s Vault-native operator with first-class dynamic-secret lease management and the ability to trigger a rolling Deployment update when a lease rotates via VaultDynamicSecret. Use ESO if you are multi-backend or already standardized on it; use VSO if you are Vault-centric and want lease-managed rotation baked in at the operator level.
Does using External Secrets Operator mean my secrets are never stored in the cluster?
No. ESO materializes the value as a normal Kubernetes Secret that pods read — that is a real copy in etcd. A short refreshInterval combined with Vault dynamic secrets narrows the exposure window considerably, but the copy exists at rest. For workloads where the secret must never land in a Secret object, use the Vault Agent Injector (renders to an in-memory tmpfs volume) or have the agent read Vault directly, holding the credential only in process memory for the duration of one task.
What Kubernetes feature makes short-lived Vault auth possible?
Projected (bound) service account tokens, available since Kubernetes v1.22. They are audience-scoped (the audience field limits the token to one specific consumer), time-bound via expirationSeconds, automatically rotated by the kubelet before expiry, and invalidated when the pod is deleted. That short-lived, workload-bound token is what Vault’s Kubernetes auth method validates against the TokenReview API — so the entire chain from pod identity to issued credential stays ephemeral, with no human-managed long-lived secret anywhere in the path.