Deploy Terraform resources to AWS using GitHub Actions via OIDC
Tags: terraform, githubactions, aws
Published: 2024-06-27 01:56:51

The article explains how to configure OpenID Connect within your GitHub Actions workflows to authenticate with AWS, so that the workflow can access AWS resources. The common use case is define AWS infrastructure as code, using CloudFormation, CDK or Terraform, etc, then sync the infrastructure update in AWS through workflows on each code change commit. As we only focus on the IODC setup here, to make it simple, the demo workflow authenticates to AWS account firstly, then list all S3 buckets in that account.

So, what is OpenID Connect? 

[OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/) is an identity authentication protocol that is an extension of open authorization (OAuth) 2.0 to standardize the process for authenticating and authorizing users when they sign in to access digital services. OIDC provides authentication, which means verifying that users are who they say they are.

Let's talk about how OpenID Connect works with identity provider and federation.

In AWS, when managing user identities outside of AWS, you can use identity providers instead of creating IAM users in AWS account. With an identity provider (IdP), you can give these external user identities permissions (defined in IAM role) to use AWS resources in your account. An external IdP provides identity information to AWS using either OpenID Connect (OIDC) or SAML 2.0. Identity providers help keep your AWS account secure because you don't have to distribute or embed long-term security credentials, such as access keys, in your application. 

In the demo, GitHub is an external identity provider for AWS. GitHub Actions workflows can be treated as external user identities.

The core process is to authenticate with AWS using temporary credentials within your GitHub Actions workflows. It contains the following steps: 

1. Firstly, establish trust between AWS account and GitHub by adding GitHub identity provider in AWS IAM service.
2. Create IAM role that allows to be assumed by the new added identity provider.
3. GitHub actions workflow assumes the IAM role:
    * Workflow retrieves a JWT token from GitHub;
    * Workflow makes an AssumeRoleWithWebIdentity call to AWS STS service with JWT token. 
    * AWS STS service validates the trust relationship, and returns temporary credentials in AWS that map to the IAM role with permissions to access specific resources in AWS account
4. Workflow access AWS resources. 

Here is a diagram that displays the authentication process.

![auth-process](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dclfbzm6zpvn8k75pq28.png)

## Prerequisites

* An AWS account with permission to create OIDC identity provider, role, attach policy in AWS IAM service.
* An GitHub account to create a repository and workflows.

## Solution Overview

### Step 1 & 2 Add GitHub IdP & Create IAM Role in AWS Account

