GitHub: workflow authentication to AWS using OIDC

GitHub Actions provide the ability to define a build workflow, which per your requirements may include calls to AWS infrastructure.

However, for those AWS calls to work you need to establish an IAM identity.   In the past, you would have to rely on AWS access keys set in project variables/secrets, or applying an IAM role to the compute of a self-hosted GitHub Runner.

A better way is to use the GitHub OIDC integration, which generates a signed JWT token that can be validated by the AWS STS and exchanged for short-lived credentials when the workflow runs.

Overview

As a prerequisite, a trusted IAM Identity Provider is created on the AWS side for GitHub and retrieves the root CA certificate thumbprint that is used for verification of signed JWT coming from GitHub (OIDC provider specifically for GitHub Actions is located at https://token.actions.githubusercontent.com).

An IAM role that governs the permissions on a set of AWS resources is created and lists this Identity Provider as its principal.  This role defines conditionals on the incoming JWT claim such as audience, repository, and branch.

Then when a GitHub workflow action is run, a JWT token signed by GitHub is injected into the job context .  This is sent to the AWS STS (Security Token Service) for authentication. In OIDC terms, GitHub is the Identity Provider and AWS is the Relying Party.

If the JWT claims match the IAM role conditions AND is signed by GitHub, short-lived AWS credentials are provided back to the GitHub workflow action.

This set of short-lived credentials can then be used to invoke AWS service calls, limited by the policy permissions of the assumed IAM role.

AWS infrastructure

As a prerequisite, we need to create the AWS resources that support this OIDC federated trust:

  • AWS Identity Provider
  • IAM Role
  • IAM Policy

This could be done manually or via AWS CLI calls, but we will use Terraform in this article.  Here is the full terraform definitions for the snippets below.

Create Identity Provider

The aws_iam_openid_connect_provider is used to create the AWS Identity Provider.

data "tls_certificate" "github" {
  url = "https://token.actions.githubusercontent.com/oauth/discovery/keys"
}

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["https://sts.amazonaws.com"]
  thumbprint_list = [data.tls_certificate.github.certificates.0.sha1_fingerprint]
}

The ‘client_id_list’ is the ‘Audience’ identifier. This is a unique identifier (not an actual URL target), and should be the URL of the entity evaluating the JWT, “https://sts.amazonaws.com”.

Create IAM Role

Create an IAM role that uses the newly created Identity Provider as a principal, and allows AssumeRoleWithWebIdentity when the audience and GitHub repository project path match.

resource "aws_iam_role" "github_ci_readwrite" {
  name = "gh-oidc-role-s3-readwrite"

  # IAM 'trust relationship' tab - who can assume this role
  assume_role_policy = jsonencode({
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${aws_iam_openid_connect_provider.github.arn}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "https://sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:fabianlee/github-pipeline-aws-oidc-auth-readwrite:*"
        }
      }
    }
  ]
  })

} # aws_iam_role

The ‘StringLike’ and wildcard “*” usage allows for any branch in this repository to assume the IAM role. The value “fabianlee/github-pipeline-aws-oidc-auth-readwrite” is clearly the path to my example repository and needs to be changed to match your environment.

Associate IAM policy with role

Assign the IAM role policy that defines which AWS resources and permissions are being granted to the IAM role.  In this case, we grant full privileges to S3 storage.

resource "aws_iam_role_policy" "github_role_policy" {
  name = "github_oidc_policy"
  role = aws_iam_role.github_ci_readwrite.id

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": "*"
        }
    ]
  })

} # aws_iam_role_policy

Running example terraform

Use the Terraform scripts from my GitHub example project.

git clone https://github.com/fabianlee/github-pipeline-aws-oidc-auth-readwrite.git
cd github-pipeline-aws-oidc-auth-readwrite/aws-infra

# manually change terraform.tfvars to match your GitHub repository

# establish aws credentials
aws login

# create AWS resources described above (Identity Provider, IAM)
terraform init
terraform plan
# note the ouput IAM role ARN, which is needed by workflow
terraform apply

