From 9a847bec04aa25a962ef7c6b73cb2519e20437ae Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Fri, 4 Sep 2026 12:51:36 -0600 Subject: [PATCH 1/4] docs: add encrypted Twilio credential template --- .gitignore | 4 ++++ AGENTS.md | 4 ++-- docs/bootstrap.md | 6 ++++-- secrets/twilio.sops.env.example | 6 ++++++ 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 secrets/twilio.sops.env.example diff --git a/.gitignore b/.gitignore index ca178e3..55cb9b8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ plan-output.txt # Generated canonical pre-commit configuration /.pre-commit-config.yaml /.pre-commit-config.yaml.tmp + +# Ignore plaintext credential files. The exact SOPS ciphertext path is allowed. +/secrets/*.env +!/secrets/*.sops.env diff --git a/AGENTS.md b/AGENTS.md index 47f6717..5b671ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,8 +2,8 @@ OpenTofu root for Make IT Work Cloud Twilio control-plane infrastructure. -This root owns only Twilio phone-number inventory and inbound messaging-webhook configuration. It must not own the OpenCode SMS bridge workload, Cloudflare workload route or DNS, number-to-agent mapping, approved-source allowlist, bridge credentials, or encryption material. Those runtime concerns belong to `kustomize-cluster`. +This root owns only Twilio phone-number inventory and inbound messaging-webhook configuration. It must not own the OpenCode SMS bridge workload, Cloudflare workload route or DNS, number-to-agent mapping, approved-source allowlist, bridge credentials, or runtime encryption material. Those runtime concerns belong to `kustomize-cluster`. -This bootstrap contains no Twilio resources, provider configuration, backend credentials, encrypted secrets, state, or number identifiers. Use GitHub MCP and pull-request CI plans as validation authority. `main` is an environment-gated apply path; use scoped branches and pull requests, never direct pushes. Do not run OpenTofu, SOPS, state, import, or apply commands from this server. +This bootstrap contains no Twilio resources, provider configuration, backend credentials, encrypted secrets, state, or number identifiers. A separately approved future credential-delivery change may add only an encrypted Twilio provider environment file; it must never hold bridge runtime credentials or encryption material. Use GitHub MCP and pull-request CI plans as validation authority. `main` is an environment-gated apply path; use scoped branches and pull requests, never direct pushes. Do not run OpenTofu, SOPS, state, import, or apply commands from this server. The shared workflow is owned by `shared-workflows`; the runner image and canonical pre-commit configuration are owned by `images/tfroot-runner`. Keep any future SOPS data encrypted and never expose credentials, decrypted values, state, private keys, or sensitive plans. diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 5e18d1b..99c5964 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -8,13 +8,15 @@ The evaluated provider is `RJPearson94/twilio` `0.27.1`. Registry documentation ## Ownership boundary -The future root may own only Twilio phone-number inventory and inbound messaging-webhook configuration. `kustomize-cluster` remains the owner of the OpenCode bridge workload, workload `TunnelBinding`/DNS, fixed number-to-agent map, approved-source allowlist, runtime secrets, and state-encryption inputs. The root must never create a bridge-worker API key or duplicate a runtime owner. +The future root may own only Twilio phone-number inventory and inbound messaging-webhook configuration. `kustomize-cluster` remains the owner of the OpenCode bridge workload, workload `TunnelBinding`/DNS, fixed number-to-agent map, approved-source allowlist, runtime secrets, and runtime-encryption inputs. The root must never create a bridge-worker API key or duplicate a runtime owner. ## Backend and credential contract [`tfroot-aws` PR #43](https://github.com/makeitworkcloud/tfroot-aws/pull/43) created the canonical backend producer: a dedicated private, encrypted, versioned bucket and an exact-repository GitHub OIDC role restricted to the state object, lockfile, and SOPS KMS decrypt/describe access. This root selects that backend through the reusable workflow's `aws-role-to-assume` input and uses S3 native locking. No static AWS backend credential is stored in source or GitHub Actions secrets. -The checked-in `.sops.yaml` identifies the approved encryption recipient but no encrypted secret file exists yet. A later, separately reviewed change must define a Twilio credential delivery path that cannot expose a token in source, CI logs, or OpenTofu state. Only after that prerequisite passes pull-request validation may a later root change add provider configuration or Twilio inventory. Any actual Twilio provisioning or webhook update still requires explicit owner confirmation before merge because `main` invokes the environment-gated apply path. +The checked-in `.sops.yaml` identifies the approved encryption recipient. The tracked [`secrets/twilio.sops.env.example`](../secrets/twilio.sops.env.example) is placeholders only. Before any provider configuration is authored, an authorized owner must create the exact `secrets/twilio.sops.env` file from that template, replace its three values only on a secure workstation, encrypt it with the repository's SOPS policy, and commit only the resulting ciphertext. The future file will contain `TWILIO_ACCOUNT_SID`, `TWILIO_API_KEY`, and `TWILIO_API_SECRET`; it must never contain bridge runtime inputs. The provider process will receive those values through SOPS `exec-env`, not static provider attributes, plaintext files, or GitHub Actions secrets. + +Only after the encrypted credential path passes pull-request validation may a later root change add provider configuration or Twilio inventory. Any actual Twilio provisioning or webhook update still requires explicit owner confirmation before merge because `main` invokes the environment-gated apply path. ## Central generated files diff --git a/secrets/twilio.sops.env.example b/secrets/twilio.sops.env.example new file mode 100644 index 0000000..d6a9f5f --- /dev/null +++ b/secrets/twilio.sops.env.example @@ -0,0 +1,6 @@ +# Copy to secrets/twilio.sops.env. Replace values only on a secure workstation, +# encrypt the copy with the repository .sops.yaml policy, and commit only ciphertext. +# Never commit this content after substituting real values. +TWILIO_ACCOUNT_SID=REPLACE_WITH_ACCOUNT_SID +TWILIO_API_KEY=REPLACE_WITH_MAIN_API_KEY_SID +TWILIO_API_SECRET=REPLACE_WITH_MAIN_API_KEY_SECRET From 26f7885fa4ab140b0285de5a9196a309d8ce400f Mon Sep 17 00:00:00 2001 From: xnoto Date: Fri, 4 Sep 2026 12:56:21 -0600 Subject: [PATCH 2/4] chore: secrets.yaml added --- secrets/secrets.yaml | 13 +++++++++++++ secrets/twilio.sops.env.example | 6 ------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 secrets/secrets.yaml delete mode 100644 secrets/twilio.sops.env.example diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml new file mode 100644 index 0000000..86cdb7b --- /dev/null +++ b/secrets/secrets.yaml @@ -0,0 +1,13 @@ +TWILIO_ACCOUNT_SID: ENC[AES256_GCM,data:1rwEFnR254orfuyd5oajxb7zlb93laTsMsMJLjIpjnBI5Q==,iv:5XcuMzThrHkJmDpAbLmyc5e6G+L3QTN1VzG6Qhjh368=,tag:g2jeVTbcdu5NUekvhXBeUw==,type:str] +TWILIO_API_KEY: ENC[AES256_GCM,data:dQUXkSVvrV6te+UGucxhFsZPV5EGMW/+OpaHsdHk4rGmKA==,iv:GqXYHPsPlK5gZAk340h01z9Vj2HAzNfMDCE7n1DVDoo=,tag:pkWjmFtSXWazYZ1XdYN71w==,type:str] +TWILIO_API_SECRET: ENC[AES256_GCM,data:zY6hExO4kYaz+epRpo5nfqTHIPrpVTu1DB2UbAO3fnE=,iv:lfPOnaegZz4zEBdOrPDWXAHJUCffEyXO5dxgPRScu3o=,tag:qAjtyATA7lxuY2sGq/S25A==,type:str] +sops: + kms: + - arn: arn:aws:kms:us-west-2:332355796717:key/0a45c0f6-71dc-4d54-ab33-9df4de1a9e91 + created_at: "2026-09-04T18:56:10Z" + enc: AQICAHj1IggLFhM4nJnKEvmbEpk5E9RxZZoxpZYUW0taoyrz1AFIluGLSHyrmRFsHAf3aPP4AAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQM7P1gO6LNa70z3QdJAgEQgDtBYYAcxnlG3JVNNFlGvf3pSIrw0YPoGhjCBbtZ8/yrdLmynNOYcn+4WLEXs9PEDHwQuWzYIpkQ/0PN0Q== + aws_profile: "" + lastmodified: "2026-09-04T18:56:10Z" + mac: ENC[AES256_GCM,data:XmZx/uuVBBXEAqngEeLPdwvoMSIpKDN5NlZ3lDwMbnRavRqtM0tUDKsRqjYLPrz07wjRou3VS5jNfDLwiyn6apcmSsvPsL5vxrIA7JZyGUEN4cpNmmSvH3VcJtkxdB+mpGv64uMgtm6yqwzlXYa8YRsXAU1cY4sfokzmIC3LyFg=,iv:cDQbhfpuWet961tRBUdcgWktgnpzZ9xC4wp6i/XTSC8=,tag:5U7DO6e9MtyvL+U6z1MOkA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.10.2 diff --git a/secrets/twilio.sops.env.example b/secrets/twilio.sops.env.example deleted file mode 100644 index d6a9f5f..0000000 --- a/secrets/twilio.sops.env.example +++ /dev/null @@ -1,6 +0,0 @@ -# Copy to secrets/twilio.sops.env. Replace values only on a secure workstation, -# encrypt the copy with the repository .sops.yaml policy, and commit only ciphertext. -# Never commit this content after substituting real values. -TWILIO_ACCOUNT_SID=REPLACE_WITH_ACCOUNT_SID -TWILIO_API_KEY=REPLACE_WITH_MAIN_API_KEY_SID -TWILIO_API_SECRET=REPLACE_WITH_MAIN_API_KEY_SECRET From 65e0b6689fc22a915428bff4f4a7ffb9926d42c5 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Fri, 4 Sep 2026 12:58:59 -0600 Subject: [PATCH 3/4] feat: validate encrypted Twilio credentials --- .gitignore | 4 +++- AGENTS.md | 2 +- Makefile | 17 +++++++++++++---- docs/bootstrap.md | 6 +++--- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 55cb9b8..5c7124a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ plan-output.txt /.pre-commit-config.yaml /.pre-commit-config.yaml.tmp -# Ignore plaintext credential files. The exact SOPS ciphertext path is allowed. +# Ignore plaintext credential files. Only the validated SOPS ciphertext path is tracked. /secrets/*.env !/secrets/*.sops.env +/secrets/*.yaml +!/secrets/secrets.yaml diff --git a/AGENTS.md b/AGENTS.md index 5b671ab..307f5af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,6 @@ OpenTofu root for Make IT Work Cloud Twilio control-plane infrastructure. This root owns only Twilio phone-number inventory and inbound messaging-webhook configuration. It must not own the OpenCode SMS bridge workload, Cloudflare workload route or DNS, number-to-agent mapping, approved-source allowlist, bridge credentials, or runtime encryption material. Those runtime concerns belong to `kustomize-cluster`. -This bootstrap contains no Twilio resources, provider configuration, backend credentials, encrypted secrets, state, or number identifiers. A separately approved future credential-delivery change may add only an encrypted Twilio provider environment file; it must never hold bridge runtime credentials or encryption material. Use GitHub MCP and pull-request CI plans as validation authority. `main` is an environment-gated apply path; use scoped branches and pull requests, never direct pushes. Do not run OpenTofu, SOPS, state, import, or apply commands from this server. +The root owns the SOPS-encrypted `secrets/secrets.yaml` provider credential file and no other secret material. It contains only the Twilio provider environment-variable names. The root contains no Twilio resources, provider configuration, state, or number identifiers. Use GitHub MCP and pull-request CI plans as validation authority. `main` is an environment-gated apply path; use scoped branches and pull requests, never direct pushes. Do not run OpenTofu, SOPS, state, import, or apply commands from this server. The shared workflow is owned by `shared-workflows`; the runner image and canonical pre-commit configuration are owned by `images/tfroot-runner`. Keep any future SOPS data encrypted and never expose credentials, decrypted values, state, private keys, or sensitive plans. diff --git a/Makefile b/Makefile index 97e74e3..573da03 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,9 @@ TERRAFORM := $(shell which tofu) S3_BUCKET := mitw-tf-twilio-infra S3_REGION := us-west-2 S3_KEY := tofu/twilio/terraform.tfstate +TWILIO_CREDENTIALS := secrets/secrets.yaml -.PHONY: clean init plan apply test pre-commit-config pre-commit-check-deps pre-commit-install-hooks +.PHONY: clean init credentials-check plan apply test pre-commit-config pre-commit-check-deps pre-commit-install-hooks clean: @find . -name .terraform -type d | xargs -r rm -rf @@ -18,12 +19,20 @@ init: clean -backend-config="region=${S3_REGION}" \ -backend-config="use_lockfile=true" -plan: init +# The encrypted file is verified and exposed only to this short-lived child +# process. It does not configure the Twilio provider or call Twilio APIs. +credentials-check: + @test -f "${TWILIO_CREDENTIALS}" + @sops filestatus "${TWILIO_CREDENTIALS}" | jq -e '.encrypted == true' >/dev/null + @sops exec-env "${TWILIO_CREDENTIALS}" 'test -n "$$TWILIO_ACCOUNT_SID" && test -n "$$TWILIO_API_KEY" && test -n "$$TWILIO_API_SECRET"' + +plan: init credentials-check @${TERRAFORM} plan -refresh=false -input=false -compact-warnings # There are intentionally no provider configurations or Twilio resources in -# this root, so backend selection is the only stateful behavior on main. -apply: init +# this root, so backend selection and encrypted-credential validation are the +# only stateful behaviors on main. +apply: init credentials-check @${TERRAFORM} apply -auto-approve -refresh=false -input=false -compact-warnings test: pre-commit-config pre-commit-install-hooks diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 99c5964..8742993 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -2,9 +2,9 @@ ## Scope -This root validates the OpenTofu toolchain, remote state backend, and provider installation only. It creates, imports, updates, or deletes **no** Twilio resources. In particular, it does not purchase or configure phone numbers, webhooks, credentials, messaging services, or runtime routing. +This root validates the OpenTofu toolchain, remote state backend, and encrypted provider-credential delivery only. It creates, imports, updates, or deletes **no** Twilio resources. In particular, it does not purchase or configure phone numbers, webhooks, messaging services, or runtime routing. -The evaluated provider is `RJPearson94/twilio` `0.27.1`. Registry documentation confirms it supports Twilio phone-number resources and inbound `messaging` webhook fields, but it is community-maintained and requires existing account credentials. The root intentionally declares no provider configuration, so CI receives no Twilio credentials and performs no provider-side API call. +The evaluated provider is `RJPearson94/twilio` `0.27.1`. Registry documentation confirms it supports Twilio phone-number resources and inbound `messaging` webhook fields, but it is community-maintained and requires existing account credentials. The root intentionally declares no provider configuration, so CI validates SOPS credential delivery without performing a provider-side API call. ## Ownership boundary @@ -14,7 +14,7 @@ The future root may own only Twilio phone-number inventory and inbound messaging [`tfroot-aws` PR #43](https://github.com/makeitworkcloud/tfroot-aws/pull/43) created the canonical backend producer: a dedicated private, encrypted, versioned bucket and an exact-repository GitHub OIDC role restricted to the state object, lockfile, and SOPS KMS decrypt/describe access. This root selects that backend through the reusable workflow's `aws-role-to-assume` input and uses S3 native locking. No static AWS backend credential is stored in source or GitHub Actions secrets. -The checked-in `.sops.yaml` identifies the approved encryption recipient. The tracked [`secrets/twilio.sops.env.example`](../secrets/twilio.sops.env.example) is placeholders only. Before any provider configuration is authored, an authorized owner must create the exact `secrets/twilio.sops.env` file from that template, replace its three values only on a secure workstation, encrypt it with the repository's SOPS policy, and commit only the resulting ciphertext. The future file will contain `TWILIO_ACCOUNT_SID`, `TWILIO_API_KEY`, and `TWILIO_API_SECRET`; it must never contain bridge runtime inputs. The provider process will receive those values through SOPS `exec-env`, not static provider attributes, plaintext files, or GitHub Actions secrets. +The checked-in `.sops.yaml` identifies the approved encryption recipient. `secrets/secrets.yaml` is the only accepted provider credential file. It holds the encrypted `TWILIO_ACCOUNT_SID`, `TWILIO_API_KEY`, and `TWILIO_API_SECRET` values; it must never contain bridge runtime inputs. `make plan` and `make apply` first require SOPS to recognize the file as encrypted, then use `sops exec-env` to assert the three variables in a short-lived child process. They never write plaintext files, set static provider attributes, or use GitHub Actions secrets. The older `secrets/twilio.sops.env.example` placeholder is superseded and must not be used. Only after the encrypted credential path passes pull-request validation may a later root change add provider configuration or Twilio inventory. Any actual Twilio provisioning or webhook update still requires explicit owner confirmation before merge because `main` invokes the environment-gated apply path. From 18fcae402dcad747a793c56afff5a3225d76d1ce Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Fri, 4 Sep 2026 13:00:10 -0600 Subject: [PATCH 4/4] chore: retire superseded credential template --- .gitignore | 1 - secrets/twilio.sops.env.example | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 secrets/twilio.sops.env.example diff --git a/.gitignore b/.gitignore index 5c7124a..1cc7265 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,5 @@ plan-output.txt # Ignore plaintext credential files. Only the validated SOPS ciphertext path is tracked. /secrets/*.env -!/secrets/*.sops.env /secrets/*.yaml !/secrets/secrets.yaml diff --git a/secrets/twilio.sops.env.example b/secrets/twilio.sops.env.example new file mode 100644 index 0000000..ef29c1c --- /dev/null +++ b/secrets/twilio.sops.env.example @@ -0,0 +1,3 @@ +# Superseded: do not use this path. +# The only accepted Twilio provider credential file is the SOPS-encrypted +# secrets/secrets.yaml document described in docs/bootstrap.md.