Github Actions: How does Open ID connect really work with AWS ?

Published on: Tue Jul 05 2022

Series

Content

Introduction

Why do we even need this ?

Why use Open ID connect (OIDC) with Github Actions and AWS in the first place ?

The real reason comes down to security of the AWS credentials for accessing our infrastructure resources.

Before OIDC, the best approach was to store the credentials in a secrets store (ie Github Vault).

In addition, these credentials tend to be long lived and do not expire (which can be problematic if the token is leaked).

How does Open ID Connect solve this problem ?

OIDC changes the flow by establishing a trust relationship between our cloud provider (AWS) and Github Actions.

Once configured, a JWT token is generated when the a Github Actions job runs.

This token allows us to authenticate and authorize with AWS to obtain a temporary credentials (Access ID and Secret) based on a certain assumed role.

These credentials are short lived that become invalid upon expiry and will also expire after the Github Actions job is finished.

So, even if the token gets leaked, the hacker won’t be able do anything with the credentials.

In addition, with a JWT, which is the mechanism used by OIDC, you can also achieve more granularity in the authentication and authorization process checking the token’s claims.

Here is a presentation slide to get a gist of this flow:

When using Open ID Connect with AWS and Github Actions, you will most likely be using the custom actions provided by AWS - aws-actions/configure-aws-credentials.

It provides options you can give it which will help you setup your CI/CD environment to have the correct AWS Access ID and secrets.

Now you may be wondering what actually happening behind the scenes between Github Actions and AWS ?

This is what we will be covering today.

We will dive deeper into what happens behind the scene to understand this exchange process and also dive into the code paths.

The Credential exchange process

Let’s take a look credential exchange process happening behind the scene at a high level.

The Authentication request

Assuming our trust relationship is setup between AWS and Github Actions, when we run our actions, it will need to make an authentication request to AWS to return the temporary credentials.

Github Actions authentication request to AWS

The request consists of these three core parameters:

  1. Identity Token - This is the JWT token issued within the Github Action runs

  2. AWS Assume Role - This is the custom role that will be assumed to the credentails used by your Github Actions

  3. Duration - This is the expiry duration of the temporary credential

Note: There are other options in this API call (assume role), see assume-role-with-web-identity for other options.

Assuming a role

Within the context of AWS, each role can have specific permissions, and when we assume a role, we are granted permissions within the scope of that role.

If the Github Actions is authenticated and is able assume that role, then they will have permission to perform actions within that scope.

For example, this can mean accessing certain API on a Lambda function or a S3 bucket.

Example of the CI/CD role for AWS Lambda:

Github Actions assume role request to AWS

The “Assume Role” process is as follows:

  1. Github Actions reaches out to AWS Security Token Service (STS) to assume a given role

  2. The token is authenticated through AWS STS to ensure it is the correct JWT token (ie checking signature & claims)

  3. A temporary credential is generated within the scope of that assumed role

Important: The temporary credentials only have permissions to perform actions according to the scope of permissions on the role assumed.

Gathering the temporary credentials

Once the request is authenticated and a role is assumed, AWS will return temporary credentials as a response from this request.

Then, the custom action - aws-actions/configure-aws-credentials will set these credentials in our Github Actions environment.

Dependings on the role, it will allow us to make API calls to certain AWS resource if granted.

Github Actions response with AWS credentials

The Github Actions Configuration

Here is an example of the Github actions configuration using the custom action - aws-actions/configure-aws-credentials.

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Configure
        uses: actions/checkout@v2
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@master
        with:
            role-to-assume: "<role-to-assume>"
            aws-region: "us-east-1"
            role-duration-seconds: <credential-expiry-in-seconds> # Defaults to 60 minutes / 1 hour 

      - run: # Then when configuration is done, run your AWS related calls 

The Code Paths

Here is a deeper dive into the code itself which is really just leveraging the AWS JS SDK and Github Actions core toolkit.

