Skip to content
Anthony DiMarcoJul 19, 2022 2:27:46 AM7 min read

Identity Federation for GitHub Actions on AWS

Identity Federation for GitHub Actions on AWS

Identity Federation for GitHub Actions on AWS

Identity Federation for GitHub Actions on AWS

OIDC for GitHub Actions

GitHub Actions recently implemented 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

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).

Build job Docker image to ECR

Build job Docker image to 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.

GitHub encrypted secrets

GitHub encrypted secrets

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.

The old way - IAM Users with static credentials

The old way - IAM Users with static credentials

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: OIDC Identity Federation

Now that GitHub has added OpenID Connect (OIDC) support for GitHub Actions (as documented here on the GitHub Roadmap), we can now 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)
The new way - OIDC Identity Federation

The new way - OIDC Identity Federation

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 do it

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.

GitHub Actions console output

GitHub Actions console output

Conclusion

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 GCP or Azure, or if you want to integrate it with HashiCorp Vault - check the official GitHub documentation here.

avatar

Anthony DiMarco

Read more articles by Anthony DiMarco

RELATED ARTICLES

The information presented in this article is accurate as of 7/19/23. Follow the ScaleSec blog for new articles and updates.