---
id: 9
title: "No long-lived cloud keys; workloads authenticate by federated identity"
status: current
date: 2026-06-12
authors:
  - "Theo Zourzouvillys"
tags: [security, auth, infra, cloud]
summary: "No static AWS or GCP keys anywhere — not in code, secret stores, or env. Workloads use their runtime's own identity and cross clouds by exchanging it (OIDC) for short-lived credentials via federation. Static keys are a documented carve-out only."
supersedes: null
superseded_by: null
aliases: []
---

## 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](/zfn/3-default-encrypt-internal-traffic/):

1. **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.
2. **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.
3. **Scoped to the minimum**, used by exactly the one workload that needs it — no sharing, no
   copying into a second service.
4. **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](/zfn/3-default-encrypt-internal-traffic/) — the carve-out-with-documentation-at-the-use-site pattern reused here.
- [ZFN-2](/zfn/2-engineering-priority-ordering/) — security-first ordering that motivates eliminating standing bearer secrets.
- [ZFN-10](/zfn/10-verify-resource-owner/) — once a workload has credentials, verify the *target* of an authorized call is owned by who you expect.
- [AWS: `AssumeRoleWithWebIdentity`](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html); [GCP: Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) and [why to avoid service-account keys](https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys).
- [GitHub Actions OIDC → cloud](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) for keyless CI.

## Changelog

- **2026-06-12**: First published as a Field Note.
