January 08, 2023     min read

Automate AWS OIDC Role Changes with GitHub Configurable OIDC Claims

Automate AWS OIDC Role Changes with GitHub Configurable OIDC Claims

If you are using GitHub Actions to deploy to AWS, hopefully by now you've discovered OpenID Connect (OIDC) as the best mechanism for authenticating between GitHub and AWS.

You might be like me and have a large number of OIDC roles that you need to manage across several GitHub repositories in your organization you would have likely run into the problem that you can't safely have the IAM role code (policy documents) live in the same repository as the code it is deploying - as automating its deployment would allow anyone with write access to the repository to change the IAM role to anything they want.

In this blog, we will explore how you could utilize GitHub's new configurable OpenID Connect subject claims to automate the process of syncing a large number of OIDC roles that might be used across several GitHub repositories in your organization.

The final solution can be seen in the diagram below:

OIDC Role Sync Architecture
OIDC Role Sync Architecture

BIG CAVEAT - README BEFORE YOU GO INSTALLING THIS YOURSELF: This is not a production-ready solution. The way I demo this possible solution has some very broadly scoped IAM permissions that shouldn't just be installed in your environment without security considerations.

Enhanced OIDC support

Before we get into the solution, I want to better explain why the recent changes to GitHub's OIDC support are so important to this solution. Before the enhanced configurable subject claim format was released, you were limited to only a small piece of information being passed to AWS in the sub claim of the OIDC token.

The default claim keys were repo and context which allowed you to specify filters on the repository name and branch references

repo:<orgName/repoName>:ref:refs/heads/<branchName>

With the new enhanced claim format, you can now specify the sub claim format with a larger number of claim keys GitHub will include in the sub claim of the OIDC token. A full list can be found on the GitHub documentation page.

This format gave me an idea that perhaps with a particular format of these claim keys, I could validate whether a GitHub Action workflow was triggered by a particular user and if so, allow the workflow to assume a more privileged IAM role capable of deploying the changes to its own IAM role.

I came up with the following possible format for the sub claim:

repository_owner:<orgName>:
actor:<user-running-workflow>:
job_workflow_ref:<orgName/repoName>/.github/workflows/oidc-role-deploy.yml@refs/heads/main:
repo:<orgName>/*

When a user triggers a change or triggers a GitHub action workflow, the OIDC token containers a key called actor which contains their GitHub username - this allows us to distinguish a specific user is triggering a change and grant the privileged IAM role to the workflow.

The Solution

Create a repository for shared workflows

I've created an example repository called t04glovern/GitHub-Actions that contains a shared workflow that can be used across any repository in my organization. This is done by changing the Actions > General > Access settings to the following:

Share action workflow in the organization
Share action workflow in the organization

In this repository, create a workflow that will be used to deploy the CloudFormation of your OIDC roles. I've created a workflow called .github/workflows/oidc-role-deploy.yml with the following contents:

name: OIDC Role Deploy

on:
  workflow_call:
    inputs:
      aws-account:
        description: AWS Account to deploy role to
        type: string
        required: true
      aws-region:
        description: AWS Region to deploy role to
        type: string
        required: false
      role-template:
        description: The AWS OIDC Role cloudformation template to deploy
        type: string
        default: .github/aws-oidc-role.yml
        required: false

jobs:
  deploy:
    name: Update AWS OIDC Role for a repository
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    
    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        role-to-assume: arn:aws:iam::${{ inputs.aws-account }}:role/github-oidc-t04glovern
        aws-region: ${{ inputs.aws-region }}

    - name: Run OIDC Role deploy via CloudFormation
      run: |
        aws cloudformation deploy \
            --template-file ${{ inputs.role-template }} \
            --stack-name oidc-t04glovern-${{ github.event.repository.name }} \
            --capabilities CAPABILITY_NAMED_IAM \
            --region ${{ inputs.aws-region }}

Deploy OIDC role for shared workflow repository

Next, we need to deploy an OIDC role that can be used by the shared workflow repository - in my case t04glovern/GitHub-Actions. This role is where we will grant highly privileged IAM permissions PROVIDED the subject claim of the OIDC token contains a specific actor key.

Create a CloudFormation template called .github/aws-oidc-role.yml with the following contents:

AWSTemplateFormatVersion: 2010-09-09
Description: 'GitHub OIDC: t04glovern/GitHub-Actions'

Parameters:
  GitHubOrgName:
    Type: String
    Default: t04glovern
  GitHubRepoName:
    Type: String
    Default: GitHub-Actions

Resources:
  OIDCRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub github-oidc-${GitHubOrgName}
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
        - arn:aws:iam::aws:policy/IAMFullAccess
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRoleWithWebIdentity
            Principal:
              Federated: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
            Condition:
              StringLike:
                token.actions.githubusercontent.com:sub: !Sub repository_owner:${GitHubOrgName}:actor:t04glovern:job_workflow_ref:${GitHubOrgName}/${GitHubRepoName}/.github/workflows/oidc-role-deploy.yml@refs/heads/main:repo:${GitHubOrgName}/*

Outputs:
  OidcRoleAwsAccountId:
    Value: !Ref AWS::AccountId
  OidcRoleAwsRegion:
    Value: !Ref AWS::Region
  OidcRoleAwsRoleToAssume:
    Value: !GetAtt OIDCRole.Arn

NOTE: If you are deploying a version of this, please replace the GitHubOrgName and GitHubRepoName parameters with your values - Additionally, make sure you replace the actor key with your own GitHub username.

Pay careful attention to the actor key in the token.actions.githubusercontent.com:sub conditional statement.

Deploy the OIDC role to the AWS account you would like to deploy OIDC roles to by running the following CloudFormation

aws cloudformation deploy \
    --template-file .github/aws-oidc-role.yml \
    --stack-name oidc-t04glovern-GitHub-Actions \
    --capabilities CAPABILITY_NAMED_IAM \
    --region ap-southeast-2

Role deployment for repository setup

To deploy an OIDC role from a repository and make use of the shared workflow, I created a sample repository called t04glovern/role-sync-sample and created a workflow called .github/workflows/aws-oidc-role-sync.yml with the following contents:

name: Sync OIDC

on:
  push:
    branches:
      - main
    paths:
      - .github/aws-oidc-role.yml

jobs:
  sync-role:
    name: Sync OIDC Deployment role for this repository
    uses: t04glovern/GitHub-Actions/.github/workflows/oidc-role-deploy.yml@main
    with:
      aws-account: 012345678901
      aws-region: ap-southeast-2
      role-template: .github/aws-oidc-role.yml

  sync-role-test:
    name: Test the OIDC role
    runs-on: ubuntu-latest
    needs: sync-role
    permissions:
      id-token: write
      contents: read
    
    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        role-to-assume: arn:aws:iam::012345678901:role/oidc-t04glovern-role-sync-sample
        aws-region: ap-southeast-2

    - name: Test S3 List with OIDC role
      run: |
        aws s3 ls s3://t04glovern-role-sync-sample

This workflow will call the shared workflow in the t04glovern/GitHub-Actions repository and tell it to deploy a change to its AWS IAM role by passing it the following three parameters:

  • aws-account - The AWS account ID to deploy the role to
  • aws-region - The AWS region to deploy the role to
  • role-template - The CloudFormation template path to deploy

You should also create the AWS CloudFormation template that contains the role for the repository you are deploying. The following file .github/aws-oidc-role.yml should be created with contents similar to the following:

AWSTemplateFormatVersion: 2010-09-09
Description: 'GitHub OIDC: t04glovern/role-sync-sample'

Parameters:
  GitHubOrgName:
    Type: String
    Default: t04glovern
  GitHubRepoName:
    Type: String
    Default: role-sync-sample

Resources:
  Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub oidc-${GitHubOrgName}-${GitHubRepoName}
      Policies:
        - PolicyName: s3-read-permissions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetObject
                Resource:
                  - !Sub arn:aws:s3:::t04glovern-role-sync-sample
                  - !Sub arn:aws:s3:::t04glovern-role-sync-sample/*
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRoleWithWebIdentity
            Principal:
              Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
            Condition:
              StringLike:
                token.actions.githubusercontent.com:sub: !Sub repository_owner:${GitHubOrgName}:actor:*:job_workflow_ref:${GitHubOrgName}/${GitHubRepoName}/.github/workflows/*:repo:${GitHubOrgName}/${GitHubRepoName}

Outputs:
  OidcRoleAwsAccountId:
    Value: !Ref AWS::AccountId
  OidcRoleAwsRegion:
    Value: !Ref AWS::Region
  OidcRoleAwsRoleToAssume:
    Value: !GetAtt Role.Arn

In the above example, the role is given the ability to list and get objects from the S3 bucket t04glovern-role-sync-sample - This is just an example and you can change this to whatever you want.

Customize the GitHub repository OIDC subject claims

The final step is to specify the OIDC subject claims for any GitHub repository that you'd like to use this new role sync system on. To do this you will need to generate a personal access token (PAT) with the repo scope.

Personal Access Token scopes
Personal Access Token scopes

Use this personal access token to set the OIDC subject claims for the repository you'd like to use this new role sync system on by running the following command (substituting the repository name and your PAT):

curl \
  -X PUT \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer <YOUR-TOKEN>" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/t04glovern/role-sync-sample/actions/oidc/customization/sub \
  -d '{"use_default":false,"include_claim_keys":["repository_owner","actor","job_workflow_ref","repo"]}'

NOTE: Once you have run the command above and confirmed it worked, you can delete the personal access token you created.

Additional CODEOWNERS setup

An extra step that can be taken to ensure that any pull requests looking to update the OIDC role are reviewed by the correct people is to add a CODEOWNERS file to the repository. This file can be used to specify which users or teams are required to review pull requests that modify certain files or directories. For example, the following can be added to a CODEOWNERS file in the root of the repository to ensure that any pull requests that modify the .github/aws-oidc-role.yml and .github/workflows/oidc-role-sync.yml files are reviewed by the user @t04glovern:

.github/workflows/oidc-role-sync.yml @t04glovern
.github/aws-oidc-role.yml @t04glovern

Summary

In this post, we explored how we could theoretically enable the semi-self-service updates of OIDC roles by using the new customizable OIDC subject claims feature and GitHub Actions.

I'm interested to hear what you think of this approach - and more importantly, if you can see any glaring issues with doing this kind of automation. I've gotten some feedback that having some manual intervention for expanding deployment roles is perhaps a good thing and doesn't need to be automated (which I think I am beginning to come around to).

A major caveat to this approach is that it puts a lot of trust in the actor's GitHub account not being compromised - and that they are not maliciously trying to expand their permissions. I think this is a valid concern and I'm interested to hear what you think about this.

Please feel free to reach out to me on Twitter @nathangloverAUS or leave a comment below!

devopstar

DevOpStar by Nathan Glover | 2023