Skip to content
Start with Identity
Recipe · Intermediate · 45 min

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.

GitHub ActionsAWS IAMEntra IDOIDC
Before you start
  • 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

  1. Run the workflow from main with the prod environment. Both clouds return an identity.
  2. Push the same workflow to a feature branch without the environment. AWS must return AccessDenied and Azure must fail the exchange. If either succeeds, your sub condition is too broad.
  3. In CloudTrail, find AssumeRoleWithWebIdentity events for the role and confirm the subject matches your repository.
  4. Delete the old AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and any stored Azure client secret (often AZURE_CREDENTIALS), then deactivate the IAM access key and the app client secret. Follow rotate a leaked secret if they were ever exposed.

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.
Last reviewed By SWI Community TeamSuggest a correctionHow we research
Free to read and use. Code is illustrative, vendor-neutral, and built on the open standards. Review it against your provider's docs and your threat model before shipping. See more in the recipes index and the standards library.