GitHub Actions OIDC with AWS
A practical guide to GitHub Actions OIDC providers, IAM role trust policies, deployment roles, and repository access patterns in AWS.
GitHub Actions can deploy to AWS without storing long-lived AWS access keys. It does this by using OpenID Connect (OIDC) to request short-lived AWS credentials.
The two AWS resources that make this work are:
- An IAM OIDC provider, which allows the AWS account to recognize identity tokens issued by GitHub.
- One or more IAM roles, which define which GitHub repositories or environments may access AWS and what they are allowed to do.
The most important rule is:
Create the GitHub OIDC provider once per AWS account, then create dedicated IAM roles for repositories, environments, and permission boundaries.
1. High-level architecture
GitHub Actions workflow
|
| Requests a short-lived OIDC token
v
GitHub OIDC issuer
token.actions.githubusercontent.com
|
| Sends token to AWS STS
v
AWS account-level OIDC provider
|
| AWS validates issuer and audience
v
IAM role trust policy
|
| Validates repository, branch, tag,
| environment, or reusable workflow
v
AWS STS issues temporary credentials
|
| Credentials are limited by the
| IAM role permission policies
v
GitHub Actions deploys to AWS
No permanent AWS access key or secret access key needs to be stored in GitHub.
2. What is an IAM OIDC provider?
An IAM OIDC provider represents an external identity issuer that AWS trusts.
For GitHub Actions, the provider normally looks like this:
arn:aws:iam::<AWS_ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com
Example:
arn:aws:iam::610452361839:oidc-provider/token.actions.githubusercontent.com
Its important configuration is:
Provider URL: token.actions.githubusercontent.com
Audience: sts.amazonaws.com
The provider tells AWS:
Tokens issued by GitHub Actions for AWS STS are from a recognized identity source.
The provider does not contain:
- A list of allowed repositories
- Allowed branches
- GitHub environment names
- AWS deployment permissions
- ECS, ECR, S3, or Terraform permissions
Those restrictions are configured on IAM roles.
One provider, many roles
A single provider can be shared by multiple roles:
GitHub OIDC provider
│
├── ops-pilot-api-github-plan-dev
├── ops-pilot-api-github-deploy-dev
├── ops-pilot-api-github-plan-prod
├── ops-pilot-api-github-deploy-prod
├── signal-tester-github-plan-dev
└── signal-tester-github-deploy-dev
You normally do not create a new provider when adding a repository.
3. What is an IAM role?
An IAM role is an AWS identity that can be assumed temporarily. Unlike an IAM user, a role does not have permanent access keys of its own.
For GitHub deployments, an IAM role has two distinct security layers:
Trust policy
The trust policy answers:
Who is allowed to assume this role?
For GitHub OIDC, it identifies the GitHub provider and restricts token claims such as repository and environment.
Permission policies
Permission policies answer:
What can the workflow do after assuming this role?
For example:
ecr:GetAuthorizationToken
ecr:PutImage
ecs:RegisterTaskDefinition
ecs:UpdateService
ecs:DescribeServices
iam:PassRole
A workflow must pass both checks:
- Its GitHub identity must satisfy the role trust policy.
- Its AWS action must be allowed by the role permission policies.
4. Provider versus role
| Resource | Purpose | Typical scope |
|---|---|---|
| IAM OIDC provider | Makes AWS recognize GitHub as an identity issuer | Once per AWS account |
| IAM role trust policy | Defines which GitHub identity may assume the role | Repository, environment, branch, tag, or workflow |
| IAM role permission policy | Defines which AWS actions are allowed | ECS, ECR, Terraform, S3 state, IAM pass role, and similar |
| GitHub environment | Adds approvals, variables, secrets, and deployment restrictions | dev, staging, prod |
A useful mental model is:
OIDC provider = AWS recognizes GitHub-issued ID cards
Role trust policy = The entry list for a particular room
Role permission policy = What a person may do inside that room
5. What happens during a GitHub deployment?
A typical deployment follows this sequence:
- A GitHub Actions job begins.
- The workflow requests an OIDC token from GitHub.
- GitHub issues a short-lived JSON Web Token.
- The token includes claims describing the workflow identity.
aws-actions/configure-aws-credentialssends the token to AWS STS.- AWS recognizes GitHub through the account-level OIDC provider.
- AWS evaluates the target IAM role’s trust policy.
- AWS confirms that the token’s audience and subject are allowed.
- AWS STS returns temporary credentials.
- The workflow uses those credentials until they expire.
- IAM permission policies determine which AWS operations succeed.
The AWS STS operation involved is:
sts:AssumeRoleWithWebIdentity
6. GitHub OIDC token subjects
The role trust policy commonly evaluates the token’s sub claim.
GitHub environment
When a workflow job uses a GitHub environment:
jobs:
deploy:
environment: dev
The subject normally resembles:
repo:thecultofdev/signal-tester:environment:dev
Branch
For a workflow tied directly to a branch, the subject can resemble:
repo:thecultofdev/signal-tester:ref:refs/heads/master
Tag
For a tag-triggered workflow:
repo:thecultofdev/signal-tester:ref:refs/tags/v1.0.0
Using a protected GitHub environment is usually preferable for Terraform apply, infrastructure destruction, and deployments because GitHub can require approvals and restrict which branches may use the environment.
7. Example trust policy for signal-tester
The following policy permits only the thecultofdev/signal-tester repository using the GitHub environment named dev:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::610452361839: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:thecultofdev/signal-tester:environment:dev"
}
}
}
]
}
This policy answers only who may assume the role. It does not grant ECS or ECR access by itself.
8. Why the existing OpsPilot roles cannot be reused automatically
An OpsPilot role may contain a condition such as:
{
"StringLike": {
"token.actions.githubusercontent.com:sub": ["repo:thecultofdev/ops-pilot-api:environment:dev"]
}
}
A token from signal-tester will have a different subject:
repo:thecultofdev/signal-tester:environment:dev
Because the values do not match, AWS denies the role assumption.
You could add both repositories to the same role, but both repositories would then receive the role’s AWS permissions. That creates unnecessary coupling.
A safer design is:
ops-pilot-api
→ ops-pilot-api role
→ OpsPilot resources
signal-tester
→ signal-tester role
→ Signal Tester resources
9. Recommended role structure
For a small sandbox application, use at least two roles:
signal-tester-github-plan-dev
signal-tester-github-deploy-dev
A stronger separation is:
signal-tester-github-plan-dev
signal-tester-github-terraform-dev
signal-tester-github-deploy-dev
Plan role
Used for Terraform validation and planning.
Typical access:
- Read Terraform state
- Read AWS resource metadata
- Run
Describe,List, andGetoperations - Create and remove the S3 state lock file when necessary
- No application deployment
- No broad infrastructure mutation
Terraform role
Used for terraform apply and terraform destroy.
Typical access:
- Create and modify signal-tester infrastructure
- Manage ECS, ECR, ALB, CloudWatch, EventBridge, IAM roles, and security groups
- Read and update Terraform state
- Be restricted by resource names and tags where practical
Deploy role
Used for application image deployment.
Typical access:
- Authenticate to ECR
- Push image layers and image manifests
- Read ECS task definitions
- Register task definition revisions
- Update the ECS service
- Describe deployment status
- Pass only the signal-tester task and execution roles
Terraform generally needs broader permissions than an application deployment. Separating them limits the effect of a compromised workflow.
10. GitHub Actions workflow configuration
A job using OIDC must request permission to issue an identity token:
permissions:
contents: read
id-token: write
Example deployment job:
name: Deploy
on:
workflow_dispatch:
jobs:
deploy:
name: Deploy Signal Tester
runs-on: ubuntu-latest
environment: dev
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ vars.AWS_GITHUB_DEPLOY_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Confirm AWS identity
run: aws sts get-caller-identity
The job uses:
environment: dev
because the IAM trust policy expects:
repo:thecultofdev/signal-tester:environment:dev
Without the matching GitHub environment, role assumption will fail.
11. GitHub variables versus secrets
IAM role ARNs are identifiers, not credentials.
Store these as GitHub repository or environment variables:
AWS_REGION
AWS_ACCOUNT_ID
AWS_GITHUB_PLAN_ROLE_ARN
AWS_GITHUB_TERRAFORM_ROLE_ARN
AWS_GITHUB_DEPLOY_ROLE_ARN
ECS_CLUSTER_NAME
ECS_SERVICE_NAME
ECR_REPOSITORY_NAME
TF_STATE_BUCKET
TF_STATE_KEY
Example:
AWS_GITHUB_DEPLOY_ROLE_ARN=
arn:aws:iam::610452361839:role/signal-tester-github-deploy-dev
Do not configure long-lived credentials when using OIDC:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
Application secrets, such as an internal API key, are separate from GitHub OIDC and should normally live in AWS Secrets Manager.
12. GitHub OIDC versus AWS Secrets Manager
These resources solve different problems.
GitHub OIDC role
Used by GitHub Actions to authenticate to AWS.
arn:aws:iam::610452361839:role/signal-tester-github-deploy-dev
Application secret
Used by the running ECS task.
arn:aws:secretsmanager:ap-southeast-1:610452361839:
secret:signal-tester/dev/internal-api-key-xxxxxx
The GitHub deployment workflow usually does not need the secret value.
Terraform only needs the secret ARN so it can configure the ECS task definition:
secrets = [
{
name = "INTERNAL_API_KEY"
valueFrom = var.internal_api_key_secret_arn
}
]
At task startup, ECS retrieves the value through the ECS task execution role.
GitHub Actions OIDC
→ authenticates the deployment workflow
Secrets Manager
→ provides application secrets to the ECS task
They are not interchangeable.
13. Terraform example for a GitHub OIDC role
data "aws_iam_openid_connect_provider" "github" {
arn = "arn:aws:iam::610452361839:oidc-provider/token.actions.githubusercontent.com"
}
data "aws_iam_policy_document" "github_deploy_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [
data.aws_iam_openid_connect_provider.github.arn
]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = [
"repo:thecultofdev/signal-tester:environment:dev"
]
}
}
}
resource "aws_iam_role" "github_deploy" {
name = "signal-tester-github-deploy-dev"
assume_role_policy = data.aws_iam_policy_document.github_deploy_trust.json
tags = {
Application = "signal-tester"
Environment = "dev"
Purpose = "github-actions-deployment"
ManagedBy = "terraform"
}
}
The OIDC provider is referenced as an existing account-level resource. Terraform creates only the new repository-specific role.
14. Example deploy permission policy
The following is illustrative and must be adjusted to the exact ECS, ECR, and IAM resource ARNs:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EcrAuthorization",
"Effect": "Allow",
"Action": ["ecr:GetAuthorizationToken"],
"Resource": "*"
},
{
"Sid": "PushSignalTesterImage",
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart"
],
"Resource": "arn:aws:ecr:ap-southeast-1:610452361839:repository/signal-tester"
},
{
"Sid": "DeploySignalTester",
"Effect": "Allow",
"Action": [
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService"
],
"Resource": "*"
},
{
"Sid": "PassSignalTesterRoles",
"Effect": "Allow",
"Action": ["iam:PassRole"],
"Resource": [
"arn:aws:iam::610452361839:role/signal-tester-ecs-execution-dev",
"arn:aws:iam::610452361839:role/signal-tester-ecs-task-dev"
]
}
]
}
Some ECS actions do not support complete resource-level restriction in every context. Use condition keys, naming conventions, and narrowly scoped iam:PassRole wherever possible.
15. Verify the OIDC provider
List account OIDC providers:
aws iam list-open-id-connect-providers
Inspect the GitHub provider:
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn \
arn:aws:iam::610452361839:oidc-provider/token.actions.githubusercontent.com
Expected values include:
{
"Url": "token.actions.githubusercontent.com",
"ClientIDList": ["sts.amazonaws.com"]
}
You do not need to add a repository to this provider.
16. Find roles that use GitHub OIDC
for role in $(aws iam list-roles \
--query "Roles[].RoleName" \
--output text); do
policy=$(aws iam get-role \
--role-name "$role" \
--query "Role.AssumeRolePolicyDocument" \
--output json)
if echo "$policy" | grep -q "token.actions.githubusercontent.com"; then
echo
echo "===== $role ====="
echo "$policy" | jq .
fi
done
Inspect a specific role:
aws iam get-role \
--role-name signal-tester-github-deploy-dev \
--query "Role.AssumeRolePolicyDocument" \
--output json | jq .
17. Inspect role permissions
List managed policies:
aws iam list-attached-role-policies \
--role-name signal-tester-github-deploy-dev
List inline policies:
aws iam list-role-policies \
--role-name signal-tester-github-deploy-dev
Read an inline policy:
aws iam get-role-policy \
--role-name signal-tester-github-deploy-dev \
--policy-name <POLICY_NAME>
The trust policy and permission policies must be reviewed separately.
18. Test the role from GitHub Actions
Add a temporary verification step after configuring credentials:
- name: Confirm assumed AWS identity
run: aws sts get-caller-identity
Expected output should contain an assumed-role ARN resembling:
arn:aws:sts::610452361839:
assumed-role/signal-tester-github-deploy-dev/<session-name>
It should not show an IAM user ARN.
Remove or retain this step according to your logging preferences. It does not reveal secret credentials.
19. Common errors
Not authorized to perform sts:AssumeRoleWithWebIdentity
Likely causes:
- Repository name in
subdoes not match - GitHub environment name does not match
- The workflow job does not declare the expected environment
- Audience is not
sts.amazonaws.com - The workflow lacks
id-token: write - The wrong role ARN is configured
- The role references the wrong OIDC provider
Workflow uses environment: dev, but trust policy allows a branch
These subjects are different:
repo:thecultofdev/signal-tester:environment:dev
repo:thecultofdev/signal-tester:ref:refs/heads/master
The role must allow the subject GitHub actually issues.
OIDC works, but deployment receives AccessDenied
OIDC authentication succeeded, but the role’s permission policies do not allow the requested AWS action.
Check the failing API operation and update only the necessary permissions.
iam:PassRole failure
The deployment role must be permitted to pass the ECS task role and execution role:
iam:PassRole
Restrict this permission to the exact roles used by the signal-tester task definition.
Existing role works for one repo but not another
The trust policy is probably restricted to the original repository. Create a dedicated role or deliberately add the new repository subject after reviewing the shared permissions.
20. Security recommendations
- Create the GitHub OIDC provider once per AWS account.
- Create dedicated roles per application and environment.
- Use protected GitHub environments for deployment and Terraform apply.
- Separate Terraform and application deployment permissions.
- Restrict trust policies to exact repositories and environments.
- Avoid organization-wide wildcard subjects.
- Restrict
iam:PassRoleto exact ECS roles. - Use short-lived OIDC credentials instead of IAM user access keys.
- Keep application secrets in Secrets Manager or SSM Parameter Store.
- Review CloudTrail for
AssumeRoleWithWebIdentityactivity. - Use IAM Access Analyzer to validate trust and permission policies.
- Pin third-party GitHub Actions to trusted versions or commit SHAs for stricter supply-chain controls.
21. Recommended setup for a new repository
For each new repository:
1. Keep the existing account-level GitHub OIDC provider.
2. Decide which GitHub environments will be used.
3. Create repository- and environment-specific IAM roles.
4. Configure each role's trust policy.
5. Attach least-privilege permission policies.
6. Add role ARNs as GitHub variables.
7. Add `id-token: write` to the workflow.
8. Configure `aws-actions/configure-aws-credentials`.
9. Run `aws sts get-caller-identity` to verify.
10. Test plan, apply, and deploy permissions separately.
For signal-tester:
Existing provider:
arn:aws:iam::610452361839:
oidc-provider/token.actions.githubusercontent.com
Recommended roles:
signal-tester-github-plan-dev
signal-tester-github-terraform-dev
signal-tester-github-deploy-dev
Allowed GitHub identity:
repo:thecultofdev/signal-tester:environment:dev
22. Final mental model
AWS account
│
├── OIDC provider: GitHub
│ ├── Recognizes GitHub-issued tokens
│ ├── Audience: sts.amazonaws.com
│ └── Shared by multiple IAM roles
│
├── IAM role: ops-pilot-api deploy dev
│ ├── Trusts the GitHub provider
│ ├── Allows ops-pilot-api/dev
│ └── Can deploy OpsPilot resources
│
└── IAM role: signal-tester deploy dev
├── Trusts the same GitHub provider
├── Allows signal-tester/dev
└── Can deploy Signal Tester resources
The provider establishes trust with GitHub as a platform.
The IAM role establishes trust with a specific GitHub workload and defines its AWS permissions.