If your AWS CI/CD pipelines authenticate with AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, you have a problem the next SOC 2 auditor will catch — and a problem any attacker who finds your repo will exploit. This is the playbook we use to migrate teams off static credentials in 3 weeks, with zero deploy-pipeline downtime.
Why static credentials in CI/CD are now a hard fail
Static AWS access keys in CI/CD systems used to be the default. They are now an audit failure under multiple frameworks:
- SOC 2 CC6.1 requires logical access controls including credential lifecycle management. An auditor walking your CI/CD will ask where the keys live and when they were last rotated. "We rotate quarterly" without an automated rotation log is a finding.
- NIST 800-53 IA-5 requires authenticator management. Static keys with no expiration violate the control.
- CIS AWS Foundations Benchmark 1.4 requires no root access keys, and 1.13/1.14 require IAM access keys to be rotated within 90 days. Most CI/CD keys we audit haven't been rotated in 18+ months.
- HIPAA §164.312(d) requires authentication procedures. Reused, static, never-rotated credentials don't meet the standard.
The fix everyone recommends and few finish: replace static credentials with OIDC + Workload Identity Federation (WIF). Trust policy on the IAM role allows the CI/CD provider to assume the role with short-lived tokens scoped per workflow. There is no credential to rotate, because there is no credential.
The OIDC + WIF model in 90 seconds
OIDC works in three pieces:
- The CI/CD provider (GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines) issues a signed JWT for each workflow run. The JWT contains claims like the repo, branch, environment, workflow name, and run ID.
- AWS IAM is configured with an OIDC identity provider that trusts the CI/CD provider's signing certificate.
- An IAM role has a trust policy that allows the OIDC provider to assume it — but only when the JWT claims match conditions you specify (e.g., specific repo, specific branch, specific environment).
The CI/CD job runs aws sts assume-role-with-web-identity, gets a 1-hour token, and uses that to call AWS. No long-lived credentials anywhere. No rotation problem because there's nothing to rotate. Audit trail is automatic — every assume-role call lands in CloudTrail with the workflow context.
The migration plan we run
A Secretless CI/CD Sprint at CompliTru runs in three weeks. Below is the structure we use. You can run it yourself if you have the cycles; we ship it for $15K–$22K when teams want it done in 3 weeks with the audit-ready evidence package included.
Week 1: Inventory and trust foundation
Day 1–2: CI/CD credential inventory. List every credential currently used by every workflow. Don't just look at GitHub Actions secrets — also look at: - Repo-level secrets (in GitHub Actions, GitLab CI variables) - Organization-level secrets - Self-hosted runner credentials in EC2 instance profiles - Codepipeline source stage credentials (often missed) - Terraform Cloud / Spacelift / Atlantis credentials - Container registry push credentials - Lambda deployment role keys
Map each credential to: (1) its IAM permissions, (2) its rotation date, (3) the workflows that consume it, (4) the AWS account it targets.
Day 3: Establish the OIDC provider in each AWS account.
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
(The thumbprint is GitHub's, current as of 2026. Always re-verify before use — GitHub has rotated this thumbprint twice in the past 3 years.)
For GitLab self-hosted: the issuer URL is your GitLab instance, and the OIDC discovery doc is at <gitlab>/.well-known/openid-configuration.
Day 4–5: Design the role architecture.
The cardinal mistake we see: one wide-permission role that every workflow assumes. This collapses the security benefit of OIDC by re-creating a "god role" attached to a less-secure auth method.
The correct pattern: one role per (workflow purpose × environment). For a typical mid-market team you'll end up with 5–15 roles per AWS account:
gha-deploy-frontend-prodgha-deploy-frontend-staginggha-deploy-backend-prodgha-deploy-backend-staginggha-run-tests-cigha-publish-ecr-prodgha-publish-ecr-staginggha-deploy-infra-terraform
Each role's trust policy uses condition keys to constrain WHICH workflow can assume it:
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::ACCT:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:ORG/REPO:environment:production"
}
}
}
Critical: Use StringEquals for the sub claim, not StringLike with wildcards, unless you have a specific reason. StringLike with repo:ORG/*:* is the most common security regression we find post-migration — it grants the role to any workflow in the org, defeating the segmentation.
Week 2: Migration in waves
Day 6–7: Pilot workflow.
Pick one workflow that's important but not production-critical (a staging deploy is ideal). Modify it to use OIDC:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCT:role/gha-deploy-backend-staging
aws-region: us-east-1
- run: aws sts get-caller-identity
- run: ./deploy.sh
Verify the assume-role call hits CloudTrail with the expected userIdentity.sessionContext.sessionIssuer showing the role and requestParameters showing the OIDC subject.
Day 8–10: Production workflows.
Migrate workflows one at a time. After each migration:
- Run the workflow against a real change, not a dummy commit. Many workflows have hidden permission requirements that only trigger on real deploys (e.g., S3 putObject on an artifact upload).
- Watch CloudTrail for AccessDenied errors over the next 24 hours.
- Keep the old static-credential path enabled but with a feature flag to fall back; remove only after 7 days of clean runs.
Day 11–14: Self-hosted runners and edge cases.
Self-hosted runners (especially on EC2) are the gnarliest part of the migration. Two patterns:
- Runner uses EC2 instance profile, workflow uses OIDC. The runner machine has minimal IAM (just enough to register with GitHub/GitLab and pull workflow code). The workflow inside it does its own
assume-role-with-web-identity. This is the cleanest model. - Runner uses OIDC via the runner controller. ARC (Actions Runner Controller) on EKS supports OIDC at the runner pod level via IRSA. More complex but more secure.
Ephemeral runners always preferred over persistent. A persistent runner that has accumulated cached AWS credentials is exactly the surface area you're trying to remove.
Week 3: Decommission and evidence
Day 15–17: Static credential decommissioning.
For every static credential the inventory captured in Week 1: 1. Confirm via CloudTrail that the corresponding access key has not been used in the past 7 days. 2. Deactivate the access key (don't delete yet — keep it in deactivated state for 14 days as a rollback.) 3. After 14 days of clean OIDC operation, delete the access key.
Day 18–19: Drift monitoring.
Set up alarms for:
- Any new IAM access key creation (CloudWatch event on iam:CreateAccessKey)
- Any IAM user with active access keys (daily Config rule)
- Any role with overly-permissive trust policy (StringLike on the sub claim)
- Any OIDC role assumed by an unexpected sub claim pattern
The single highest-value alarm: any iam:CreateAccessKey event on an IAM user category that should never have static keys. We see migrations slowly regress over 6–12 months when an engineer in a hurry creates a static key for "quick debugging." Drift monitoring catches this within hours.
Day 20–21: Audit evidence package.
The deliverable that closes SOC 2 / NIST CC6.1 cleanly:
- Before/after credential inventory. N static keys before, 0 static keys after.
- OIDC provider configuration screenshots for each AWS account.
- IAM trust policies for each new role, with
subclaim conditions documented. - CloudTrail samples showing OIDC-based assume-role calls during real deploys.
- Drift monitoring runbook documenting the alarms and ownership.
- Rollback procedure documenting how to re-enable a static key under emergency.
Auditors love this package. It answers their question before they ask it.
The 3 mistakes we keep seeing
1. Wildcard sub claims in trust policies.
Engineers running the migration in a hurry write repo:ORG/*:ref:refs/heads/main. This grants the role to any repo in the org. We've seen migrations technically remove all static credentials but introduce wider blast radius than they removed.
Fix: pin to specific repos at minimum, ideally specific environments. If you have many repos, generate the policies programmatically rather than wildcard.
2. Forgotten Terraform Cloud / Atlantis credentials.
OIDC migrations focus on GitHub Actions / GitLab CI. The Terraform CI step often runs in a separate platform (Spacelift, Terraform Cloud, Atlantis) and gets missed. These platforms now also support OIDC — migrate them at the same time.
3. Static keys hidden in Secrets Manager / Parameter Store.
You "migrated to OIDC" but a runtime application is still reading IAM credentials from Secrets Manager. Those credentials are static. They're just behind a different access layer. The migration isn't complete until runtime services use IAM roles for service accounts (IRSA on EKS, EC2 instance profiles, Lambda execution roles, ECS task roles) rather than fetching keys from a vault.
Drift monitoring after migration
The migration is the easy part. Keeping it clean for 12+ months is the hard part. The pattern that maintains it:
- Weekly: Run a
iam list-users --query "Users[*].UserName"cross-referenced withaws iam list-access-keys. Any user with active keys gets a Jira ticket. - Monthly: Pull all IAM roles with OIDC trust policies. Audit the
subclaim conditions for newly-introduced wildcards. - Quarterly: Review CloudTrail for any
assume-role-with-web-identitycalls from unexpectedsubpatterns.
This cadence is built into the Secretless CI/CD Sprint as a 90-day drift monitoring deliverable.
When to do this in-house vs hire
You can do this in-house if you have: - A senior platform engineer with 3+ days of OIDC familiarity - Bandwidth to run the migration without it deprioritizing for two weeks - Stakeholder buy-in for the audit-evidence package generation work (the part nobody wants to do)
You should hire it out if: - Your audit window is in the next 90 days and this is a known finding - Your CI/CD environment spans multiple platforms (GitHub Actions + GitLab + CircleCI is common in M&A scenarios) - You're running 5+ AWS accounts under Organizations and need consistent role architecture across all of them - The static-credential surface is large enough that drift monitoring is non-trivial to design
CompliTru's Secretless CI/CD Sprint runs this end-to-end for $15K–$22K in 3 weeks with the full evidence package and 90-day drift monitoring included. Read more at services.complitru.ai or book a 15-min scoping call.
Related sprints: - IAM Least-Privilege & CIEM Sprint — usage-driven IAM tightening, $12K–$18K - Exposure Kill List & Rapid Patch — find what attackers will find first, $12K - SOC 2 Readiness Assessment — full AWS-to-trust-criteria mapping, $5K–$8K
Free starting point: Free AWS Credential Scan — see every static credential in your AWS environment in 10 minutes.