How Do I Authenticate to AWS With OIDC?
Grant id-token write in the job, exchange the signed token with AWS STS for temporary credentials, and pin the role trust policy to one repository and branch.
Answer
Request an identity token inside the job, exchange it with AWS STS for temporary credentials, and let an IAM role trust policy decide which repository and which ref are allowed to make that exchange. Three pieces have to line up: the id-token: write permission on the job, an IAM OpenID Connect identity provider registered once per AWS account for token.actions.githubusercontent.com, and a role whose trust policy pins the aud and sub claims from the token GitHub signs (GitHub: security hardening with OpenID Connect, checked on 2026-08-13).
Nothing long lived ends up in the repository. The credentials AWS returns expire on their own, and the branch restriction is enforced by AWS against a claim GitHub signed, so editing the workflow file cannot widen it. The exchange happens between the job, the GitHub token endpoint, and the AWS STS endpoint, so it behaves the same on any runner that can reach both. Every label in that catalog runs the exchange the same way.
The workflow side is one grant and one step:
name: publish-image
on:
push:
branches: [main]
permissions: {}
jobs:
publish:
runs-on: warp-ubuntu-latest-x64-4x
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5
- name: Exchange the GitHub identity token for AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/github-actions-publish
role-session-name: publish-${{ github.run_id }}
aws-region: us-east-1
- uses: aws-actions/amazon-ecr-login@v2
id: ecr
- name: Build and push the image
run: |
docker build -t "${{ steps.ecr.outputs.registry }}/api:${{ github.sha }}" .
docker push "${{ steps.ecr.outputs.registry }}/api:${{ github.sha }}"permissions: {} at the top of the file sets every scope to none for jobs that stay silent, and the publish job restates the two grants it needs. The credentials action requests a token with audience sts.amazonaws.com, calls sts:AssumeRoleWithWebIdentity, and exports the returned access key, secret key, and session token as environment variables for the remaining steps (aws-actions/configure-aws-credentials).
Detail
Register the identity provider once per AWS account
AWS needs to know the issuer before any role can trust it. The provider is an account level object, so one registration serves every role and every repository in that account:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.comThe client ID list is the set of audience values AWS will accept, and sts.amazonaws.com is what the credentials action requests by default. AWS documents when a thumbprint list is required for this call and how to obtain the value (AWS: obtain the thumbprint for an OpenID Connect identity provider). Missing this step is the cause of the No OpenIDConnect provider found in your account error further down.
Scope the trust policy to one repository and one ref
The trust policy is where the security of the pattern lives. It names the provider as a federated principal, allows the one STS action, and compares two claims from the signed token using the condition keys token.actions.githubusercontent.com:aud and token.actions.githubusercontent.com:sub:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GitHubActionsPublishFromMain",
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333: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/example-repo:ref:refs/heads/main"
}
}
}
]
}The sub value follows the trigger that started the job, and the four shapes below cover most pipelines (GitHub: security hardening with OpenID Connect, checked on 2026-08-13):
repo:example-org/example-repo:ref:refs/heads/mainfor a push to a branch.repo:example-org/example-repo:ref:refs/tags/v1.2.0for a tag.repo:example-org/example-repo:environment:productionwhen the job declares a deployment environment.repo:example-org/example-repo:pull_requestfor a pull request run.
Two conditions belong in every policy. Pinning aud stops a token minted for another service from being replayed at STS. Pinning the full sub with StringEquals keeps the role attached to one repository and one ref. A StringLike condition on a prefix such as repo:example-org/* grants the role to every repository in the organization, which turns a per pipeline credential into an organization wide one.
Tag deploys are the case where a wildcard is tempting, because tag names change every release. Prefer a deployment environment there: gate the job with environment: production, match repo:example-org/example-repo:environment:production, and let GitHub's required reviewers decide when the credential is reachable.
What each failure message means
The exchange fails in a small number of ways, and the message names the half that is wrong. Messages come from the credentials action and from AWS STS (aws-actions/configure-aws-credentials, AWS STS API reference).
| Message you see | Half that is wrong | Fix |
|---|---|---|
Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable | Workflow | Add id-token: write to the job, and check that no other permissions block above it dropped the scope |
Credentials could not be loaded, please check your action inputs | Workflow | Same missing grant, or role-to-assume absent from the step inputs |
Not authorized to perform sts:AssumeRoleWithWebIdentity | Trust policy | The sub in the token differs from the condition. A release tag, a fork pull request, or a job in an environment produces a different subject than a push to a branch |
Incorrect token audience | Trust policy | The aud condition and the audience the action requested disagree. Leave the default sts.amazonaws.com on both sides unless a second provider needs its own value |
No OpenIDConnect provider found in your account | AWS account | The identity provider was never created in the account holding the role |
DurationSeconds exceeds the MaxSessionDuration set for this role | Role | Lower role-duration-seconds on the step or raise the maximum session duration on the role |
For the third row, print the subject the job actually presents before editing the policy again. The github context carries the same fields the token does, so echo "${{ github.ref }}" and echo "${{ github.repository }}" in a debug step reconstruct the string the policy has to match (GitHub: contexts).
How this differs from an instance profile on a BYOC runner
An instance profile attaches an IAM role to the EC2 instance itself, so every job that lands on that runner class reads the same credentials from instance metadata. The exchange attaches an identity to the workflow run instead, so authority follows the repository and the ref. Both are available on BYOC, where BYOC runs on AWS, GCP, and Azure and the Instance Profile ARN is a field on the custom runner configuration (instance profile setup).
runs-on label | Where AWS credentials come from | What the grant is scoped to |
|---|---|---|
warp-ubuntu-latest-x64-4x (4 vCPU, 16 GB) | The OIDC exchange inside the job | One repository and one ref per role |
warp-custom-<runner-name> (BYOC on AWS) | An instance profile on the EC2 instance, the OIDC exchange, or both | The instance profile covers every job on that runner class; the exchange stays per repository and ref |
ubuntu-latest (GitHub-hosted) | The OIDC exchange inside the job | One repository and one ref per role |
The two compose cleanly because of ordering in the AWS SDK credential chain. Environment variables are read before instance metadata, so a job that starts with instance profile credentials and then runs the exchange step uses the exchanged role for everything after that step (AWS SDK credential provider chain). A common split is baseline pulls through the instance profile and deployment authority through the exchange, so a compromised test job never reaches the deploy role.
Use the instance profile when the permission is a property of the machine class, such as reading a shared artifact bucket. Use the exchange when the permission is a property of the pipeline, such as pushing to production. The BYOC runner IAM answer covers the iam:PassRole grant that the profile path needs first.
What a security review records
The exchange removes one class of secret from the repository, and reviewers usually ask what happens to the rest. WarpBuild does not access or store build secrets; they stay in your source code repository and reach only the runner environment, which is destroyed with its encrypted volume when the build ends (runner security documentation). The SOC 2 Type 2 attestation covering Security, Availability, and Confidentiality is requested through trust.warpbuild.com. The secrets management guide covers what stays in repository, environment, and organization secrets after the AWS keys are gone.
Pricing does not gate a trial of any of this. Once authentication is settled, the guide to speeding up GitHub Actions covers the runner side of the same pipeline.
Related Questions
Why does the credential step fail with a missing ACTIONS_ID_TOKEN_REQUEST_URL variable?
The job does not hold the id-token: write grant. GitHub injects ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN only into jobs that request it, and any permissions block sets every scope it leaves out to none, so a workflow level block listing contents: read alone strips the grant from every job below it (GitHub: workflow syntax). Add id-token: write to the job that runs the exchange. The OIDC token exchange definition walks through what the token carries once the grant is in place.
Can one IAM role serve every repository in the organization?
It can, and that is the configuration to avoid. A StringLike condition on a subject prefix such as repo:example-org/* hands the role to every current and future repository in the organization, including one a contributor creates tomorrow. Pin the full subject with StringEquals and create one role per repository and deployment target, which keeps the blast radius of a compromised workflow to the resources that one pipeline touches. The secrets management guide covers the same blast radius question for the repository and organization secrets that sit next to this role.
Do I still need OIDC if my BYOC runner already carries an instance profile?
It depends on how the permission varies. An instance profile gives every job on that runner class the same AWS role, which fits baseline access such as pulling base images (instance profile setup). An OIDC exchange gives each workflow run an identity carrying the repository, branch, and environment, so authority can differ per repository and per branch on shared runners. The two compose, and credentials set by the exchange step take precedence over instance metadata for the rest of the job.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.