Anthony DiMarcoFeb 28, 2023 12:00:00 AM7 min read

GitHub Actions on AWS: How to Implement Identity Federation

GitHub Actions on AWS: How to Implement Identity Federation

 

GitHub Actions on AWS: How to Implement Identity Federation

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

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

GitHub Actions console output

 

GitHub Actions console output

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.

RELATED ARTICLES

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