Secure Terraform Workflow With GitHub Actions OIDC Role

by Henrik Larsen 56 views

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:

  1. 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).
  2. 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 run terraform init and terraform plan for our envs/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 the main branch, assume the OIDC role, and run terraform init and terraform apply for our envs/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:

  1. 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 be sts.amazonaws.com.
  2. 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" (or environment:<name>): This is the most important part! It limits the trust to our specific repository and branch. You can also use the environment claim for more granular control.
  • 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.
  • 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:*, and ssoadmin:*. We'll narrow these down later to follow the principle of least privilege.
  • Attach Both Policies to Role:

    • Finally, we'll attach both the TerraformStateAccess and TerraformIdentityCenterMinimal policies to the terraform-apply role.

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 like kms:Encrypt, kms:Decrypt, and kms:GenerateDataKey on the key.
  • 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 for envs/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 the terraform-apply role using OIDC.
    • It will then run terraform init and terraform plan for our envs/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 to main β†’ OIDC Assume β†’ init/apply for envs/sandbox:

    • This workflow will trigger on pushes to the main branch.
    • It will use the aws-actions/configure-aws-credentials action to assume the terraform-apply role using OIDC.
    • It will then run terraform init and terraform apply for our envs/sandbox environment.
  • (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.

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, the apply workflow should run and apply the changes to our infrastructure without errors.
  • CloudTrail Shows AssumeRoleWithWebIdentity for terraform-apply: We should be able to see AssumeRoleWithWebIdentity events in CloudTrail for the terraform-apply role, indicating that the role is being assumed using OIDC.
  • terraform init/plan Succeed (No KMS AccessDenied): The terraform init and terraform plan commands should run successfully without any KMS AccessDenied 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!