On this page

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:

OIDC token exchange with AWS STS before the S3 upload GitHub Actions AWS STS OIDC token IAM Role AssumeRoleWithWebIdentity Temporary AWS credentials S3
The token is posted to AWS STS, which returns temporary credentials scoped to one role and one session.

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:

  1. Deciding who can assume the role.
  2. 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.