Skip to content
Français
CI/CD & Automatisation medium

GitHub Actions OIDC: authentication without secrets

30 min de lecture

Read this page in French

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: write and 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 }}
RiskDescription
ExposureA compromised workflow exfiltrates the credentials
RotationThe keys must be changed regularly
ScopeHard to limit the permissions per workflow
AuditWho 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
BenefitDescription
Short-livedThe token is valid for a few minutes only
Secret-freeNothing to store in GitHub Secrets
TraceableEvery token identifies the workflow, the repository, the branch
GranularDifferent policies per repository, branch or environment

How it works

The OIDC exchange runs in five steps, with no secret in transit and none stored:

  1. The workflow asks GitHub for an OIDC token
  2. GitHub generates a signed JWT (JSON Web Token)
  3. The workflow presents that JWT to the cloud provider
  4. The provider verifies the signature with GitHub's public keys
  5. 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/repo repository
  • Allow only the main branch
  • Allow only the production environment

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 change
repo:my-org/my-project:ref:refs/heads/main
# Immutable format, the default on repositories created after 15 July 2026
repo:my-org@1234567/my-project@89012345:ref:refs/heads/main

Three 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-1

Configuring 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.

ProviderOfficial actionTrust policy
AWSaws-actions/configure-aws-credentialsIAM role with OIDC
Azureazure/loginApp Registration plus federated credentials
GCPgoogle-github-actions/authWorkload 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-bucket

Pushing 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:

  1. Create the cloud role with an OIDC trust policy
  2. Add permissions: id-token: write to the workflow
  3. Replace the secrets with the OIDC action
  4. Test on a dedicated branch
  5. 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: write permission 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

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn