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:
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:
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 that 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
andGitHubRepoName
parameters with your values - Additionally, make sure you replace theactor
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.
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!