Secure Terraform Workflow With GitHub Actions OIDC Role
Hey guys! Today, we're diving into setting up a secure and efficient workflow for Terraform using GitHub Actions and OpenID Connect (OIDC). This approach lets us ditch those static AWS keys and embrace short-lived credentials, making our infrastructure management way more secure. We'll be focusing on assuming an AWS IAM role (terraform-apply
) that has access to our backend (S3/DDB/KMS) and can manage Identity Center resources. Let's jump in!
Context: Why OIDC for Terraform?
In the world of infrastructure as code, security is paramount. Storing static AWS keys directly in your repositories is a big no-no. It's like leaving the keys to your kingdom under the doormat! OpenID Connect (OIDC) provides a much safer alternative. By using OIDC, we can have GitHub Actions request temporary credentials from AWS, which are valid only for a short period. This significantly reduces the risk of compromised credentials.
Our goal is to enable GitHub Actions to run Terraform commands without needing those long-term static keys. We'll achieve this by configuring an IAM role in AWS that trusts GitHub's OIDC provider. When a workflow runs, it will assume this role, gaining the necessary permissions to interact with our AWS resources. This setup ensures that only authorized actions can make changes to our infrastructure, and the temporary nature of the credentials minimizes the potential damage from any accidental exposure.
This approach is crucial for maintaining a strong security posture in your infrastructure management. By adopting OIDC, you're not just following best practices; you're building a more robust and secure system. The benefits extend beyond just security; it also simplifies credential management and reduces the overhead of rotating static keys. Itβs a win-win for both security and operational efficiency. Plus, it's way cooler to say you're using OIDC. π
Scope: What We'll Cover
We're going to set up an IAM role named terraform-apply
that trusts our GitHub repository and a specific branch or environment. This role will have two main policies attached:
- TerraformStateAccess: This policy will grant the role permissions to access our Terraform state backend, which includes the S3 bucket, DynamoDB lock table, and KMS Customer Master Key (CMK).
- Identity Center Minimal: Initially, this policy will have broader permissions for Identity Center-related actions (
identitystore:*
,sso:*
,ssoadmin:*
). We'll start broad and then narrow down the permissions later to follow the principle of least privilege.
We'll also update the KMS key policy to allow our terraform-apply
role to use the alias/tf-state
key. This ensures that the role can encrypt and decrypt the Terraform state, which is essential for secure state management.
Finally, we'll create two minimal GitHub Actions workflows:
.github/workflows/plan.yml
: This workflow will trigger on pull requests, assume the OIDC role, and runterraform init
andterraform plan
for ourenvs/sandbox
environment. It will then post the plan as a comment on the pull request, making it easy to review changes before they're applied..github/workflows/apply.yml
: This workflow will trigger on pushes to themain
branch, assume the OIDC role, and runterraform init
andterraform apply
for ourenvs/sandbox
environment. This will automatically apply changes to our infrastructure when code is merged into the main branch.
What's Out of Scope
To keep things focused, we're excluding a few advanced topics from this guide:
- Fine-grained least-privilege per action: We'll start with broader permissions and refine them later.
- Multi-account federation: We're focusing on a single account setup.
- Production approvals: We won't cover manual approval steps for production deployments, but this can be added as a follow-up.
Pre-requisites: Getting Ready
Before we dive into the implementation, there are a couple of things we need to make sure are in place:
- OIDC Identity Provider: You need to have an OIDC identity provider configured in your AWS account. This involves setting up a trust relationship between your GitHub organization and your AWS account. The issuer should be
https://token.actions.githubusercontent.com
, and the audience should bests.amazonaws.com
. - Backend ARN Values: We'll need the ARNs (Amazon Resource Names) for our Terraform backend resources, including the S3 bucket, DynamoDB lock table, and KMS CMK. These values are typically available from the outputs of your bootstrap Terraform configuration. Make sure you have these ARNs handy, as we'll need them to configure the IAM policies.
These pre-requisites are essential for the OIDC workflow to function correctly. If you haven't already set up the OIDC identity provider, you'll need to do that first. It's a one-time setup per AWS account and provides the foundation for secure authentication with GitHub Actions. Think of it as the handshake between your GitHub workflows and your AWS environment. Once these are in place, we can move on to the exciting part: implementing the Terraform configurations and GitHub Actions workflows!
Implementation Steps: Let's Build It!
Alright, let's get our hands dirty and build this thing! We'll break it down into a few key steps:
1. IAM Terraform (infra/ci-oidc/
)
We'll start by creating the necessary IAM resources using Terraform. This will involve defining the IAM role, the policies, and attaching them together.
-
aws_iam_role.terraform_apply
with Trust Policy:- We'll create an IAM role named
terraform-apply
. The crucial part here is the trust policy, which defines who can assume this role. We'll configure it to trust the GitHub OIDC provider, but only for our specific repository and branch (or environment). - The trust policy will include the following conditions:
Principal.Federated = arn:aws:iam::<acct>:oidc-provider/token.actions.githubusercontent.com
: This specifies that the role can be assumed by principals federated through the OIDC provider.Condition.StringEquals["token.actions...:aud"] = "sts.amazonaws.com"
: This ensures that the token is intended for the AWS STS service.Condition.StringLike["token.actions...:sub"] = "repo:<org>/<repo>:ref:refs/heads/main"
(orenvironment:<name>
): This is the most important part! It limits the trust to our specific repository and branch. You can also use theenvironment
claim for more granular control.
- We'll create an IAM role named
-
aws_iam_policy.TerraformStateAccess
:- This policy will grant the role permissions to access our Terraform state backend. This includes permissions for:
- S3:
s3:ListBucket
,s3:GetObject
,s3:PutObject
,s3:DeleteObject
on the state bucket. - DynamoDB:
dynamodb:PutItem
,dynamodb:GetItem
,dynamodb:DeleteItem
,dynamodb:UpdateItem
on the DynamoDB lock table. - KMS:
kms:Encrypt
,kms:Decrypt
,kms:GenerateDataKey
,kms:DescribeKey
for the KMS CMK.
- S3:
- This policy will grant the role permissions to access our Terraform state backend. This includes permissions for:
-
aws_iam_policy.TerraformIdentityCenterMinimal
:- This policy will grant the role minimal permissions to manage Identity Center resources. Initially, we'll use broader actions like
identitystore:*
,sso:*
, andssoadmin:*
. We'll narrow these down later to follow the principle of least privilege.
- This policy will grant the role minimal permissions to manage Identity Center resources. Initially, we'll use broader actions like
-
Attach Both Policies to Role:
- Finally, we'll attach both the
TerraformStateAccess
andTerraformIdentityCenterMinimal
policies to theterraform-apply
role.
- Finally, we'll attach both the
2. KMS Key Policy Update in Bootstrap
We need to update the KMS key policy to allow our terraform-apply
role to use the CMK. This ensures that the role can encrypt and decrypt the Terraform state.
- Allow
arn:aws:iam::<acct>:role/terraform-apply
to Use the CMK:- We'll add a statement to the KMS key policy that allows the
terraform-apply
role to perform actions likekms:Encrypt
,kms:Decrypt
, andkms:GenerateDataKey
on the key.
- We'll add a statement to the KMS key policy that allows the
- Keep Root Admin Statement to Avoid Lockout:
- It's crucial to keep the root admin statement in the KMS key policy. This prevents accidental lockouts and ensures that the root user can always access the key.
3. GitHub Actions
Now, let's create the GitHub Actions workflows that will automate our Terraform deployments.
-
.github/workflows/plan.yml
: PR Trigger β OIDC Assume βinit/plan
forenvs/sandbox
β Comment Plan:- This workflow will trigger on pull requests to the repository.
- It will use the
aws-actions/configure-aws-credentials
action to assume theterraform-apply
role using OIDC. - It will then run
terraform init
andterraform plan
for ourenvs/sandbox
environment. - Finally, it will post the plan as a comment on the pull request using a GitHub Action for commenting.
-
.github/workflows/apply.yml
: Push tomain
β OIDC Assume βinit/apply
forenvs/sandbox
:- This workflow will trigger on pushes to the
main
branch. - It will use the
aws-actions/configure-aws-credentials
action to assume theterraform-apply
role using OIDC. - It will then run
terraform init
andterraform apply
for ourenvs/sandbox
environment.
- This workflow will trigger on pushes to the
-
(Optional) Add Protected Environment to Require Manual Approval for Apply:
- For added security, you can add a protected environment in GitHub and require manual approval before the
apply
workflow can run. This provides an extra layer of control over your deployments.
- For added security, you can add a protected environment in GitHub and require manual approval before the
Acceptance Criteria (Definition of Done): How We Know It Works
To make sure everything is working as expected, we'll use the following acceptance criteria:
- From PR: Workflow Posts a Plan Comment: When a pull request is created, the
plan
workflow should run and post a comment with the Terraform plan. - From Merge to Main: Workflow Applies Successfully: When code is merged into the
main
branch, theapply
workflow should run and apply the changes to our infrastructure without errors. - CloudTrail Shows
AssumeRoleWithWebIdentity
forterraform-apply
: We should be able to seeAssumeRoleWithWebIdentity
events in CloudTrail for theterraform-apply
role, indicating that the role is being assumed using OIDC. terraform init
/plan
Succeed (No KMS AccessDenied): Theterraform init
andterraform plan
commands should run successfully without any KMSAccessDenied
errors.- OIDC Trust Is Scoped to This Repo and Desired Branch/Env: We need to verify that the OIDC trust policy is correctly scoped to our repository and the desired branch or environment. This ensures that only authorized actions can assume the role.
Validation Commands: Let's Test It Out
To validate our setup, we can use the following commands:
# Identify role ARN
aws iam list-roles | grep terraform-apply
# Verify KMS policy includes the role
aws kms get-key-policy --key-id alias/tf-state --policy-name default | jq .
# Run a local plan using the same role (SSO or test assume)
cd envs/sandbox
terraform init
terraform plan
These commands will help us verify that the IAM role exists, the KMS key policy is correctly configured, and we can run Terraform commands locally using the same role.
Conclusion
So there you have it! We've walked through setting up a secure Terraform workflow using GitHub Actions and OIDC. This approach not only enhances your security posture but also streamlines your infrastructure management. By using short-lived credentials and automating deployments, you can focus on building awesome things without worrying about the risks of static keys. Remember to always prioritize security and follow the principle of least privilege. Happy Terraforming, guys!