preloader

Keyless AWS Deployments from GitHub Actions: A Practical Guide to OIDC Federation

Posted on: 3/4/2023 Keyless AWS Deployments from GitHub Actions: A Practical Guide to OIDC Federation

If your pipeline deploys to AWS with an access key ID and secret saved in GitHub secrets, you’re carrying around a long-lived credential. Someone has to rotate it, and if it ever leaks, whoever finds it can use it. There’s a cleaner option: let GitHub prove who it is to AWS with OpenID Connect (OIDC), and let AWS give back credentials that expire when the job finishes. In this post I’ll walk through how that trust works and how to set it up on both sides.

A quick word on GitHub Actions

GitHub Actions runs automated jobs in response to events in a repository. Its most common use is CI/CD, where it builds, tests and deploys every time code is pushed. It isn’t limited to pipelines, though. Teams also use it to tag releases, send notifications when issues are opened, migrate repositories and handle plenty of other chores.

For CI/CD in particular, I think it has a lot going for it:

Once a workflow is allowed to change things in your cloud account, though, the most important question is how much access it gets. The answer should be only what the deployment needs, and only for as long as the job runs. OIDC makes that easy to achieve.

How the OIDC handshake between GitHub and AWS works

The idea is a trust relationship. You tell AWS that it may accept identity tokens issued by GitHub, and you attach conditions to an IAM role that say which GitHub workflows may assume it. Those conditions are checked against claims inside the token. No AWS secret is ever stored in GitHub.

Here’s the flow for each job run:

  1. GitHub issues a token. When the job starts, GitHub’s OIDC provider mints a signed token. It contains claims that describe the run, such as repository, repository_owner and job_workflow_ref, and it’s presented to AWS.
  2. AWS checks the signature. AWS confirms that the token really came from GitHub by validating it against GitHub’s published signing keys.
  3. AWS evaluates the trust policy. If the token is genuine, AWS compares its claims with the conditions on the role. For example, the condition below allows any branch, pull request merge ref or environment in the octo-org/octo-repo repository to assume the role, and nobody else:
"Condition":
   "StringLike": {
   "token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:*"
 }
}
  1. AWS returns temporary credentials. If every condition matches, AWS issues short-lived credentials that only last for the job.

Sequence diagram of a GitHub Actions job exchanging a GitHub OIDC token with AWS for temporary role credentials

The pattern isn’t specific to AWS. Azure, Google Cloud and tools such as HashiCorp Vault accept GitHub’s OIDC tokens in the same way.

Setting up the AWS side

AWS needs two things: to know about GitHub as an identity provider, and a role that GitHub is allowed to assume.

1. Register GitHub as an OIDC identity provider in IAM. Use https://token.actions.githubusercontent.com as the provider URL and sts.amazonaws.com as the audience.

AWS IAM console adding an OpenID Connect identity provider for token.actions.githubusercontent.com with audience sts.amazonaws.com

2. Create the deployment role. Give it the permissions your deployment needs, as you would for any role. What’s different here is the trust relationship. That’s where you add conditions on the token’s claims so that only the repositories, branches or environments you choose can assume it. GitHub documents every claim you can use here.

IAM role trust relationship with the GitHub OIDC provider as a federated principal and conditions on the token’s claims

From now on, AWS will let a workflow assume this role, but only if it presents a valid GitHub token whose claims satisfy the trust policy.

Setting up the GitHub side

Over in the workflow, AWS’s own configure-aws-credentials action takes care of swapping the OIDC token for AWS credentials. You tell it which role to assume, give the session a name and pick a region, and the steps after it can call AWS as usual.

The example below copies a file into an S3 bucket on every push. Two details matter. The id-token: write permission is what lets the job ask GitHub for a token at all. The configure-aws-credentials step is where the role ARN, session name and region are set:

name: AWS example workflow
on:
  push
env:
  BUCKET_NAME: "example-bucket-name"
  AWS_REGION: "us-east-1"
# permission can be added at job level or workflow level
permissions:
  id-token: write # This is required for requesting the JWT
  contents: read # This is required for actions/checkout
jobs:
  S3PackageUpload:
    runs-on: ubuntu-latest
    steps:
      - name: Git clone the repository
        uses: actions/checkout@v3
      - name: configure aws credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: arn:aws:iam::1234567890:role/example-role
          role-session-name: samplerolesession
          aws-region: ${{ env.AWS_REGION }}
      # Upload a file to AWS s3
      - name: Copy index.html to s3
        run: aws s3 cp ./index.html s3://${{ env.BUCKET_NAME }}/

Each time this workflow runs, the action presents GitHub’s OIDC token to AWS and gets back temporary credentials. Those credentials carry exactly the permissions of the requested role and nothing more.

What you gain over static access keys

Static IAM user keys OIDC federation
Secret stored in GitHub Yes, a long-lived key pair None
Rotation Manual and easy to forget Not needed, because credentials expire with the job
Scope of access Whatever the IAM user can do A role you can limit to specific repos, branches or environments
Where access is managed Spread across IAM users and repo secrets In one place, through IAM roles and trust policies

Common pitfalls

Quick FAQ

Do I still need any AWS secrets in GitHub? No. The only thing the workflow needs is the role ARN, and that isn’t sensitive.

How long do the credentials last? They last for the role session. By default that’s one hour, and you can change it with the action’s role-duration-seconds input, up to the role’s maximum session duration.

Can I use different roles for staging and production? Yes, and you should. Create one role per environment and limit each trust policy to the matching GitHub environment or branch.

Wrapping up

Using OIDC between GitHub Actions and AWS means no long-lived keys to leak or rotate. Each job gets credentials that are scoped by an IAM trust policy and expire when the job ends. Setup takes one identity provider and one role in AWS, plus a single action in your workflow. If you’re still deploying with stored access keys, this is one of the easiest security wins available.

Learn more