OIDC (OpenID Connect) lets your workflows authenticate to the cloud providers (AWS, Azure, GCP) without storing static credentials. No more secrets to manage, no more rotation to schedule.
What you will learn
- Understand OIDC and why it replaces static cloud secrets
- Read the JWT GitHub issues and its claims (
sub,repository,environment) - Declare
id-token: writeand wire in the official cloud action - Restrict the trust policy by repository, branch or environment
- Migrate an existing workflow from secrets to OIDC
Why OIDC?
Authenticating a workflow to a cloud has historically meant storing an access key in the secrets. OIDC removes that storage: GitHub proves the identity of the workflow, and the cloud issues temporary access.
The problems with static secrets
An AWS access key sitting in the GitHub secrets is a long-lived credential that stacks up the risks.
# ❌ Static secrets: multiple risks- name: Configure AWS env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}| Risk | Description |
|---|---|
| Exposure | A compromised workflow exfiltrates the credentials |
| Rotation | The keys must be changed regularly |
| Scope | Hard to limit the permissions per workflow |
| Audit | Who used these credentials? Impossible to trace |
The benefits of OIDC
OIDC replaces that static credential with a short-lived token, negotiated on every workflow run.
# ✅ OIDC: short-lived tokens, no secrets- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions aws-region: eu-west-1| Benefit | Description |
|---|---|
| Short-lived | The token is valid for a few minutes only |
| Secret-free | Nothing to store in GitHub Secrets |
| Traceable | Every token identifies the workflow, the repository, the branch |
| Granular | Different policies per repository, branch or environment |
How it works
The OIDC exchange runs in five steps, with no secret in transit and none stored:
- The workflow asks GitHub for an OIDC token
- GitHub generates a signed JWT (JSON Web Token)
- The workflow presents that JWT to the cloud provider
- The provider verifies the signature with GitHub's public keys
- The provider issues temporary credentials
The GitHub Actions JWT
The OIDC token carries information about the workflow, as many claims as the cloud provider can verify:
{ "iss": "https://token.actions.githubusercontent.com", "sub": "repo:owner/repo:ref:refs/heads/main", "aud": "https://github.com/owner", "ref": "refs/heads/main", "repository": "owner/repo", "repository_owner": "owner", "actor": "username", "workflow": "Deploy", "job_workflow_ref": "owner/repo/.github/workflows/deploy.yml@refs/heads/main", "environment": "production"}Those claims are what make granular policies possible:
- Allow only the
owner/reporepository - Allow only the
mainbranch - Allow only the
productionenvironment
The immutable subject on repositories created after July 2026
The historical sub designates the repository by its name:
repo:owner/repo:.... A name can be renamed and a namespace can be
recycled, which opens a structural hole: if an organisation releases a name,
whoever picks it up gets an OIDC token with an identical subject, and
therefore the access your trust policy granted to the former repository.
GitHub closed that door on 15 July 2026. Repositories created after that date emit, by default, a subject built on immutable numeric identifiers, those of the owner and of the repository:
# Historical format, based on names that can changerepo:my-org/my-project:ref:refs/heads/main
# Immutable format, the default on repositories created after 15 July 2026repo:my-org@1234567/my-project@89012345:ref:refs/heads/mainThree practical consequences follow:
- Earlier repositories keep the old format until they explicitly opt in to the new one, through the repository OIDC settings or the REST API.
- A rename or a transfer after that date switches the repository to the immutable format automatically, which invalidates a trust policy written with the old subject.
- The feature does not exist on GitHub Enterprise Server.
The required permission
To use OIDC, the workflow must request the id-token: write permission;
without it, the token request fails.
permissions: id-token: write # Required for OIDC contents: read # For the checkout
jobs: deploy: runs-on: ubuntu-24.04 steps: - name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Configure the AWS credentials through OIDC uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions aws-region: eu-west-1Configuring OIDC per cloud
The cloud-side configuration, creating the role or the federated identity, differs per provider. The guide OIDC for AWS, Azure and GCP covers each case; here is the overview.
Summary per provider
Each cloud has its official action and its own trust mechanism.
| Provider | Official action | Trust policy |
|---|---|---|
| AWS | aws-actions/configure-aws-credentials | IAM role with OIDC |
| Azure | azure/login | App Registration plus federated credentials |
| GCP | google-github-actions/auth | Workload Identity Federation |
Good practices
OIDC removes the secrets, but the real security rests on the trust policy on the cloud side. Four reflexes lock it down.
1. Restrict by branch and environment
The sub condition of the trust policy limits which repository, branch or
environment may assume the role.
{ "Condition": { "StringEquals": { "token.actions.githubusercontent.com:sub": "repo:owner/repo:environment:production" } }}That subject follows the historical format. On a repository created after
15 July 2026, the same condition is written with the immutable
identifiers (repo:owner@OWNER-ID/repo@REPO-ID:environment:production),
otherwise the condition matches nothing and the AssumeRole fails.
2. Use GitHub environments
Declaring a GitHub environment adds the matching claim to the JWT, and lets
you require a manual approval before deployment.
jobs: deploy: environment: production # The 'environment' claim will be in the JWT runs-on: ubuntu-24.04 permissions: id-token: write steps: - name: Configure the AWS credentials through OIDC uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: ${{ vars.AWS_ROLE_ARN }}3. Minimal permissions on the cloud role
The assumed role applies least privilege: only the cloud actions that are strictly necessary.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject" ], "Resource": "arn:aws:s3:::my-bucket/*" } ]}4. Audit the uses
Every assume-role call is logged on the cloud provider side:
- AWS: CloudTrail
- Azure: Activity Log
- GCP: Cloud Audit Logs
Use cases
Two frequent scenarios show OIDC end to end: publishing a static site to S3 and pushing a Docker image to ECR.
Deploying to S3
Syncing a dist/ directory to an S3 bucket, with no AWS key stored anywhere.
jobs: deploy: runs-on: ubuntu-24.04 permissions: id-token: write contents: read steps: - name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Configure the AWS credentials through OIDC uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: arn:aws:iam::123456789012:role/deploy-s3 aws-region: eu-west-1
- name: Sync the site to S3 run: aws s3 sync ./dist s3://my-website-bucketPushing an image to ECR
Building a Docker image and pushing it to Amazon ECR; the registry authentication goes through OIDC as well.
jobs: push: runs-on: ubuntu-24.04 permissions: id-token: write contents: read steps: - name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Configure the AWS credentials through OIDC uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 with: role-to-assume: arn:aws:iam::123456789012:role/push-ecr aws-region: eu-west-1
- name: Log in to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2.1.6
- name: Build and push the image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} run: | docker build -t "$ECR_REGISTRY/app:$GITHUB_SHA" . docker push "$ECR_REGISTRY/app:$GITHUB_SHA"Migrating away from secrets
Moving an existing workflow to OIDC takes five steps, with no service interruption:
- Create the cloud role with an OIDC trust policy
- Add
permissions: id-token: writeto the workflow - Replace the secrets with the OIDC action
- Test on a dedicated branch
- Delete the old static secrets
Key points
- OIDC replaces static cloud credentials with short-lived tokens negotiated on every run.
- The workflow must declare the
id-token: writepermission to request an OIDC token. - GitHub issues a signed JWT whose claims (
repository,ref,environment) drive the trust policy on the cloud side. - The real security plays out in the trust policy: restrict by repository, branch or environment.
- The assumed cloud role applies least privilege, only the strictly necessary actions.
Next steps
- Provenance attestations: the other use of
id-token: write, signing what the workflow produces. - Verifying attestations: checking, on the consumer side, what the OIDC chain actually signed.
- Security checklist: checking that no static cloud credential is left in your workflows.