AWS-GitHub As An OIDC Security Hardening For Workflows

This blog talks about using OpenID connect provider within GitHub workflows to authenticate with AWS.
Sagar Jadhav_avatar
Sagar Jadhav
Mar 22, 2022 | 8 min read

OpenID Connect (OIDC) is an open and trusted authentication protocol, which allows verifying user identity when a user tries to access a protected HTTPS endpoint. It lets developers authenticate users without the requirement for storing and managing passwords. OIDC presents the concept of an ID token, which is a security token that allows the client to verify the user identity. It allows your GitHub actions workflows to exchange short-lived tokens directly from your cloud providers such as AWS, Azure, etc.

Overview Of OIDC

GitHub actions workflows are often designed to access a cloud provider (like AWS, Azure, GCP, or HashiCorp Vault) to deploy software or use the cloud services. Before accessing these resources, the workflow will supply credentials, such as a password or token, to the cloud provider. GitHub usually stores these credentials as a secret, and the workflow presents this secret to the cloud provider every time it runs.

However, to use hard-coded secrets, we need to create credentials in the cloud provider and then duplicate them as a secret in GitHub. We also need to rotate them at regular intervals manually or by configuring a lambda function.

With OIDC, you can take a different approach by configuring your workflow to request a short-lived access token directly from the cloud provider. Your cloud provider also needs to support OIDC on their end, and you must configure a trust relationship that controls which workflows can request the access tokens. Providers that currently support OIDC on Github Actions include Amazon Web Services, Azure, Google Cloud Platform, HashiCorp Vault, etc.

Benefits Of Using OIDC

You can adopt the following good security practices by updating your workflows to use OIDC tokens:

  • No cloud secrets: There is no need to duplicate your cloud credentials as long-lived GitHub secrets. Preferably, you can configure the OIDC trust on your cloud provider and then update your workflows to request a short-lived access token from the cloud provider through OIDC.
  • Authentication and authorization management: Using your cloud provider's authentication (authN) and authorization (authZ) tools to control access to cloud resources, you can have more granular control over how workflows can use credentials.
  • Rotating credentials: With OIDC, your cloud provider issues a short-lived access token that is only valid for a single workflow run and then expires automatically.

oidc-flow

Adding The Identity Provider To AWS

Refer to the AWS documentation Creating OpenID Connect (OIDC) identity providers to add the GitHub OIDC provider to IAM.

  1. For the provider URL: Use https://token.actions.githubusercontent.com.
  2. For the "Audience": If you are using the official GitHub action configure-aws-credentials., use sts.amazonaws.com.

Configuring The Role And Trust Policy

"Condition": {
  "ForAllValues:StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:ref:refs/heads/octo-branch"
  }
}

Replace the value of token.actions.githubusercontent.com:sub with your repository name. Eg. "token.actions.githubusercontent.com:sub": "repo:medlypharmacy/aws-infra:*" or you can also refer to a particular branch i.e. main/master.

Updating Your GitHub Actions Workflow

You will need to make the following two changes to your YAML to update your workflows for OIDC:

  1. Add permissions settings for the token.
  2. Use the aws-actions/configure-aws-credentials action to exchange the OIDC token (JWT) for a cloud access token.

Adding Permissions Settings For The Token

The workflow will require a permissions setting with id-token: write. The id-token: write setting allows the JWT to be requested from GitHub's OIDC provider using one of these approaches:

  • Using environment variables on the runner (ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN)
  • Using getIDToken() from the Actions toolkit. If you only need to fetch an OIDC token for a single job, then this permission can be set within that job. For example:

YAML

permissions:
  id-token: write

Depending on your workflow's requirements, you may need to specify additional permissions here..

Requesting The Access Token

The aws-actions/configure-aws-credentials action receives a JWT from the GitHub OIDC provider and then requests an access token from AWS. For more information, refer to the GitHub action configure-aws-credentials.

You can configure the below step to the GitHub workflow;

    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        role-to-assume: <iam-role-arn>
        aws-region: <aws-region>

For example, you can use this action with the AWS CLI. You can also run this action multiple times to use different AWS accounts, regions, or IAM roles in the same GitHub Actions workflow job.

