Configuring OIDC for GitHub Actions on AWS
GitHub Actions offers a feature that allows workflows to generate signed OpenID Connect tokens, which has exciting implications for anyone using GitHub Actions to manage resources in AWS.
This feature allows secure and seamless integration with AWS IAM and eliminates the need to store and rotate long-term AWS credentials in GitHub.
GitHub Actions for AWS Workloads: Then & Now
GitHub Actions is a popular and lightweight way to build automation into your software development workflow. Instead of maintaining a Jenkins cluster or a CodeBuild pipeline, developers can put a YAML workflow definition in a known location in their repo and be off and running.
Most build jobs finish by publishing an artifact of some kind - be it a compiled executable, a Docker image, or an AMI. Many will also deploy it to a running environment. Below we see an example of a job that builds a Docker image and publishes it to Amazon’s Elastic Container Registry (ECR).
Publishing a Docker image to ECR, or updating a running ECS service, or dropping a compiled binary in S3, or any other action touching AWS resources requires proper credentials. The job must be authenticated and authorized to perform these actions.
Until now, it was necessary to generate long-term credentials and store them somewhere accessible to the build job in GitHub. This often leads to checking AWS credentials into GitHub repos directly through sheer laziness/convenience (please, please don’t do this).
GitHub provides a way to store encrypted secrets securely, but even then - these are long-lived static credentials. Who’s rotating them and how often? I bet you aren’t.
The Old Way: IAM Users with Static Credentials
The old approach to generating long-term credentials for GitHub Actions jobs is:
- Create an IAM User for the build job
- Generate static credentials for that user (the AWS access key / secret key pair)
- Store them in GitHub (hopefully securely)
- Retrieve them at build time and set them as environment variables in the build job
Even fairly recent tutorials on the subject promote this approach and automate it for ease of use. This approach does involve some IAM best practices - e.g., having the GitHub User assume-role to acquire permissions, and automating the configuration with code, but it still requires you to generate and store keys.
If you’re used to working with security in AWS, you should have some reservations about the big-picture design. IAM Users are generally reserved for living, breathing human beings, whereas we prefer to use IAM Roles for machines and applications.
IAM Users can have web console access and MFA tokens. These are not qualities we generally associate with build jobs. The documentation for IAM Roles sums up the distinction nicely:
…instead of being uniquely associated with one person, a role is intended to be assumable by anyone who needs it. Also, a role does not have standard long-term credentials such as a password or access keys associated with it. Instead, when you assume a role, it provides you with temporary security credentials for your role session.
The New Way: GitHub OIDC Identifty Federation
Since GitHub added OpenID Connect (OIDC) support for GitHub Actions (as documented here on the GitHub Roadmap), we can securely deploy to any cloud provider that supports OIDC (including AWS) using short-lived keys that are automatically rotated for each deployment.
The primary benefits are:
- No need to store long-term credentials and plan for their rotation
- Use your cloud provider’s native IAM tools to configure least-privilege access for your build jobs
- Even easier to automate with Infrastructure as Code (IaC)
Now, your GitHub Actions job can acquire a JWT from the GitHub OIDC provider, which is a signed token including various details about the job (including what repo the action is running in).
If you’ve configured AWS IAM to trust the GitHub OICD provider, your job can exchange this JWT for short-lived AWS credentials that let it assume an IAM Role. With those credentials, your build job can use the AWS CLI or APIs directly to publish artifacts, deploy services, etc.
How to Implement Identity Federation Using GitHub OIDC Actions on AWS
Enough talk. Let’s see some code. Here’s some IaC to help you implement this solution yourself.
Step 1: Add the Identity Provider to AWS
You’ll need to configure IAM in your AWS account to trust tokens presented by the GitHub OIDC provider before your jobs can trade them for AWS credentials. AWS provides documentation for setting this up with the web console here, but we want to do this with code:
Terraform
resource "aws_iam_openid_connect_provider" "githubOidc" {
url = "https://token.actions.githubusercontent.com"
client_id_list = [
"sts.amazonaws.com"
]
thumbprint_list = ["a031c46782e6e6c662c2c87c76da9aa62ccabd8e"]
}
CloudFormation
GithubOidc:
Type: AWS::IAM::OIDCProvider
Properties:
Url: https://token.actions.githubusercontent.com
ThumbprintList: [a031c46782e6e6c662c2c87c76da9aa62ccabd8e]
ClientIdList:
- sts.amazonaws.com
Note that in both flavors of the configuration, we specify the ClientIdList
as sts.amazonaws.com
- this will be the “audience” claim presented in the JWT when we use the official action to obtain credentials later on.
Step 2: Configure an Assume-Role Policy
Now that IAM is configured to trust tokens presented by the GitHub OIDC provider, we now need to create an IAM Role for our build jobs to use and tell IAM that it can be assumed by anyone with a valid token from that provider.
Terraform
data "aws_iam_policy_document" "github_allow" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.githubOidc.arn]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${GitHubOrg}/${GitHubRepo}:*"]
}
}
}
resource "aws_iam_role" "github_role" {
name = "GithubActionsRole"
assume_role_policy = data.aws_iam_policy_document.github_allow.json
}
CloudFormation
Role:
Type: AWS::IAM::Role
Properties:
RoleName: GitHubActionsRole
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Action: sts:AssumeRoleWithWebIdentity
Principal:
Federated: !Ref GithubOidc
Condition:
StringLike:
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubOrg}/${GitHubRepo}:*
Note how in both configuration blocks, we put a condition on the assume-role policy that the token.actions.githubusercontent.com:sub
claim must match a string with our GitHub org and repo in it.
This claim will be present in the JWT with a format like repo:my-org/my-repo:ref:refs/heads/my-branch
. Adding this condition ensures that only jobs running in your own GitHub repos can assume this role. Without it, any valid token from any GitHub action could get these AWS credentials.
You can further narrow scope with this condition to ensure that specific jobs can only be run by GitHub actions in specific repos, or specific branches. See additional information about hardening this configuration in the official docs from GitHub.
Step 3: Assume-role in GitHub Actions
GitHub provides an official Action that we can use to assume a specific AWS IAM role in a workflow. The syntax is as simple as:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: arn:aws:iam::123456789100:role/my-github-actions-role
aws-region: us-east-2
Here’s an example workflow that assumes the GitHubActionsRole we defined and uses the AWS CLI to prove that it has acquired valid credentials.
jobs:
build:
name: build
permissions:
id-token: write
contents: write
runs-on: ubuntu-18.04
steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@master
with:
aws-region: us-east-2
role-to-assume: arn:aws:iam::12345678910:role/GitHubActionsRole
role-session-name: GithubActionsSession
- run: aws sts get-caller-identity
Here we just use aws sts-get-caller-identity
as proof that the assume-role flow worked, but you can substitute anything your assumed role has permission to do. e.g., if you’ve granted permission to write to S3, you could:
- run: aws s3 sync . s3://my-prod-website-bucket
Step 4: Profit
If you’ve set everything up correctly, you should see output like the following in the GitHub Actions console when you run the proof of concept job above.
Learn More About AWS Cloud Solutions From ScaleSec
Hopefully, this has been a useful example of how to configure your GitHub Actions build jobs to securely and seamlessly use IAM Roles in AWS. If you’re using another cloud provider like Google Cloud Platform (GCP) or Microsoft Azure, or if you want to integrate it with HashiCorp Vault - check the official GitHub documentation here.
If you’re looking for a cloud security and compliance expert to help lead the charge, connect with our team at ScaleSec. We specialize in cloud security engineering and cloud compliance, so we’re happy to guide you through any complex cloud challenges you encounter.