Skip to main content
INS // Insights

Replacing AWS Access Keys With OIDC in CI/CD

Updated July 2026 · 4 min read

Static AWS access keys stored as CI/CD pipeline secrets are one of the most common standing-credential risks we find during engagements — a key pair created once, pasted into a pipeline's secret store, and left valid indefinitely while the pipeline runs hundreds or thousands of times over its lifetime.

Why This Specific Pattern Is High-Risk

A CI/CD deployment key typically has meaningful permissions — the ability to push to production infrastructure, deploy Lambda functions, or modify IAM itself in some pipeline designs. If that key leaks (a misconfigured log that echoes environment variables, a compromised third-party GitHub Action, a laptop with cached credentials), the blast radius is the same as a compromised deployment engineer's full access, but with no MFA challenge and often weaker monitoring than a human login would trigger.

The OIDC Federation Reference Pattern

The fix replaces the static key with a trust relationship between your CI/CD platform's OIDC token issuer and an AWS IAM role. The pipeline authenticates by presenting a signed OIDC token; AWS validates it against the trust policy and issues short-lived temporary credentials scoped to that specific role — no key ever stored anywhere.

Step 1: Create the OIDC identity provider in AWS (once per account):

resource "aws_iam_openid_connect_provider" "github_actions" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

Step 2: Define a tightly-scoped trust policy for the deployment role:

resource "aws_iam_role" "gha_deploy" {
  name = "gha-deploy-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Federated = aws_iam_openid_connect_provider.github_actions.arn }
      Action    = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" }
        StringLike   = { "token.actions.githubusercontent.com:sub" = "repo:org/prod-service:ref:refs/heads/main" }
      }
    }]
  })
}

Step 3: Reference the role in the pipeline — no key material anywhere:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-deploy-role
          aws-region: us-west-2
      - run: aws lambda update-function-code --function-name prod-service --zip-file fileb://build.zip

The sub claim condition is doing the real security work here — it scopes trust to a specific repository and branch, so even if an attacker somehow forged a token from a different repo under the same GitHub organization, the trust policy would reject it.

GitLab CI and Other Platforms Follow the Same Pattern

GitLab CI, CircleCI, and most modern CI/CD platforms support OIDC token issuance following the same underlying model — a signed JWT with claims identifying the specific pipeline, project, and ref, validated by AWS against a configured trust policy. The specific claim names differ by platform, but the architecture is identical.

Migrating an Existing Pipeline Without an Outage

Run both authentication methods in parallel initially — keep the static key valid but stop using it in the pipeline configuration, switch to the OIDC role, and monitor CloudTrail for any continued use of the old access key ID. Once you've confirmed zero usage over a meaningful window (a full deployment cycle at minimum, ideally longer), revoke the static key permanently.

Auditing for Remaining Static Keys Across an Organization

For organizations with multiple pipelines and multiple AWS accounts, a periodic audit catches keys that individual teams forgot to migrate:

aws iam list-users --query 'Users[*].UserName' | \
  xargs -I{} aws iam list-access-keys --user-name {} \
  --query 'AccessKeyMetadata[?Status==`Active`]'

Combine this with CloudTrail-based last-used data to prioritize which remaining keys represent active risk versus already-dormant, forgotten credentials safe to revoke immediately.

Frequently Asked Questions

Does this approach work for self-hosted CI/CD runners, not just cloud-hosted ones?

Yes, as long as the runner can obtain a signed OIDC token from a trusted issuer — self-hosted GitLab and Jenkins with appropriate OIDC plugin configuration can federate the same way, though the setup requires configuring your own token issuance correctly.

What if our CI/CD platform doesn't support OIDC federation yet?

Some platforms are still catching up. For those, minimize risk by scoping the static key as narrowly as possible, storing it in a dedicated secrets manager rather than plain CI variables, and rotating it on a defined schedule while planning a migration once federation support lands.

Can a single OIDC trust policy support multiple repositories or pipelines?

Yes, using a broader StringLike condition on the sub claim, but this trades off some security granularity — a compromised token from any matching repo could assume the role. Prefer one role per repository/environment combination when the risk profile justifies the extra setup.

How do we prove to an auditor that this migration eliminated a standing credential?

Show the trust policy configuration, the CI/CD pipeline configuration referencing the federated role (with no key material present), and CloudTrail evidence that the previously-used static access key ID has zero activity following the cutover date.

Does OIDC federation add latency to pipeline runs?

The additional token exchange step adds a small amount of latency (typically well under a second), which is negligible compared to the overall pipeline runtime for most deployment workflows.


Talk to us about your credential elimination pilot → rutagon.com/contact or call 907-841-8407.