Delivery Workflows
Deploy OpenTofu/Terraform to AWS using GitHub Actions and OIDC
How to securely deploy to AWS from GitHub Actions using OIDC
GitHub Actions make CI/CD straightforward, but securely accessing AWS from runners requires care. Historically that often meant storing long-lived AWS access keys as GitHub secrets, which creates obvious security and rotation problems. With OpenID Connect (OIDC), GitHub Actions can instead assume AWS roles using short-lived credentials.
In this post, we will walk through how GitHub OIDC works with AWS, how to set it up, and how to integrate it with OpenTofu or Terraform for infrastructure deployment. Sources: GitHub OIDC in AWS, AWS IAM OIDC providers.
Understanding the OIDC Flow
At a high level, the GitHub to AWS flow looks like this:
- GitHub Actions generates a signed token for the workflow run.
- AWS validates that token against the configured OIDC provider.
- AWS STS checks the IAM role trust policy.
- If the claims match, AWS issues temporary credentials.
- The workflow uses those credentials to access AWS resources.
That removes the need to store static AWS keys in GitHub.
Setting Up the AWS OIDC Identity Provider
First, create an OIDC identity provider in AWS that trusts GitHub’s issuer:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = [
"sts.amazonaws.com",
]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1"
]
tags = {
Name = "GitHub OIDC Provider"
}
}
If you build this manually with the AWS CLI, make sure the provider URL and audience match the GitHub OIDC documentation for AWS.
Creating the IAM Role
The next step is an IAM role that GitHub Actions can assume. The most important part is the trust policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT-ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*"
}
}
}
]
}
A Terraform or OpenTofu role resource would follow the same pattern.
Understanding JWT Claims and Conditions
GitHub’s OIDC token includes several useful claims:
audfor the audiencesubfor the repository and ref contextrepositoryrepository_ownerrefactor
Those claims let you narrow trust policies so only the right repositories, branches, or pull requests can assume the role.
For example:
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:repository_owner": "your-org"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/*:*"
}
}
}
GitHub Actions Workflow Implementation
The workflow needs id-token: write so GitHub can mint an OIDC token for the run:
name: Deploy Infrastructure
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
plan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/OpenTofuExecutionRole
role-session-name: github-actions-opentofu-session
aws-region: eu-west-2
- name: Validate and Plan
uses: coresolutionsltd/tf-github-action@v1.0.0
with:
workdir: ./infra
env: prod
steps: validate plan
You can extend the same pattern for apply, multi-environment workflows, or cross-account deployment.
Multi-Environment and Cross-Account Patterns
For multiple environments, use different roles:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ github.ref == 'refs/heads/main' &&
'arn:aws:iam::123456789012:role/ProdOpenTofuRole' ||
'arn:aws:iam::123456789012:role/DevOpenTofuRole' }}
role-session-name: ${{ github.run_id }}-${{ github.run_attempt }}
aws-region: eu-west-2
For multiple AWS accounts, a matrix strategy works well:
strategy:
matrix:
environment: [dev, staging, prod]
include:
- environment: dev
account_id: '111111111111'
region: 'eu-west-1'
- environment: staging
account_id: '222222222222'
region: 'eu-west-2'
- environment: prod
account_id: '333333333333'
region: 'eu-west-2'
Security Best Practices
1. Principle of Least Privilege
Grant only the permissions the workflow actually needs.
2. Branch and Repository Restrictions
Use specific subject conditions so only the right repositories and branches can assume the role.
3. Session Duration Limits
Keep role sessions as short as practical:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/OpenTofuExecutionRole
role-session-name: session
aws-region: eu-west-2
role-duration-seconds: 1800
Troubleshooting Common Issues
”No OpenIDConnect provider found”
This usually means the IAM OIDC provider was not created, was created in the wrong account, or does not match GitHub’s issuer URL.
”AssumeRoleWithWebIdentity is not authorised”
This almost always points to a trust policy mismatch. Common causes are:
- the wrong repository name in the
subclaim - missing or incorrect
aud - branch restrictions that do not match the current ref
Insufficient Permissions
If the role assumption succeeds but the workflow still fails, inspect CloudTrail or the action logs to identify which AWS actions are missing.
Conclusion
GitHub OIDC with AWS gives you a secure, keyless way to run OpenTofu or Terraform from GitHub Actions. By configuring the OIDC provider, tightening the IAM trust policy, and keeping permissions narrow, you can build a pipeline that is both more secure and easier to operate than long-lived access keys.