Avoid Static Credentials in Your GitHub Workflows

C Armstrong
KPMG UK Engineering
6 min readDec 19, 2023

If like me you’re pedantic when it comes to security and can’t stand the thought of generating an access key and secret somewhere only to store it for use elsewhere, such as deployment pipelines or scripts, this guide may just help you achieve peace of mind if you’re using GitHub and AWS.

One of the things I love about AWS (and other public cloud providers) is the concept of temporary credentials obtained by assuming roles. I like it because you know you never have to rotate these credentials, there’s an audit record of when things happened and where. The only thing you have to do is control who can assume what role and from where.

This worked fine when you used build servers running within AWS that could assume their EC2 instance or container execution roles, but what about if you wanted to use a service outside of AWS to deploy. Suddenly you’re finding yourself generating a user with static keys and having to transport those safely into configuration and hope they don’t fall into the wrong hands!

Enter GitHub OIDC

Fortunately for us, there’s a solution that works between GitHub and AWS that allows GitHub to use the same assume-role paradigm without having to concern ourselves with self-hosting GitHub or configuring any AWS credentials directly into the GitHub configuration!

The high-level concept is to configure your AWS account to use GitHub as an external identity provider using the OpenID Connect protocol. Essentially this configuration will allow GitHub to generate a JSON Web Token which can be sent to AWS requesting access to an IAM role. AWS can then validate the claims in the Web Token against GitHub and if all is well, provide temporary access credentials to a role we configure. The result is that no permanent credentials are stored in GitHub and we can control the role and what repositories and users can access it all in an IAM document!

Below are the steps needed, but before you begin ensure you have

  • An AWS Account and access to create IAM roles and OIDC configuration
  • A GitHub repository and the ability to run GitHub actions
  • A basic knowledge of GitHub actions and AWS as I’ll not be diving into these in detail

This tutorial will not generate any additional costs in your AWS account, although it’s always a good idea to make sure you have AWS Billing alarms configured and keep an eye on expendature for anything unexpected or accidental!

Step 1: Configure GitHub OIDC Provider

The first thing we need to do is head over to the AWS IAM service and look for “Identity Providers” in the side-bar, shown here selected in blue.

Identity Providers in IAM Sidebar
Identity Providers in IAM Sidebar

Once in there, select the “Add Provider” button and fill in the fields as follows:

GitHub OIDC Settings in AWS Console
GitHub OIDC Settings in AWS Console

Add any tags as necessary for your configuration and hit “Add provider”. Take note of the OIDC provider ARN as we’ll use it later.

Once complete this means AWS is aware of GitHub as a trusted source of identity, however we still need to tell AWS what roles are available and which GitHub repositories are allowed to assume those roles, which we do with IAM assume-role policy documents.

Step 2: Configure IAM Role and Assume-role Policy

For the purposes of this post I’m going to only allow GitHub to execute an sts get-caller-identity command in my AWS account, so first I’ll set up an IAM Policy with the following:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowGetCallerId",
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity"
],
"Resource": ["*"]
}
]
}

I’ll then Create a Role with a Custom Trust Policy:

Custom trust policy
Custom Trust Policy

Note this policy has several differences from one you would normally see:

  • The Action is AssumeRoleWithWebIdentity rather than just AssumeRole
  • The Principal type is Federated rather than AWS or Service
  • The ARN listed is that of the OIDC provider you created in Step 1
  • There is a conditional statement, which is based on elements of the claims in the GitHub token. A full description of the token can be found in the GitHub documentation

Specifically this policy allows requests to the sts.amazonaws.com service from GitHub identities from any branch of the my-repo repository in the MY-ORG organisation in Github. You can apply wildcards or further limit the policy to only allow a certain branch to assume which could be useful for example to ensure only merges to “main” are allowed to assume production roles etc.

Once the trust policy is set, attach the IAM policy to the role and we’re ready to move on to use the role in a GitHub action!

Step 3: Use the role in a GitHub Action:

So we’ve set up the OIDC provider in AWS to let AWS trust GitHub as an identity provider. We’ve also configured an IAM role in AWS with a basic policy and given access to a GitHub Repository. The next and final thing we need to do is actually assume this role from a GitHub action so that we can perform activities in AWS without having to generate any IAM keys and store these in GitHub.

For this, GitHub have an action called configure-aws-credentials that we can use to assume the role.

I’m not going to cover full GitHub action syntax here so if you’re unfamiliar with it I’d recommend checking out some documentation.

Firstly I’ll create a new workflow in my repository with the following contents:

name: Medium Assume-Role Demonstration
on:
workflow_dispatch:
permissions:
id-token: write
jobs:
assume:
runs-on: ubuntu-latest
steps:
- run: aws sts get-caller-identity
shell: bash
continue-on-error: true
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: arn:aws:iam::xxxxxxxxxxxx:role/GitHubActionMediumDemonstration
role-session-name: mediumdemosession
aws-region: eu-west-1
- run: aws sts get-caller-identity
shell: bash

This job has the following things of note:

  • It requests write access to the id-token permission to allow it to retrieve a JWT from the GitHub token provider
  • It calls aws sts get-caller-identity with continue-on-error set to true to show that there are no pre-configured AWS credentials in the environment
  • It calls the configure-aws-credentials action providing the region, a session name and the ARN of the role we want to assume (I’ve removed my account number)
  • It then calls the sts get-caller-identity call again to show that we now have a set of valid user credentials

I’ve set this job up to run on workflow_dispatch, but any valid triggers will work ok.

Step 4: Review run output

In the Actions tab of my repository I can now see the job has run with the following output:

The first step output is an error as we’d expect:

Run aws sts get-caller-identity

<botocore.awsrequest.AWSRequest object at 0x7fb10ad6eed0>
Error: Process completed with exit code 255.

The second I can see the assume-role operation occur which appears to be successful:

Run aws-actions/configure-aws-credentials@v3
Assuming role with OIDC
Authenticated as assumedRoleId AROXXXXXXXXXXXXXXXXXX:mediumdemosession

Finally the second sts call is successful and shows a successful operation has occurred to configure AWS credentials for my account:

Run aws sts get-caller-identity
{
"UserId": "AROXXXXXXXXXXXXXXXXXX:mediumdemosession",
"Account": "xxxxxxxxxxxx",
"Arn": "arn:aws:sts::xxxxxxxxxxxx:assumed-role/GitHubActionMediumDemonstration/mediumdemosession"
}

Conclusion

In this article we’ve seen how we can avoid hard coded credentials in GitHub by:

  • Configuring AWS to trust GitHub as an identity provider (OIDC Provider)
  • Configuring which AWS roles can be assumed and by which GitHub organisations, repositories and even branches
  • Configured a GitHub action to use the configuration to assume a role in our AWS account with temporary session credentials

Next you can proceed to run whatever you need to in your CI/CD workflow to query and deploy to your AWS account using your infrastructure tool of choice!

--

--