GitHub Workflow

On the GitHub side, now we need to craft a workflow definition that get the GitHub JWT injected into an action, does the OIDC exchange with AWS, and uses the returned short-lived credentials to make general AWS API calls.

Inject JWT into Job context

The first thing we need is for a GitHub JWT to be inserted into the job context, this is done by setting the ‘id-token’ permissions to ‘write’.

permissions:
  id-token: write
  contents: read

OIDC exchange with AWS

Using the JWT Token and IAM role ARN, we want to exchange this for short-lived credentials.  This can be done using the ‘aws-actions/configure-aws-credentials‘ action as shown below.

 - name: Configure AWS credentials via OIDC
    uses: aws-actions/configure-aws-credentials@v5
    with:
      role-to-assume: "${{ vars.AWS_ROLE_ARN }}"
      role-session-name: GitHub_to_AWS_via_FederatedOIDC-${{ github.run_id }}
      aws-region: "${{ vars.AWS_REGION }}"
      audience: "${{ vars.OIDC_AUDIENCE }}"
      retry-max-attempts: 2

Action that uses AWS authentication

With the configure-aws-credentials job as a previous action, we can now run an action that invokes AWS API calls to S3.  The action below will list all buckets, create a bucket, then upload a file to that new bucket.

    - name: Test access to S3
      run: |
        aws s3 ls
        aws s3 mb s3://$S3_BUCKET_NAME --region ${{ vars.AWS_REGION }} || true # create s3 bucket
        echo "run id: $GITHUB_RUN_ID date: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" > test.txt
        aws s3 cp test.txt s3://$S3_BUCKET_NAME/test.txt
        echo "just UPLOADED test.txt to s3 bucket $S3_BUCKET_NAME"

Here is the full aws.yaml workflow definition.

Create workflow variables

There are several variables used in the workflow definition above that need to be set at the GitHub repository level.  Setting them as variables (instead of hardcoding into the workflow) allows more flexibility.

In GitHub, navigate to Settings > Secrets and Variables > Actions, click on the ‘Variables’ tab, then press “New repository variable”.

Create the following variables:

  • AWS_REGION = us-east-1 or any other region where you want S3 bucket created
  • AWS_ROLE_ARN = arn:aws:iam::xxxxxxx:role/gh-oidc-role-s3-readwrite (role ARN to be assumed)
  • OIDC_AUDIENCE = https://sts.amazonaws.com

Invoke workflow

You can invoke the example GitHub workflow either by pushing to the repository, or navigating to Actions, selecting”test GitHub workflow authentication into AWS using OIDC”, and pressing “Run workflow”.

The invoked jobs will exercise the JWT token injection, OIDC exchange,  then use the short-lived credentials to make calls to S3 as a test.  Clicking into the workflow run will show the details of each action.

Navigating to Amazon S3 >Buckets will show the new bucket and “test.txt” content just uploaded by the GitHub job.

REFERENCES

GitHub docs, Overview of OpenID Connect (OIDC)

GitHub docs, OpenID Connect reference

GitHub docs, Configuring OpenID Connect in Amazon Web Services

Amazon blog, Use IAM roles to connect GitHub Actions o actions in AWS

Firefly, Integration OIDC with Github Action to Manage Terraform Deployment on AWS

Ravinda Singh, Securely Connect GitHub Actions to AWS using IAM roles and OIDC

Igor Zhivilo, OpenID Connect and Github Actions to authentication with AWS

github.com, source for action, configure-aws-credentials

Tobias Schmidt, Connect GitHub Actions to AWS – OIDC Auth Guide, specific bucket names

GitHub docs, configuring customized claims

GitHub REST api, PUT customized claim template

Kunal Parkhade, Replacing AWS Access Keys with OIDC in GitHub Actions – A hands-on guide to zero rotation

NOTES

github discovery endpoint for Actions

https://token.actions.githubusercontent.com/.well-known/jwks