You can follow the process and description from the [blog](https://aws.amazon.com/blogs/security/use-iam-roles-to-connect-github-actions-to-actions-in-aws/) just like what I did. I won't repeat the process here because the blog is clear and understandable. 

After completed, you will have an identity provider named **token.actions.githubusercontent.com** with OpenID Connect type, and an IAM role named **GitHubAction-AssumeRoleWithAction** with trust relationship. 

Identity Provider

![IdP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q95fz1362glmb0fbeiuv.png)

IAM Role -> Trust Relationship

![trust-replationship](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ci4a7qsos7dlsxz49usu.png)

### Step 3. Create GitHub Actions Workflow

In your GitHub repository, create a workflow yaml file named **get-started.yaml** in **.github/workflows** directory with below code. 

Update <YOUR_AWS_ACCOUNT_REGION> and <YOUR_AWS_ACCOUNT_ID> with the real values. Update the branches in the code if you want to trigger the workflow from other branches.

```yaml
# This is a basic workflow to help you get started with Actions
name: Get Started
# Controls when the action will run. Invokes the workflow on push events but only for the main branch
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
env:
  AWS_REGION: <YOUR_AWS_ACCOUNT_REGION>
  ROLE_TO_ASSUME: arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:role/GitHubAction-AssumeRoleWithAction
  ROLE_SESSION_NAME: GitHub_to_AWS_via_FederatedOIDC
# 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:
  AssumeRoleAndCallIdentity:
    runs-on: ubuntu-latest
    steps:
      - name: Git clone the repository
        uses: actions/checkout@v4
      - name: configure aws credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.ROLE_TO_ASSUME }}
          role-session-name: ${{ env.ROLE_SESSION_NAME }}
          aws-region: ${{ env.AWS_REGION }}
      # Hello from AWS: WhoAmI
      - name: Sts GetCallerIdentity
        run: |
          aws sts get-caller-identity
```

Commit and push to remote. A build is triggered automatically. Below screenshot shows the workflow assumes the IAM role ROLE_TO_ASSUME successfully. 

![workflow-get-started](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wsmmuy8f4wfq6x1sr9ks.png)

At this moment, the IAM role doesn't attach any policy, which means your workflow has no permissions for AWS resources. 

Next, I'm going to use the workflow to list all buckets in my AWS account. To enable it, we need to attach an IAM policy with necessary permission on IAM role **GitHubAction-AssumeRoleWithAction**.

1. From AWS IAM Console, find role, Add permissions -> create inline policy. 

![create-inline-policy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ia9v4uk6ll90qy062w37.png)

2. Select a service: S3
3. Action allowed: ListAllMyBuckets
4. Resources: All
5. Next
6. Policy name: AllowListAllMyBuckets
7. Create Policy

![role-policies](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oesdztzxeann2aabq79z.png)

> Please be noted, to list all AWS buckets, we choose action s3:ListAllMyBuckets, not s3:ListBucket.

Now, you have attached an inline policy on IAM role. Then add a new step after step **Sts GetCallerIdentity** to list all buckets using AWS CLI. The step name is **List All S3 Buckets**, it executes shell script "aws s3 ls" to list all buckets from your AWS account.

```
...
- name: List All S3 Buckets
  run: aws s3 ls
```

Commit and push change to remote. A new build is triggered automatically. 

![workflow-list-all-buckets](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zq6uj96bf02x1g5t7ujj.png)

Following the least privilege principle, I only granted s3:ListAllMyBuckets permission to the role. The policies assigned to the role determine what the federated users are allowed to do in AWS. You should follow the principle for best practice according to your needs in the daily work. 

## Others...

Since we are taking about best practice, let's enhance our workflow by replacing hard-coded or sensitive environment variables with GitHub Secrets and variables. I'm going to save these environment variables in **Settings -> Secrets and variables -> Actions**. Add repository variables. 

![add-variables](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4slkxxu1us4mayzei64q.png)

Finally replace the hard-coded environment variables in workflow.

Hard-coded:
```yaml
AWS_REGION: xxxxxx
ROLE_TO_ASSUME: arn:aws:iam::xxxxxxxxxxx:role/GitHubAction-AssumeRoleWithAction
ROLE_SESSION_NAME: GitHub_to_AWS_via_FederatedOIDC
```
Retrieved from GitHub repository variables
```yaml
AWS_REGION: ${{ vars.AWS_REGION }}
ROLE_TO_ASSUME: ${{ vars.ROLE_TO_ASSUME }}
ROLE_SESSION_NAME: ${{ vars.ROLE_SESSION_NAME }}
```

Commit and push to remote. A new build is triggered automatically. The workflow will retrieve these variables from GitHub Secrets and variables at the beginning of job.

You can find the source code from GitHub repo: https://github.com/camillehe1992/demo-for-aws-deployment-via-oidc

## Summary

You have learned how to add GitHub as an identity provider and create an IAM role with correct trust relationship in AWS. Besides, you created a GitHub Actions workflow that authenticates to AWS and list all S3 buckets in the AWS account within the build. 

As I mentioned, the common use case is we define AWS infrastructure as code, these AWS resources are provisioned and managed through GitHub repositories and Actions workflows, a.k.a CICD pipelines. All changes are traceable and controllable, easy to repeat and recover with the power of IoC (Infrastructure as Code) and CICD tools.

## References

https://aws.amazon.com/blogs/security/use-iam-roles-to-connect-github-actions-to-actions-in-aws/
https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers.html
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html

Thanks for reading!