GitHub Actions OIDC to AWS and Azure without long-lived keys
Replace stored cloud access keys and client secrets in GitHub Actions with short-lived OIDC federation, and scope the trust to an exact repository, branch, or environment.
- Admin access to the GitHub repository (settings and environments)
- IAM permissions to create an OIDC provider and roles in the target AWS account
- Rights to add federated credentials to an Entra app registration or a user-assigned managed identity, plus a role assignment on the Azure subscription
- The AWS CLI and Azure CLI
GitHub Actions can authenticate to AWS and Azure with a short-lived OIDC token instead of a stored access key or client secret. The job requests a signed JWT from GitHub, the cloud verifies it against the issuer https://token.actions.githubusercontent.com, checks the aud and sub claims against a trust you configured, and returns credentials valid for that job only. The end state of this recipe: one AWS IAM role and one Entra identity that only a named repository's production deployment can use, and no long-lived cloud keys in GitHub secrets. This is workload identity federation applied to CI, the same pattern trusted publishing uses for package registries.
The security of the whole setup rests on one control: the sub claim condition. Get it wrong and any workflow in your organization, or anyone's organization, can assume the role.
Before you start: the sub claim
GitHub documents the default subject formats:
| Trigger | sub value |
|---|---|
| Job with an environment | repo:ORG/REPO:environment:NAME |
| Branch | repo:ORG/REPO:ref:refs/heads/BRANCH |
| Tag | repo:ORG/REPO:ref:refs/tags/TAG |
| Pull request | repo:ORG/REPO:pull_request |
If a job references an environment, the environment format applies and the branch is not in sub. Use environments for deployments and add deployment branch rules and required reviewers to them, which AWS also recommends.
One 2026 change matters. For repositories created after July 15, 2026, or that opted in to immutable subject claims, GitHub's AWS guide shows owner and repository IDs in the subject, such as repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main. That format is not available on GitHub Enterprise Server. Decode a real token from your repository before writing conditions.
Why this matters: in the tj-actions/changed-files compromise, a poisoned action printed CI secrets into public logs, and in the GhostAction wave hijacked accounts pushed workflows that dumped Actions secrets. A stored key leaks once and works until rotated. A federated role scoped to environment:prod gives a pushed rogue workflow on a feature branch nothing. See secrets in repositories and CI.
1. Create the GitHub OIDC provider in AWS
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com
--thumbprint-list is optional. AWS verifies the JWKS endpoint's TLS certificate with its library of trusted root CAs and uses thumbprints only as a fallback.
Check: aws iam list-open-id-connect-providers returns arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com.
Common failure: an error that the provider already exists. Each URL can be registered once per account, so reuse the existing provider.
Rollback: aws iam delete-open-id-connect-provider --open-id-connect-provider-arn <arn>.
2. Create the AWS role with an exact sub condition
trust.json:
{
"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",
"token.actions.githubusercontent.com:sub": "repo:example-org/payments-api:environment:prod"
}
}
}]
}
aws iam create-role --role-name gha-payments-prod-deploy \
--assume-role-policy-document file://trust.json
aws iam attach-role-policy --role-name gha-payments-prod-deploy \
--policy-arn arn:aws:iam::123456789012:policy/payments-deploy
Check: aws iam get-role --role-name gha-payments-prod-deploy --query Role.AssumeRolePolicyDocument shows StringEquals on both keys.
Common failure: reaching for StringLike with repo:example-org/*. IAM rejects a sub condition that is missing or only a wildcard, but it accepts repo:example-org/* or repo:example-org/payments-api:*. The first trusts every repository in the org; the second trusts every branch and pull request in the repo, including the one a compromised account just pushed. Use StringLike only for a deliberate, narrow pattern, and never for production roles.
Rollback: aws iam detach-role-policy, then aws iam delete-role --role-name gha-payments-prod-deploy.
3. AWS workflow
aws-actions/configure-aws-credentials is on major version v6 and defaults the audience to sts.amazonaws.com. Pin third-party actions to a full commit SHA in production; the tag is shown here for readability.
name: deploy-prod
on:
push:
branches: [main]
permissions:
id-token: write # required to request the OIDC JWT
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-payments-prod-deploy
role-session-name: payments-prod-${{ github.run_id }}
aws-region: us-east-1
- run: aws sts get-caller-identity
Check: the last step prints an assumed-role/gha-payments-prod-deploy/... ARN.
Common failure: Not authorized to perform sts:AssumeRoleWithWebIdentity. The token's sub does not equal the condition, usually because the job lacks environment: prod so sub carries the branch ref instead, or the repository uses the immutable ID format.
Rollback: revert the workflow file; the role is unused without it.
4. Create the Azure federated identity credential
Use an Entra app registration or a user-assigned managed identity, then give it a role assignment on the subscription or resource group. For an app (Microsoft Learn), credential.json:
{
"name": "payments-prod",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:example-org/payments-api:environment:prod",
"audiences": ["api://AzureADTokenExchange"]
}
az ad app federated-credential create --id <app-object-id> --parameters credential.json
For a user-assigned managed identity:
az identity federated-credential create --name payments-prod \
--identity-name id-payments-deploy --resource-group rg-payments \
--issuer https://token.actions.githubusercontent.com \
--subject repo:example-org/payments-api:environment:prod \
--audiences api://AzureADTokenExchange
Check: az ad app federated-credential list --id <app-object-id> (or az identity federated-credential list) shows the exact subject.
Common failure: the exchange fails with no useful error. Microsoft notes a wrong subject is accepted at creation and only fails at token exchange. Wildcards are not supported in any property, and each identity holds at most 20 federated credentials, so plan one credential per environment.
Rollback: az ad app federated-credential delete --id <app-object-id> --federated-credential-id payments-prod.
5. Azure workflow
Store the client, tenant, and subscription IDs as environment secrets (they are identifiers, not credentials). The Azure/login README uses v3; Microsoft Learn still shows v2.
name: deploy-prod-azure
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: az account show
Check: az account show prints the expected subscription, signed in as the app or managed identity.
Common failure: the login step fails at token exchange. It is almost always a sub mismatch, such as a job missing environment: prod.
Rollback: revert the workflow and delete the federated credential.
Verify it works
- Run the workflow from
mainwith theprodenvironment. Both clouds return an identity. - Push the same workflow to a feature branch without the environment. AWS must return
AccessDeniedand Azure must fail the exchange. If either succeeds, yoursubcondition is too broad. - In CloudTrail, find
AssumeRoleWithWebIdentityevents for the role and confirm the subject matches your repository. - Delete the old
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, and any stored Azure client secret (oftenAZURE_CREDENTIALS), then deactivate the IAM access key and the app client secret. Follow rotate a leaked secret if they were ever exposed.
Related
Frequently asked questions
- How do I use GitHub Actions OIDC with AWS without access keys?
- Create an IAM OIDC provider for https://token.actions.githubusercontent.com with audience sts.amazonaws.com, create a role whose trust policy checks the aud and sub claims, grant the workflow id-token: write, and call aws-actions/configure-aws-credentials with role-to-assume. No access key is stored in GitHub.
- What should the sub claim condition be for GitHub Actions?
- Match one repository plus one branch or one environment exactly, for example repo:ORG/REPO:environment:prod. A wildcard such as repo:ORG/* lets every workflow in every repository in the organization assume the role.
- Does Azure support wildcards in GitHub federated credentials?
- No. Microsoft states that wildcard characters are not supported in any federated identity credential property, so each subject must match exactly. An app or user-assigned managed identity can hold up to 20 federated credentials.
- Do I still need an OIDC thumbprint for GitHub in AWS?
- AWS now verifies the JWKS endpoint certificate against its library of trusted root CAs and only falls back to thumbprints when that fails, so the thumbprint list is optional when you create the provider.
- Does id-token: write give the workflow write access to my repository?
- No. GitHub states that the permission only allows the job to request the OIDC token and does not grant write access to other resources.
Related on Start with Identity
- BlogCISA contractor left AWS GovCloud admin keys in a public GitHub repo for six months
844 MB of agency data in a repo named Private CISA, including a file called importantAWStokens and plaintext internal passwords. Nine automated GitGuardian aler
- BlogGheist turns stolen GitHub tokens into secret dumps through Actions logs
Wiz reported on October 25 that the Gheist campaign used stolen GitHub tokens from infostealer logs and past supply-chain leaks to plant more than 13,500 ci-syn
- BlogJadePuffer used a service principal leaked in a GitHub issue to wipe Azure resources in seven minutes
Microsoft says the operator it tracks as Storm-3168 used two compromised service principals and an AI-driven toolchain to map an Azure tenant, pull storage keys
- GuideMulti-Cloud IAM Strategy Guide: Unified Identity Across AWS, Azure, and GCP
Design a multi-cloud IAM strategy with cross-cloud identity federation, unified governance, and practical patterns for managing access across AWS, Azure, and GC
- GuideWorkload identity 101: replacing long-lived secrets
Workload identity replaces long-lived secrets (API keys, client secrets, service account passwords) with short-lived credentials that the platform issues to a w
- RecipeAdd login to a Next.js app with OIDC
A complete, copy-paste OpenID Connect login for the Next.js App Router using authorization code flow with PKCE, server-side token exchange, and httpOnly cookies