Authenticating GitHub Actions to AWS with OIDC
Creating the GitHub OIDC identity provider in AWS and an IAM role restricted to this repository and branch, so the workflow assumes a role instead of storing access keys.
Creating the GitHub OIDC provider
The next problem is authentication.
A common approach is to create an AWS access key and put it into GitHub Secrets.
That would look like:
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
But those are long-lived credentials.
Instead, I used GitHub OpenID Connect.
The authentication flow becomes:
GitHub documents OIDC authentication with AWS specifically as a way to avoid storing long-lived AWS credentials in GitHub. GitHub OIDC with AWS
Add the GitHub OIDC identity provider in AWS
In AWS IAM:
IAM
└── Identity providers
└── Add provider
Select:
OpenID Connect
Use:
Provider URL
https://token.actions.githubusercontent.com
Audience
sts.amazonaws.com
The resulting provider is:
token.actions.githubusercontent.com
This establishes trust between AWS and GitHub’s OIDC identity system.
Create the IAM role
Next, create an IAM role for GitHub Actions.
For example:
GitHubActionsDocumentationBlog
The role is responsible for two things:
- Deciding who can assume the role.
- Deciding what the role can do after it is assumed.
These are controlled separately.
The first is the trust policy.
The second is the permissions policy.
Restrict the role to the GitHub repository
The role should not be available to every GitHub repository.
The trust policy should identify the repository and, if appropriate, branch that is allowed to assume the role.
For a repository named:
YOUR_GITHUB_OWNER/YOUR_GITHUB_REPOSITORY
the standard subject pattern is:
repo:YOUR_GITHUB_OWNER/YOUR_GITHUB_REPOSITORY:ref:refs/heads/main
A trust policy can look like:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID: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:YOUR_GITHUB_OWNER/YOUR_GITHUB_REPOSITORY:ref:refs/heads/main"
}
}
}
]
}
Replace:
YOUR_AWS_ACCOUNT_ID
YOUR_GITHUB_OWNER
YOUR_GITHUB_REPOSITORY
with your own values.
This is an important security control.
I don’t want:
any GitHub repository
to be able to assume my deployment role.
I want:
specific GitHub repository
+
specific branch
↓
AWS deployment role
GitHub recommends restricting the OIDC trust relationship to the intended repository/workflow identity rather than using an unrestricted wildcard. GitHub OIDC with AWS
Important: verify the current sub claim
GitHub has evolved its OIDC subject-claim behavior. Depending on repository configuration and when the repository was created, the subject claim can differ from older examples.
Therefore, don’t blindly copy an old sub value if AWS rejects the role assumption.
The trust policy must match the actual OIDC claims emitted by the GitHub workflow.
Give the role S3 permissions
The deployment role only needs enough permission to synchronize the generated website.
A least-privilege policy can look like:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::YOUR_S3_BUCKET"
},
{
"Sid": "ManageWebsiteObjects",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::YOUR_S3_BUCKET/*"
}
]
}
Replace:
YOUR_S3_BUCKET
with your bucket name.
This allows the deployment process to:
List objects
Upload objects
Read objects
Delete objects
It does not grant permission to create or delete arbitrary S3 buckets.
Why DeleteObject is required
The deployment command will use:
aws s3 sync dist/ s3://YOUR_S3_BUCKET --delete
Suppose the website previously contained:
projects/
old-project/
about/
and I remove:
old-project/
from the source project.
Without --delete, the old object might remain in S3.
With:
--delete
the destination is synchronized with the current build.
Therefore:
dist/
becomes the source of truth for the deployed website.