Connecting GitHub Actions to AWS the Right Way: OIDC Federation
Introduction
If your GitHub Actions workflows deploy to AWS, you need credentials. The old approach — storing long-lived IAM access keys as GitHub secrets — works, but it’s a security liability. Keys don’t expire automatically, they can leak, and rotating them is a manual chore.
The modern approach is OIDC federation: GitHub proves its identity to AWS using short-lived tokens, and AWS grants temporary credentials in return. No secrets stored in GitHub. No keys to rotate. No credentials that can leak in logs.
This article walks through how it works and how to set it up with a CloudFormation template you can deploy today.
How OIDC Federation Works
The flow has three actors: your GitHub Actions workflow, GitHub’s OIDC provider, and AWS IAM.
GitHub Actions requests a token — When your workflow runs, GitHub generates a signed JWT (JSON Web Token) containing claims about the workflow: which repository triggered it, which branch, which environment.
The workflow presents the token to AWS — Using the
aws-actions/configure-aws-credentialsaction, the workflow callssts:AssumeRoleWithWebIdentity, passing the GitHub-signed JWT.AWS validates and grants credentials — AWS checks the token’s signature against GitHub’s OIDC provider (which you registered as a trusted identity provider in IAM). If the token is valid and the claims match your role’s trust policy conditions, AWS issues temporary credentials (typically valid for 1 hour).
The result: your workflow gets credentials that are scoped, short-lived, and automatically rotated on every run. Nothing is stored in GitHub secrets.
Step-by-Step Setup
Step 1: Register GitHub as an OIDC Provider in AWS
You need to tell AWS to trust tokens signed by GitHub. This is a one-time setup per AWS account — all repositories in your GitHub organisation can share the same provider.
Deploy the CloudFormation template below. It creates:
- An OIDC identity provider pointing to
token.actions.githubusercontent.com - An IAM role that GitHub Actions can assume
Step 2: Deploy the CloudFormation Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
AWSTemplateFormatVersion: 2010-09-09
Description: >
GitHub OIDC Provider and IAM Role for GitHub Actions.
See: https://github.com/aws-actions/configure-aws-credentials
Parameters:
GitHubOrg:
Type: String
Description: Your GitHub organisation or username
RepositoryName:
Type: String
Default: "*"
Description: >
Repository name to allow (use * for all repos in the org).
For tighter security, specify a single repo name.
OIDCProviderArn:
Description: >
ARN of an existing GitHub OIDC Provider.
Leave empty to create a new one.
Default: ""
Type: String
Conditions:
CreateOIDCProvider: !Equals
- !Ref OIDCProviderArn
- ""
Resources:
GitHubOIDC:
Type: AWS::IAM::OIDCProvider
Condition: CreateOIDCProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
ThumbprintList:
- 6938fd4d98bab03faadb97b34396831e3780aea1
- 1c58a3a8518e8759bf075b76b750d4f2df264fcd
GitHubActionsRole:
Type: AWS::IAM::Role
Properties:
RoleName: GitHubActions
MaxSessionDuration: 3600
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: sts:AssumeRoleWithWebIdentity
Principal:
Federated: !If
- CreateOIDCProvider
- !Ref GitHubOIDC
- !Ref OIDCProviderArn
Condition:
StringEquals:
token.actions.githubusercontent.com:aud: sts.amazonaws.com
StringLike:
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/${RepositoryName}:*
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
Outputs:
RoleARN:
Description: ARN of the IAM role for GitHub Actions
Value: !GetAtt GitHubActionsRole.Arn
Deploy it:
1
2
3
4
5
aws cloudformation deploy \
--template-file github-oidc.yaml \
--stack-name github-oidc \
--parameter-overrides GitHubOrg=your-org-name \
--capabilities CAPABILITY_NAMED_IAM
Step 3: Configure Your GitHub Actions Workflow
In your workflow, use the aws-actions/configure-aws-credentials action to assume the role:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-arn: arn:aws:iam::123456789012:role/GitHubActions
aws-region: ap-southeast-2
- name: Verify credentials
run: aws sts get-caller-identity
- name: Deploy
run: |
# Your deployment commands here
cdk deploy --require-approval never
The critical line is permissions: id-token: write — without it, the workflow can’t request an OIDC token from GitHub.
Understanding the Trust Policy
The trust policy on the IAM role controls which GitHub workflows can assume it. Let’s break down the conditions:
1
2
3
4
5
Condition:
StringEquals:
token.actions.githubusercontent.com:aud: sts.amazonaws.com
StringLike:
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/${RepositoryName}:*
aud(audience): Must bests.amazonaws.com— ensures the token was intended for AWSsub(subject): Matches the repository and optionally the branch, environment, or event type
Tightening the Subject Claim
The sub claim format is: repo:<org>/<repo>:<context>
You can restrict access beyond just the repository:
| Pattern | Allows |
|---|---|
repo:my-org/*:* | Any repo in the org, any branch |
repo:my-org/my-repo:* | Specific repo, any branch |
repo:my-org/my-repo:ref:refs/heads/main | Only the main branch |
repo:my-org/my-repo:environment:production | Only the production environment |
repo:my-org/my-repo:pull_request | Only pull request events |
For production deployments, always restrict to a specific branch or environment rather than using wildcards.
Security Considerations
Don’t Use AdministratorAccess in Production
The template above attaches AdministratorAccess for simplicity. In practice, create a custom policy with only the permissions your workflow needs:
1
2
3
4
5
ManagedPolicyArns:
# Instead of AdministratorAccess, use specific policies:
- arn:aws:iam::aws:policy/AmazonS3FullAccess
- arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
- arn:aws:iam::aws:policy/AWSLambda_FullAccess
Or better yet, write an inline policy scoped to the exact resources your deployment touches.
One Role Per Deployment Scope
Consider creating separate roles for different trust levels:
GitHubActions-Deploy-Prod— restricted tomainbranch, limited permissionsGitHubActions-Deploy-Dev— broader access, any branchGitHubActions-ReadOnly— for workflows that only need to read from AWS (e.g., integration tests)
Session Duration
The template sets MaxSessionDuration: 3600 (1 hour). This is usually sufficient for CI/CD workflows. If your deployments take longer, increase it — but keep it as short as practical.
Key Takeaways
- Stop using long-lived IAM keys in GitHub secrets. OIDC federation is more secure, requires no rotation, and is straightforward to set up.
- The OIDC provider is per-account, the role is per-use-case. Register the provider once, then create as many roles as you need with different trust policies and permissions.
- Restrict the subject claim. Don’t use
*wildcards in production — scope to specific repos, branches, or environments. - Apply least-privilege to the IAM role.
AdministratorAccessis fine for getting started, but scope it down before going to production. - Remember
permissions: id-token: writein your workflow — it’s the most common reason OIDC auth fails.