Static keys leak and live forever. Short-lived credentials from STS and Vault expire on their own — here's the token-exchange machinery and the TTL math that make it work.
A static access key has no idea it's been stolen. It works for the attacker exactly as well as it works for you, and it keeps working until a human notices and revokes it. That's the whole problem in one sentence. The fix isn't better rotation — it's credentials that expire on their own, minted the moment you need them and dead an hour later. Once you internalize that, most of the secrets-in-CI, keys-in-git, and token-in-Slack incidents stop being possible.
This is the machinery underneath keyless authentication: the token exchange that hands you temporary credentials, and the lease model that reclaims them.
The pattern is always the same. You show up holding something that proves who you are — an OIDC token from your CI provider, a signed instance identity, an existing session — and a broker hands back a fresh set of credentials with an expiry stamped on them. You never store the result. When it dies, you exchange again.
AWS STS is the canonical broker. AssumeRole takes an existing AWS identity and returns temporary creds for a role. AssumeRoleWithWebIdentity takes an OIDC token from outside AWS — GitHub Actions, a Kubernetes service account, GCP — and does the same with no stored AWS key anywhere in the chain.
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::123456789012:role/ci-deploy \
--role-session-name gha-build-4192 \
--web-identity-token "$OIDC_TOKEN" \
--duration-seconds 3600
What comes back is the important part:
{
"Credentials": {
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"SessionToken": "...",
"Expiration": "2026-07-12T15:04:07Z"
}
}
Three things instead of two. The SessionToken is what marks these as temporary — a request without it is rejected, and it's baked with the same expiry. The ASIA prefix (versus AKIA for static keys) is a giveaway you can grep for in logs and CloudTrail to confirm nobody's using long-lived keys where they shouldn't.
--duration-seconds defaults to one hour and caps at the role's MaxSessionDuration (up to 12 hours). Role chaining — assuming a role from an already-assumed session — hard-caps at one hour regardless of what you ask for, which trips people up constantly.
The instinct is to crank duration up so nothing expires mid-job. Resist it. A shorter session is a smaller blast radius. If a token leaks, the attacker's window is measured against how long it had left, not against how long until someone rotates a key. For CI I set 15–30 minutes for most jobs. If a deploy legitimately runs longer than an hour, that's usually a signal the job should be split, not that the session should live half a day.
STS handles AWS. For everything with a username and password — Postgres, MySQL, RabbitMQ, cloud APIs — HashiCorp Vault does the equivalent with dynamic secrets. Instead of storing a database password, the app asks Vault at request time and Vault creates a brand-new database user, scoped to a role, with a lease and a TTL. When the lease expires (or you revoke it), Vault runs the revocation SQL and the user is gone.
# connection: Vault holds ONE privileged cred, apps never see it
resource "vault_database_secret_backend_role" "app_ro" {
backend = "database"
name = "app-ro"
db_name = "app-postgres"
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
]
revocation_statements = ["DROP ROLE IF EXISTS \"{{name}}\";"]
default_ttl = "1h"
max_ttl = "24h"
}
Reading a credential is one call:
$ vault read database/creds/app-ro
Key Value
lease_id database/creds/app-ro/8Yx...
lease_duration 1h
username v-token-app-ro-x7Qp2f9k
password A1b-c3D...
Every app instance gets a distinct username. That alone changes incident response — a leaked credential is traceable to exactly one workload and one lease, and revoking it touches nothing else.
Rotation is a batch job that pretends to be security. Between rotations, the credential is just as static and just as valid as before. You're betting that 30 days is short enough to matter and long enough not to break anything, and you're usually wrong in both directions.
TTL flips the default. The credential is dead unless something actively keeps renewing it. There's no rotation calendar to miss, no "we forgot this key existed" three years later. Revocation is immediate and per-lease instead of a mass invalidation that takes out half the fleet. Leak detection becomes less urgent because the leak has an expiry baked in.
You don't call STS on every API request — that's a throttle waiting to happen and it adds latency to everything. The AWS SDKs cache the session in memory and refresh it automatically a few minutes before Expiration. Vault's Agent and the various SDK helpers do the same with lease renewal. If you're wiring this yourself, the rule is refresh before expiry, not on the failure. Waiting for the first 403 to trigger a refresh means real requests eat the failure while the retry warms up.
A few things that will bite you:
SecretAccessKey, SessionToken, and Vault passwords at the logging boundary.Kill static keys everywhere they can be killed, and make short-lived the only path that works — not the recommended one. STS for anything talking to AWS, Vault dynamic secrets for databases and everything else with a password. Set sessions to the shortest duration the workload tolerates, run NTP so expiry math stays honest, and grep your logs for AKIA and raw secrets until both come back empty. The goal isn't credentials that rotate faster. It's credentials that a thief inherits already half-dead.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A request leaving a laptop somehow lands on a server 20ms away. Here is what actually decides which point of presence answers.
GitHub Actions OIDC gets all the attention, but GitLab, Buildkite, and CircleCI issue the same signed tokens. Here's how to trust them without opening a hole.
Explore more articles in this category
The edge is fast because it's constrained. This is the decision map for what belongs at the edge, what belongs at origin, and how compute, data, caching, and auth fit together.
Static keys leak. The question isn't if but how fast you notice and how clean your response runbook is when the pager goes off.
Edge code runs in hundreds of PoPs, lives for milliseconds, and gives you no shell. Here's how we get logs, traces, and metrics out of it anyway.