jobs:
  deploy:
    name: Upload to S3 bucket
    runs-on: ubuntu-latest
    # These permissions are needed to interact with GitHub's OIDC Token endpoint.
    permissions:
      id-token: write
      contents: read
    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        role-to-assume: arn:aws:iam::111111111111:role/my-github-actions-role-test
        aws-region: us-east-1

    - name: Copy files to s3 bucket
      run: |
        aws s3 cp . s3://my-bucket

Please note you should always try to avoid hardcoding the values/passwords wherever possible in your workflow. Instead make use of GitHub secrets so, in the above example you can pass the IAM role arn like role-to-assume: ${{ secrets.iam_role_arn }}. The value for iam_role_arn will be taken from the GitHub secret of your repository.

AWS secures communication with some OIDC identity providers (IdPs) through their library of trusted certificate authorities (CAs) instead of using a certificate thumbprint to verify the OIDC IdP server certificate. These OIDC IdPs include Google and others that use an Amazon S3 bucket to host a JSON Web Key Set (JWKS) endpoint. In these cases, your legacy thumbprint remains in your configuration, but AWS cannot use it for validation.

Challenges Of Using OIDC

  • The main challenge is to get a valid thumbprint from an OIDC provider during the workflow runtime. If the new certificate is renewed/issued to the GitHub OIDC provider, then the updated thumbprint/fingerprint must be added here. This is to ensure GitHub workflow remains up and running.

Solution To Avoid Using Invalid Or Expired Thumbprint

  1. A simple python script mentioned below would be helpful to get the latest thumbprint from an OIDC provider. We can automate this through lambda functions and schedule it once a week or once a fortnight.
## The script will fetch the SHA1 fingerprint from https://token.actions.githubusercontent.com OIDC url during runtime.
## And compare it against the existing thumbprint in the code.
## TODO: we can set up the notification like slack, sns etc. to get up to date about OIDC cert changes.

import urllib
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
import ssl, socket, hashlib

def lambda_handler(event, context):
    
    oidcUrl = 'https://token.actions.githubusercontent.com/.well-known/openid-configuration'    ## Pass it via env vars..
    addr = 'token.actions.githubusercontent.com'                                                ## Pass it via env vars..
    existing_thumb_sha1 = '784t6rgdbj812hd63478hd946d7kc34s28n5'                                ## dummy thumbprint..
    
    try:
        result = urllib.request.urlopen(oidcUrl)
        #print (result.getcode())
        
        context = ssl.create_default_context()
        wrappedSocket = context.wrap_socket(socket.socket(), server_hostname=addr)
        wrappedSocket.connect((addr, 443))

    except urllib.error.HTTPError as e:
        print ('HTTPError: No web page was found..')
        print('HTTPError:', e.code)
        
    except urllib.error.URLError as e:
        print ('URLError: Failed to reach/connect to the website..')
        print('URLError:', e.reason)
    
    else:
      der_cert = wrappedSocket.getpeercert(True)
      pem_cert = ssl.DER_cert_to_PEM_cert(wrappedSocket.getpeercert(True))
      #print(pem_cert)
     
      #Get SHA1 Thumbprint..
      thumb_sha1 = hashlib.sha1(der_cert).hexdigest()
      print("SHA1 Fingerprint: " + thumb_sha1)

    if (existing_thumb_sha1 == thumb_sha1):
        print ("Fingerprint match found..Ignore.")
    
    else:
        print ("A new fingerprint found as: " + thumb_sha1 + " Kindly update..")

There is definite scope to add notifications like slack and SNS to the above script to get notified about latest thumbprint of OIDC.

  1. To obtain a thumbprint/fingerprint from an OIDC IdP refer AWS documentation Obtaining the thumbprint for an OpenID Connect Identity Provider

  2. There are some GitHub actions available which could be useful to monitor OIDC thumbprint during the workflow runtime.

  1. Setting up the calendar invite/reminder to check if thumbprint are up to date.

  2. Subscribe to GitHub changelog to get notified about latest GitHub updates and other items.

Concluding Thoughts

This blog gives us a better understanding of OIDC and how to leverage it and make it a part of good security practices. It walks you through the workflows for OIDC alongside the benefits and challenges of using OIDC. Using OIDC, we can bypass creating the Access Keys/Secret Keys in the IAM console, thereby avoiding the extra efforts for their rotation. I hope you find this blog informative and helpful. Visit us at medly.tech for more such technical blogs. Thank you for reading, and have a great day ahead!