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 |
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