OIDC, Workload Identity Federation, and the End of Long-Lived CI Secrets

Paul Wechuli
March 18, 2026
github-actions
oidc
security
aws
azure
gcp
devops

How GitHub Actions OIDC tokens replace static cloud credentials, what the trust policy actually does, and the misconfigurations that quietly leave you wide open.

OIDC, Workload Identity Federation, and the End of Long-Lived CI Secrets

If your CI still has an AWS access key, a GCP service account JSON, or an Azure client secret stored as a repository secret in 2026, you are running an outdated and dangerous pattern. OIDC-based workload identity federation has been production-ready across all three major clouds for years now, and the migration is no longer a heroic project — for most repos it's a one-afternoon swap.

This post is the version of the explanation I wish existed when I did this migration the first time. It covers AWS, but the same model applies to GCP Workload Identity Federation and Azure federated credentials with very minor surface changes.

What the OIDC token actually is

When a workflow runs, GitHub will — on request — mint a short-lived JWT signed by https://token.actions.githubusercontent.com. The claims inside are interesting:

{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:my-org/payments-api:ref:refs/heads/main",
  "repository": "my-org/payments-api",
  "repository_owner": "my-org",
  "workflow": "Deploy",
  "actor": "wechuli",
  "environment": "production",
  "job_workflow_ref": "my-org/ci-workflows/.github/workflows/deploy.yml@refs/tags/v3",
  "ref": "refs/heads/main",
  "exp": 1742310000
}

The cloud provider then exchanges this JWT for a short-lived cloud credential, but only if its trust policy says the claims are acceptable. The entire security model lives in that trust policy. If you write it loosely, OIDC is worse than a static secret because it's invisible.

The trust policy is where mistakes happen

Here is a trust policy I see in the wild far too often:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/*"
        }
      }
    }
  ]
}

This says: any workflow, on any branch, in any repo under my-org can assume this role. That includes:

  • A workflow on an attacker's PR branch that they got merged
  • A workflow in a new repo someone just created
  • A reusable workflow called from a fork

Every one of those scenarios has shown up in real incident reports. The fix is to constrain the sub claim much more tightly:

"StringEquals": {
  "token.actions.githubusercontent.com:sub":
    "repo:my-org/payments-api:environment:production"
}

Now only workflow runs in my-org/payments-api that target the production environment can assume the role. Combine with branch protection on the environment, and you have a credential that is genuinely scoped to "production deploys of this service."

The job_workflow_ref claim is the one that actually matters

For platform teams who own shared reusable workflows, the most valuable claim isn't sub or repository. It's job_workflow_ref. It identifies exactly which workflow file at which version is currently running. You can write a trust policy that says:

Only the job that comes from my-org/ci-workflows/.github/workflows/deploy.yml at tag v3 may assume this role.

"StringEquals": {
  "token.actions.githubusercontent.com:job_workflow_ref":
    "my-org/ci-workflows/.github/workflows/deploy.yml@refs/tags/v3"
}

Combined with sub scoping by repo and environment, this is the strongest practical guarantee you can give: only deploys via the blessed pipeline, in the blessed repo, in the blessed environment, can touch production. A developer cannot bypass the platform team's workflow by writing their own, because their custom workflow will have a different job_workflow_ref and the role will refuse.

The workflow side is boring (good)

The configure-aws-credentials action handles the exchange:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/PaymentsApiDeploy
          aws-region: us-east-1
      - run: aws sts get-caller-identity

The two things people miss:

  1. id-token: write is required. If you forget it, you get a confusing 403 from STS, not a clear "you didn't request a token" error.
  2. environment: production matters if your trust policy references the environment claim. The environment is only set on jobs that declare it.

Migrating without a flag day

The pattern I recommend:

  1. Set up the OIDC provider and a new IAM role with the OIDC trust policy, alongside the existing IAM user.
  2. In your workflows, switch to configure-aws-credentials with the new role.
  3. Run for a week. Watch CloudTrail for any AssumeRoleWithWebIdentity failures.
  4. Once clean, rotate the old IAM user's access keys to confirm nothing still uses them.
  5. Delete the IAM user.

The rotation step is the one people skip. Don't. Rotating is a free check that you actually migrated everything — including the workflows you forgot about in archived repos that still run nightly.

What to audit right now

If you adopt nothing else from this post, run these three audits this week:

  • Any IAM role trusting token.actions.githubusercontent.com whose sub condition uses StringLike with a wildcard. Tighten it.
  • Any GitHub repository secret named AWS_ACCESS_KEY_ID, GCP_SA_KEY, or AZURE_CLIENT_SECRET. These should not exist in 2026.
  • Any reusable workflow that assumes a privileged role. Make sure the role's trust policy pins job_workflow_ref to that specific workflow path and tag.

Static cloud credentials in CI are now the modern equivalent of leaving SSH keys in a public S3 bucket. OIDC is not just better — it's the baseline.