I’ve highlighted three key steps for this custom action - aws-actions/configure-aws-credentials which are gathering inputs, assuming the role, and exporting the credentials.

1. Gathering Inputs

Expand Details
  # NOTE: This is a sample snippet and does not contain the whole implementation

  const accessKeyId = core.getInput('aws-access-key-id', { required: false });
  const secretAccessKey = core.getInput('aws-secret-access-key', { required: false });
  const region = core.getInput('aws-region', { required: true });
  const sessionToken = core.getInput('aws-session-token', { required: false });
  const maskAccountId = core.getInput('mask-aws-account-id', { required: false });
  const roleToAssume = core.getInput('role-to-assume', {required: false});
  const roleExternalId = core.getInput('role-external-id', { required: false });
  let roleDurationSeconds = core.getInput('role-duration-seconds', {required: false}) || MAX_ACTION_RUNTIME;
  const roleSessionName = core.getInput('role-session-name', { required: false }) || ROLE_SESSION_NAME;
  const roleSkipSessionTaggingInput = core.getInput('role-skip-session-tagging', { required: false })|| 'false';
  const roleSkipSessionTagging = roleSkipSessionTaggingInput.toLowerCase() === 'true';
  const webIdentityTokenFile = core.getInput('web-identity-token-file', { required: false });

Details

2. Assuming the Role

Expand Details
  # NOTE: This is a short snippet and does not contain the whole implementation

  const roleCredentials = await retryAndBackoff(
  async () => { return await assumeRole({
    sourceAccountId,
    region,
    roleToAssume,
    roleExternalId,
    roleDurationSeconds,
    roleSessionName,
    roleSkipSessionTagging,
    webIdentityTokenFile,
    webIdentityToken
  }) }, true);

Details

3. Exporting the credentials

Expand Details
  # NOTE: This is a short snippet and does not contain the whole implementation

  function exportCredentials(params){
    // Configure the AWS CLI and AWS SDKs using environment variables and set them as secrets.
    // Setting the credentials as secrets masks them in Github Actions logs
    const {accessKeyId, secretAccessKey, sessionToken} = params;

    // AWS_ACCESS_KEY_ID:
    // Specifies an AWS access key associated with an IAM user or role
    core.setSecret(accessKeyId);
    core.exportVariable('AWS_ACCESS_KEY_ID', accessKeyId);

    // AWS_SECRET_ACCESS_KEY:
    // Specifies the secret key associated with the access key. This is essentially the "password" for the access key.
    core.setSecret(secretAccessKey);
    core.exportVariable('AWS_SECRET_ACCESS_KEY', secretAccessKey);

    // AWS_SESSION_TOKEN:
    // Specifies the session token value that is required if you are using temporary security credentials.
    if (sessionToken) {
      core.setSecret(sessionToken);
      core.exportVariable('AWS_SESSION_TOKEN', sessionToken);
    } else if (process.env.AWS_SESSION_TOKEN) {
      // clear session token from previous credentials action
      core.exportVariable('AWS_SESSION_TOKEN', '');
    }
  }

Details

Conclusion

That’s it. I hope that helps to clear up the credential exchange process happening behind the scene when you are using Open ID Connect with AWS and Github Actions.

Just to recap:

  • Open ID Connect makes managing our credentials even more secure by using temporary credentials

  • The default AWS credentials expiry for the actions is 60 minutes or 1 hour ⭐️

  • The AWS credentials will expire once the Github Actions job has finished

  • The temporary credentials only has permissions of your assigned role

  • Under the hood the custom actions uses AWS STS to handle the credentials exchange

  • The process of how this all works is:

    1. Setup the trust relationship (AWS with Github Actions)
    2. Github Actions runs assume role API (with a specific role and duration)
    3. AWS returns the temporary credentials
    4. Github actions exports these credentials into our environment (using AWS’s custom configure-aws-credentials action)

Want to keep learning ?

Github


Enjoy the content ?

Then consider signing up to get notified when new content arrives!

Jerry Chang 2022. All rights reserved.