From 35e4339d67167b078703e486744302baee3e4b64 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Fri, 4 Sep 2026 11:44:11 -0600 Subject: [PATCH 1/2] feat: add Twilio state backend foundation --- aws-twilio-state.tf | 107 ++++++++++++++++++++++++++++++++++++++++++++ main.tf | 5 ++- outputs.tf | 10 +++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 aws-twilio-state.tf diff --git a/aws-twilio-state.tf b/aws-twilio-state.tf new file mode 100644 index 0000000..43017ed --- /dev/null +++ b/aws-twilio-state.tf @@ -0,0 +1,107 @@ +# tfroot-twilio receives an isolated state bucket and OIDC role. The role +# is intentionally limited to the exact state object, its lockfile, and the +# SOPS key required to decrypt future encrypted provider inputs. +resource "aws_s3_bucket_public_access_block" "twilio_state" { + bucket = aws_s3_bucket.private[local.twilio_state_bucket].id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_ownership_controls" "twilio_state" { + bucket = aws_s3_bucket.private[local.twilio_state_bucket].id + + rule { + object_ownership = "BucketOwnerEnforced" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "twilio_state" { + bucket = aws_s3_bucket.private[local.twilio_state_bucket].id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_versioning" "twilio_state" { + bucket = aws_s3_bucket.private[local.twilio_state_bucket].id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_iam_role" "github_actions_twilio_state" { + name = "github-actions-twilio-state" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Federated = aws_iam_openid_connect_provider.github_actions.arn + } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = { + "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" + } + StringLike = { + # This repository emits the organization/repository ID subject form. + "token.actions.githubusercontent.com:sub" = "repo:makeitworkcloud@195502628/tfroot-twilio@1356437102:*" + } + } + } + ] + }) + + tags = { + ManagedBy = "Terraform" + Purpose = "twilio-opentofu-state" + } +} + +resource "aws_iam_role_policy" "github_actions_twilio_state" { + name = "twilio-opentofu-state" + role = aws_iam_role.github_actions_twilio_state.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "DecryptTwilioSops" + Effect = "Allow" + Action = [ + "kms:Decrypt", + "kms:DescribeKey" + ] + Resource = aws_kms_key.sops.arn + }, + { + Sid = "ListTwilioStateBucket" + Effect = "Allow" + Action = ["s3:ListBucket"] + Resource = aws_s3_bucket.private[local.twilio_state_bucket].arn + }, + { + Sid = "ManageTwilioStateObjects" + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ] + Resource = [ + "${aws_s3_bucket.private[local.twilio_state_bucket].arn}/${local.twilio_state_key}", + "${aws_s3_bucket.private[local.twilio_state_bucket].arn}/${local.twilio_state_key}.tflock" + ] + } + ] + }) +} diff --git a/main.tf b/main.tf index 3c0ecec..0f02c62 100644 --- a/main.tf +++ b/main.tf @@ -9,6 +9,8 @@ locals { channel_project_state_bucket = "mitw-tf-channel-project" channel_project_site_bucket = "orthodox-channel-site-332355796717" channel_project_site_log_bucket = "orthodox-channel-site-logs-332355796717" + twilio_state_bucket = "mitw-tf-twilio-infra" + twilio_state_key = "tofu/twilio/terraform.tfstate" channel_project_state_keys = toset([ "tofu/aws/terraform.tfstate", "tofu/cloudflare/terraform.tfstate", @@ -20,7 +22,8 @@ locals { "mitw-tf-cloudflare-infra", "mitw-tf-github-repos", "mitw-tf-libvirt-infra", - "mitw-tf-namecheap-infra" + "mitw-tf-namecheap-infra", + "mitw-tf-twilio-infra", ]) s3_public_buckets = toset([]) s3_web_buckets = toset([ diff --git a/outputs.tf b/outputs.tf index e2449e3..7d97b37 100644 --- a/outputs.tf +++ b/outputs.tf @@ -54,6 +54,16 @@ output "channel_project_site_deploy_role_arn" { value = aws_iam_role.github_actions_channel_project_site_deploy.arn } +output "twilio_state_bucket_name" { + description = "Versioned private S3 bucket for tfroot-twilio OpenTofu state" + value = aws_s3_bucket.private[local.twilio_state_bucket].bucket +} + +output "twilio_github_actions_state_role_arn" { + description = "GitHub OIDC role for tfroot-twilio OpenTofu state and SOPS decrypt access" + value = aws_iam_role.github_actions_twilio_state.arn +} + output "sops_kms_key_arn" { description = "KMS key ARN for future SOPS AWS KMS recipients" value = aws_kms_key.sops.arn From 323d049a28bdcb7111e1776f1ee59add8fe56def Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:46:06 +0000 Subject: [PATCH 2/2] chore: apply pre-commit fixes --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index e69090f..6373f71 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ No modules. | [aws_iam_role.github_actions_channel_project_site_infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.github_actions_channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.github_actions_sops_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.github_actions_twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.opencode_mcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.bedrock_batch_invoke](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.bedrock_batch_s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | @@ -39,6 +40,7 @@ No modules. | [aws_iam_role_policy.github_actions_channel_project_site_log_delivery](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.github_actions_channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.github_actions_sops_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.github_actions_twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.opencode_mcp_agent_pipe](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.opencode_mcp_secrets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy_attachment.opencode_mcp_oauth](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | @@ -61,16 +63,20 @@ No modules. | [aws_s3_bucket_lifecycle_configuration.agent_pipe](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource | | [aws_s3_bucket_ownership_controls.agent_pipe](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource | | [aws_s3_bucket_ownership_controls.channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource | +| [aws_s3_bucket_ownership_controls.twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource | | [aws_s3_bucket_policy.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | | [aws_s3_bucket_policy.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | | [aws_s3_bucket_public_access_block.agent_pipe](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | | [aws_s3_bucket_public_access_block.bedrock_batch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | | [aws_s3_bucket_public_access_block.channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | | [aws_s3_bucket_public_access_block.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_public_access_block.twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | | [aws_s3_bucket_public_access_block.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | | [aws_s3_bucket_server_side_encryption_configuration.agent_pipe](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | | [aws_s3_bucket_server_side_encryption_configuration.channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | | [aws_s3_bucket_versioning.channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | +| [aws_s3_bucket_versioning.twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | | [aws_s3_bucket_website_configuration.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_website_configuration) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [sops_file.secret_vars](https://registry.terraform.io/providers/carlpett/sops/latest/docs/data-sources/file) | data source | @@ -100,6 +106,8 @@ No inputs. | [sops\_kms\_key\_arn](#output\_sops\_kms\_key\_arn) | KMS key ARN for future SOPS AWS KMS recipients | | [sops\_secrets\_operator\_access\_key](#output\_sops\_secrets\_operator\_access\_key) | Access key for the k3s sops-secrets-operator to decrypt SOPS AWS KMS secrets | | [sops\_secrets\_operator\_iam\_user\_arn](#output\_sops\_secrets\_operator\_iam\_user\_arn) | IAM user ARN for the k3s sops-secrets-operator | +| [twilio\_github\_actions\_state\_role\_arn](#output\_twilio\_github\_actions\_state\_role\_arn) | GitHub OIDC role for tfroot-twilio OpenTofu state and SOPS decrypt access | +| [twilio\_state\_bucket\_name](#output\_twilio\_state\_bucket\_name) | Versioned private S3 bucket for tfroot-twilio OpenTofu state | | [web\_bucket\_endpoints](#output\_web\_bucket\_endpoints) | Website endpoints for public web S3 buckets |