Field Note 9 current
No long-lived cloud keys; workloads authenticate by federated identity
TL;DR
Don’t use long-lived static cloud credentials anywhere — no AWS access key ID / secret access key
pairs and no downloaded GCP service-account JSON keys, in source, in a secrets manager, in CI, or in
environment variables. Every workload authenticates to its cloud using the identity its runtime
already provides (instance/pod role via the metadata server: IAM roles / IRSA / EKS Pod Identity
on AWS; the attached service account and metadata server on GCP). When a workload needs to reach
another cloud or provider, it exchanges that native identity — a short-lived OIDC token the
runtime mints for it — for short-lived target-cloud credentials via Workload Identity Federation
(GCP STS token exchange) or AssumeRoleWithWebIdentity (AWS STS). No key is ever stored; the
workload proves who it is and is handed minutes-long credentials on demand.
Developer obligations: never create, download, commit, or paste a static cloud key; never add
AWS_SECRET_ACCESS_KEY or a GOOGLE_APPLICATION_CREDENTIALS JSON file to a service. Wire new
workloads to a role/service account and, for cross-cloud access, to a federation trust. If you
believe a static key is genuinely unavoidable (a third party with no OIDC federation path), it’s a
carve-out — see below.
Context
Running across more than one cloud, and splitting into independent services, multiplies the number of workloads that need to talk to cloud resources — databases, object storage, queues, KMS, secret stores — both in their own cloud and across the boundary to another. The question this note settles is how a workload proves who it is to a cloud provider.
The default, path-of-least-resistance answer is a static key: generate an AWS access key pair or download a GCP service-account JSON, drop it in a secret or an env var, and the SDK picks it up. It works on the first try, which is exactly why it spreads. It is also the single most common cause of serious cloud breaches: a long-lived key is a bearer secret with no expiry that grants standing access. Once minted it tends to be copied — into CI, onto a laptop, into a second service “just to unblock” — and every copy is a place it can leak. Static keys are hard to rotate (rotation breaks whatever still holds the old one, so it doesn’t happen), hard to attribute (the key is the identity; there’s no workload behind it in the audit log), and hard to scope down once things depend on them.
The clouds have closed the gap that made static keys necessary. Every workload already has a strong, provider-issued identity available with no stored secret:
- On AWS, the instance/task/pod assumes an IAM role; the SDK fetches short-lived credentials from the metadata server (EC2 IMDSv2, ECS task roles, IRSA / EKS Pod Identity). Nothing is stored on disk.
- On GCP, a workload runs as an attached service account and the metadata server hands it short-lived tokens; downloaded service-account keys are explicitly discouraged by Google.
The remaining hard case is cross-cloud: a workload in one cloud that needs a resource in another
(or an external OIDC-aware provider). Both clouds can federate an external identity: the
workload’s runtime mints a short-lived OIDC token asserting its identity, and the target cloud
is configured to trust that issuer and exchange the token for its own short-lived credentials —
AWS sts:AssumeRoleWithWebIdentity trusting the source OIDC issuer, or GCP Workload Identity
Federation trusting an AWS/OIDC principal. The credential that crosses the boundary is a
minutes-long token, not a stored key, and it’s bound to a specific workload identity that shows up
in the audit trail.
This is distinct from how services authenticate to each other (a platform-owned workload-identity mechanism, and sender-constrained tokens — separate notes). This note is about authenticating to a cloud provider. They’re complementary layers; a single service typically uses both.
Recommendation
Authenticate to cloud providers using federated workload identity, never long-lived static credentials. Concretely:
- No static cloud keys exist anywhere. No AWS access key pairs and no downloaded GCP service-account JSON keys committed to source, stored in a secrets manager as a standing credential, placed in CI config, baked into an image, or set in an environment variable. This holds for production, staging, developer, and CI environments alike.
- In-cloud access uses the runtime’s native identity. Each workload is assigned a least-privilege role / service account and obtains short-lived credentials from the metadata server. Code uses the default SDK credential chain — never configured to read a static key.
- Cross-cloud and external access uses identity federation. Exchange a runtime-minted OIDC token
for short-lived target credentials:
AssumeRoleWithWebIdentity(AWS as target) or Workload Identity Federation / STS token exchange (GCP as target). The trust is configured between the source identity provider and the target role/pool; no key is stored on either side. The federated role is scoped to the minimum the workload needs. - Human access follows the same rule. Reach cloud resources through SSO → short-lived role
credentials (AWS IAM Identity Center /
aws sso, GCP identity), never a personal long-lived key. - CI authenticates by federation too. CI jobs use the CI provider’s OIDC identity federated into
a scoped role (e.g. GitHub Actions OIDC →
AssumeRoleWithWebIdentity), not stored cloud secrets.
Carve-out
A static key is permitted only when a dependency offers no federation path — a third-party API that authenticates solely by a long-lived secret and supports neither OIDC nor a role-assumption flow. Where that’s genuinely the case, every one of the following must hold, following the carve-out discipline in ZFN-3:
- Documented at the use site. A comment where the secret is consumed states which provider, why federation is impossible, and links this note — greppable for a reviewer and a future auditor.
- Never configured into the application directly — fetched at runtime by secret path. The secret lives only in the managed secret store (AWS SSM Parameter Store / Secrets Manager, GCP Secret Manager), and the workload reads it at runtime using its own federated identity, referencing the secret by path, not value. The app’s source, image, config, and environment contain the path and the access grant, never the secret — so the only credential the app holds is its keyless workload identity, which is exactly what fetches the static one. Rotation is enabled and verified, and the workload supports a refresh signal (TTL, event, or version-change notification) so a rotated value is picked up without a redeploy. A secret pasted into env or config, even from the store at deploy time, defeats the carve-out.
- Scoped to the minimum, used by exactly the one workload that needs it — no sharing, no copying into a second service.
- Tracked. Each carve-out is registered so the set of standing static secrets is a known, audited list, revisited when the third party gains a federation path.
A static key without all four is a defect, not an exception.
Consequences
Easier:
- The highest-severity cloud-breach class — a leaked long-lived key — largely disappears. There’s no standing secret to exfiltrate; a stolen credential expires in minutes and is bound to a workload identity.
- Rotation stops being a project. Short-lived credentials rotate themselves continuously.
- Audit and attribution improve: every cloud action ties back to a named role / federated workload identity, not an anonymous key.
- A single, consistent credential model across both clouds and CI.
Harder:
- Federation has real setup cost and a learning curve — OIDC issuer trust, audience/subject conditions, role-assumption policies — fiddlier than pasting a key, and misconfigured trust conditions can be subtly too broad.
- Local development and scripts can’t use a downloaded key as a shortcut; you authenticate via SSO / short-lived credentials and emulators.
- Some third parties genuinely don’t federate, and the carve-out carries ongoing obligations.
- Short-lived credentials must be refreshed; long-running or hot-path callers must handle expiry/refresh correctly (the SDKs do most of this).
New obligations:
- New workloads are wired to a role / service account and, for cross-cloud needs, a federation trust. Adding a static cloud key requires the carve-out above, not a local choice.
- Infrastructure review covers federation trust conditions (issuer, audience, subject) the way it covers any security boundary; “too broad” trust is a review finding.
- Maintain the registry of approved carve-outs and revisit each when the third party gains a federation path.
- A standing, non-urgent obligation to migrate existing static keys to federation as you touch that code; new code starts federated.
References
- ZFN-3 — the carve-out-with-documentation-at-the-use-site pattern reused here.
- ZFN-2 — security-first ordering that motivates eliminating standing bearer secrets.
- ZFN-10 — once a workload has credentials, verify the target of an authorized call is owned by who you expect.
- AWS:
AssumeRoleWithWebIdentity; GCP: Workload Identity Federation and why to avoid service-account keys. - GitHub Actions OIDC → cloud for keyless CI.
Changelog
- 2026-06-12: First published as a Field Note.