Replace Long-Lived AWS Keys With OIDC
Your CI has AWS keys that never expire. Attackers know that too.
Most pipelines still authenticate to AWS with a long-lived access key ID and secret stored as a CI secret. That pair is a standing target: it works from anywhere, it works forever, and nothing about using it looks abnormal. Leak it once, in a log line, a forked workflow, or a compromised action, and it keeps working until a human notices and rotates it. OpenID Connect (OIDC) removes the stored credential entirely.
Why standing keys are the problem
A stored access key is a bearer credential with no expiry and no context. Whoever holds the string is you. The common leak paths are boring and constant:
- A workflow prints the environment during debugging.
- A third-party action you pinned by tag gets hijacked and exfiltrates secrets.
- A pull-request workflow from a fork gains access to repository secrets.
- The key lands in a build artifact or container layer and ships.
Because the key never expires, detection is the only defense, and detection is slow. You are betting your cloud account on nobody making a mistake, indefinitely.
The mechanism: OIDC role assumption
With OIDC, your CI provider (GitHub Actions, GitLab, etc.) issues a signed, short-lived identity token for each workflow run. You configure an IAM role in AWS that trusts that provider and restricts which repository, branch, or environment may assume it. At run time the workflow exchanges the token for temporary credentials via AWS STS.
# GitHub Actions
permissions:
id-token: write # allow the workflow to request the OIDC token
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/ci-deploy
aws-region: us-east-1
No aws-access-key-id or aws-secret-access-key anywhere. The trust policy on the IAM role scopes it down so only your repo on your branch can assume it:
{
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main" }
}
}
The payoff
| Static keys | OIDC role assumption | |
|---|---|---|
| Stored secret | access key + secret in CI | nothing stored |
| Lifetime | until manually rotated | minutes (STS session) |
| Scope | full user, from anywhere | one role, one repo/branch |
| Leak blast radius | works forever | already expired |
Nothing is stored, so there is nothing to leak. The credentials expire on their own, so a stolen token is worthless minutes later. Once OIDC is live, delete the static keys.
Power moves
- Scope the trust policy tightly. Pin
subto a specific branch or GitHub Environment, not the whole org. A loose wildcard lets any repo assume the role. - Give the role least privilege. OIDC removes the standing key, but the role's IAM policy still defines blast radius. Grant only the actions the pipeline needs.
- Shorten the session. Set
role-duration-seconds(or the role's max session duration) to the smallest value your job needs. - Delete the old keys and alert on their use. After migrating, deactivate the IAM user keys and add a CloudTrail alarm so any future use of them is an incident, not a surprise.
Resources
- Configuring OpenID Connect in AWS — GitHub Docs
- IAM roles for identity providers and federation — AWS docs
- AWS STS
AssumeRoleWithWebIdentity aws-actions/configure-aws-credentials- GitLab OpenID Connect with AWS
Shipping AI or cloud systems? Yeda AI audits and hardens production LLM and container pipelines. Talk to us · Read the blog