diff --git a/.gitignore b/.gitignore index ca178e3..1cc7265 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ plan-output.txt # Generated canonical pre-commit configuration /.pre-commit-config.yaml /.pre-commit-config.yaml.tmp + +# Ignore plaintext credential files. Only the validated SOPS ciphertext path is tracked. +/secrets/*.env +/secrets/*.yaml +!/secrets/secrets.yaml diff --git a/AGENTS.md b/AGENTS.md index 47f6717..307f5af 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. +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 5e18d1b..8742993 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -2,19 +2,21 @@ ## 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 -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. `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. ## Central generated files 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 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.