From 67f9ec5f803284ba6676e0f503fd721bb8329de8 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Tue, 8 Sep 2026 21:08:59 -0600 Subject: [PATCH 1/6] feat(gcloud-mcp): add keyless project inventory identity --- .github/workflows/opentofu.yml | 1 + Makefile | 10 ++++++- identity.tf | 51 ++++++++++++++++++++++++++++++++++ main.tf | 1 + outputs.tf | 12 +++++++- 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/.github/workflows/opentofu.yml b/.github/workflows/opentofu.yml index 4c2331c..f697508 100644 --- a/.github/workflows/opentofu.yml +++ b/.github/workflows/opentofu.yml @@ -20,6 +20,7 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository uses: makeitworkcloud/shared-workflows/.github/workflows/opentofu.yml@main with: + environment: production gcp-workload-identity-provider: projects/920734942788/locations/global/workloadIdentityPools/github/providers/github gcp-service-account: terraformer@makeitworkcloud.iam.gserviceaccount.com secrets: diff --git a/Makefile b/Makefile index 7108353..fe1c71d 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,14 @@ TOFU := $(shell command -v tofu) GCS_BUCKET ?= makeitworkcloud-tf-gcp-infra GCS_PREFIX ?= state +# The reusable workflow assigns the protected GitHub Environment before the +# apply job starts. Keep local `make apply` interactive while making that +# approved CI invocation non-interactive. +TOFU_APPLY_ARGS := +ifeq ($(GITHUB_ACTIONS),true) +TOFU_APPLY_ARGS := -auto-approve -input=false +endif + .PHONY: help bootstrap-prepare bootstrap-plan bootstrap-apply bootstrap-migrate init plan apply test pre-commit-config pre-commit-check-deps pre-commit-install-hooks help: @@ -53,7 +61,7 @@ plan: init @$(TOFU) plan -compact-warnings apply: init - @$(TOFU) apply -compact-warnings + @$(TOFU) apply -compact-warnings $(TOFU_APPLY_ARGS) test: pre-commit-config pre-commit-install-hooks @pre-commit run --all-files diff --git a/identity.tf b/identity.tf index 27d0912..c6555c1 100644 --- a/identity.tf +++ b/identity.tf @@ -39,6 +39,31 @@ resource "google_project_iam_member" "opencode_mcp" { member = "serviceAccount:${google_service_account.opencode_mcp.email}" } +# This identity backs the cluster-internal local gcloud-mcp server. Its roles +# support project-wide asset and IAM metadata inventory without resource +# mutation, Secret Manager payload access, or Cloud Storage object reads. +resource "google_service_account" "gcloud_mcp" { + project = google_project.this.project_id + account_id = "gcloud-mcp" + display_name = "OpenCode gcloud MCP" + + depends_on = [google_project_service.this] +} + +resource "google_project_iam_member" "gcloud_mcp" { + for_each = toset([ + "roles/cloudasset.viewer", + "roles/cloudkms.viewer", + "roles/iam.securityReviewer", + "roles/serviceusage.serviceUsageConsumer", + "roles/serviceusage.serviceUsageViewer", + ]) + + project = google_project.this.project_id + role = each.value + member = "serviceAccount:${google_service_account.gcloud_mcp.email}" +} + resource "google_iam_workload_identity_pool" "github" { project = google_project.this.project_id workload_identity_pool_id = "github" @@ -107,3 +132,29 @@ resource "google_service_account_iam_member" "opencode_mcp_workload_identity_use role = "roles/iam.workloadIdentityUser" member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.kubernetes.name}/subject/system:serviceaccount:opencode:opencode-mcp" } + +# A separate provider avoids broadening the existing OpenCode federation +# condition. Only the dedicated cluster-internal gcloud-mcp workload can +# impersonate this Google service account. +resource "google_iam_workload_identity_pool_provider" "gcloud_mcp_kubernetes" { + project = google_project.this.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.kubernetes.workload_identity_pool_id + workload_identity_pool_provider_id = "gcloud-mcp" + display_name = "gcloud MCP k3s workload" + + attribute_mapping = { + "google.subject" = "assertion.sub" + } + + attribute_condition = "assertion.sub == \"system:serviceaccount:mcp:gcloud-mcp\"" + + oidc { + issuer_uri = "https://api.makeitwork.cloud" + } +} + +resource "google_service_account_iam_member" "gcloud_mcp_workload_identity_user" { + service_account_id = google_service_account.gcloud_mcp.name + role = "roles/iam.workloadIdentityUser" + member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.kubernetes.name}/subject/system:serviceaccount:mcp:gcloud-mcp" +} diff --git a/main.tf b/main.tf index c23e1fd..15c515d 100644 --- a/main.tf +++ b/main.tf @@ -3,6 +3,7 @@ locals { project_services = toset([ "apikeys.googleapis.com", + "cloudasset.googleapis.com", "cloudbilling.googleapis.com", "cloudcli.googleapis.com", "cloudkms.googleapis.com", diff --git a/outputs.tf b/outputs.tf index 56b6c3f..acbc538 100644 --- a/outputs.tf +++ b/outputs.tf @@ -4,7 +4,7 @@ output "project_id" { } output "state_bucket_name" { - description = "GCS bucket to configure as the OpenTofu backend after bootstrap." + description = "GCS bucket to configure as the OpenTofu backend." value = google_storage_bucket.state.name } @@ -32,3 +32,13 @@ output "opencode_mcp_workload_identity_provider" { description = "WIF provider for the OpenCode Kubernetes ServiceAccount." value = google_iam_workload_identity_pool_provider.opencode_kubernetes.name } + +output "gcloud_mcp_service_account_email" { + description = "Keyless service account for the cluster-internal gcloud MCP server." + value = google_service_account.gcloud_mcp.email +} + +output "gcloud_mcp_workload_identity_provider" { + description = "WIF provider for the gcloud MCP Kubernetes ServiceAccount." + value = google_iam_workload_identity_pool_provider.gcloud_mcp_kubernetes.name +} From 52ea67cd0c34406336bb771bfc8e2b3ab67ffeff Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Tue, 8 Sep 2026 21:47:05 -0600 Subject: [PATCH 2/6] chore(outputs): preserve existing MCP description --- outputs.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outputs.tf b/outputs.tf index acbc538..957a3ed 100644 --- a/outputs.tf +++ b/outputs.tf @@ -24,7 +24,7 @@ output "github_workload_identity_provider" { } output "opencode_mcp_service_account_email" { - description = "Keyless service account for the managed Google Cloud MCP server." + description = "Keyless service account for the managed Google Cloud MCP server after bootstrap." value = google_service_account.opencode_mcp.email } From 1ddde7e45ac58eaaa48d15e9d030cc8e787418fd Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Tue, 8 Sep 2026 22:04:23 -0600 Subject: [PATCH 3/6] fix(apply): require environment gate marker --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index fe1c71d..ed0b213 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ TOFU := $(shell command -v tofu) GCS_BUCKET ?= makeitworkcloud-tf-gcp-infra GCS_PREFIX ?= state -# The reusable workflow assigns the protected GitHub Environment before the -# apply job starts. Keep local `make apply` interactive while making that -# approved CI invocation non-interactive. +# The reusable workflow assigns the protected GitHub Environment and emits this +# marker only from its apply step. Keep local `make apply` interactive and make +# CI non-interactive only after that explicit environment-gated invocation. TOFU_APPLY_ARGS := -ifeq ($(GITHUB_ACTIONS),true) +ifeq ($(GITHUB_ACTIONS):$(OPENTOFU_ENVIRONMENT_GATED),true:true) TOFU_APPLY_ARGS := -auto-approve -input=false endif From 6ff34bbbfa81c276137b4cfc00b5026cb9716ef7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 04:13:32 +0000 Subject: [PATCH 4/6] chore: apply pre-commit fixes --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d72b481..e5a17f4 100644 --- a/README.md +++ b/README.md @@ -69,16 +69,20 @@ No modules. | [google_billing_budget.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/billing_budget) | resource | | [google_iam_workload_identity_pool.github](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool) | resource | | [google_iam_workload_identity_pool.kubernetes](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool) | resource | +| [google_iam_workload_identity_pool_provider.gcloud_mcp_kubernetes](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool_provider) | resource | | [google_iam_workload_identity_pool_provider.github](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool_provider) | resource | | [google_iam_workload_identity_pool_provider.opencode_kubernetes](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool_provider) | resource | | [google_kms_crypto_key.sops](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_crypto_key) | resource | | [google_kms_key_ring.sops](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_key_ring) | resource | | [google_project.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project) | resource | +| [google_project_iam_member.gcloud_mcp](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_iam_member.opencode_mcp](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_iam_member.terraformer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_service.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_service) | resource | +| [google_service_account.gcloud_mcp](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | | [google_service_account.opencode_mcp](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | | [google_service_account.terraformer](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | +| [google_service_account_iam_member.gcloud_mcp_workload_identity_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | | [google_service_account_iam_member.github_workload_identity_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | | [google_service_account_iam_member.opencode_mcp_workload_identity_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_member) | resource | | [google_storage_bucket.state](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket) | resource | @@ -103,11 +107,13 @@ No modules. | Name | Description | | ---- | ----------- | +| [gcloud\_mcp\_service\_account\_email](#output\_gcloud\_mcp\_service\_account\_email) | Keyless service account for the cluster-internal gcloud MCP server. | +| [gcloud\_mcp\_workload\_identity\_provider](#output\_gcloud\_mcp\_workload\_identity\_provider) | WIF provider for the gcloud MCP Kubernetes ServiceAccount. | | [github\_workload\_identity\_provider](#output\_github\_workload\_identity\_provider) | GitHub Actions Workload Identity Provider resource name. | -| [opencode\_mcp\_service\_account\_email](#output\_opencode\_mcp\_service\_account\_email) | Keyless service account for the managed Google Cloud MCP server. | +| [opencode\_mcp\_service\_account\_email](#output\_opencode\_mcp\_service\_account\_email) | Keyless service account for the managed Google Cloud MCP server after bootstrap. | | [opencode\_mcp\_workload\_identity\_provider](#output\_opencode\_mcp\_workload\_identity\_provider) | WIF provider for the OpenCode Kubernetes ServiceAccount. | | [project\_id](#output\_project\_id) | Created GCP project ID. | | [sops\_kms\_resource](#output\_sops\_kms\_resource) | Cloud KMS resource for SOPS gcp\_kms recipients. | -| [state\_bucket\_name](#output\_state\_bucket\_name) | GCS bucket to configure as the OpenTofu backend after bootstrap. | +| [state\_bucket\_name](#output\_state\_bucket\_name) | GCS bucket to configure as the OpenTofu backend. | | [terraformer\_service\_account\_email](#output\_terraformer\_service\_account\_email) | GitHub Actions Workload Identity Federation service account email. | From 2636e918697d642b2e54e2b2fff5fe7b0e06f378 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Tue, 8 Sep 2026 22:15:54 -0600 Subject: [PATCH 5/6] chore(outputs): retain existing descriptions --- outputs.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outputs.tf b/outputs.tf index 957a3ed..ed181a0 100644 --- a/outputs.tf +++ b/outputs.tf @@ -4,7 +4,7 @@ output "project_id" { } output "state_bucket_name" { - description = "GCS bucket to configure as the OpenTofu backend." + description = "GCS bucket to configure as the OpenTofu backend after bootstrap." value = google_storage_bucket.state.name } @@ -24,7 +24,7 @@ output "github_workload_identity_provider" { } output "opencode_mcp_service_account_email" { - description = "Keyless service account for the managed Google Cloud MCP server after bootstrap." + description = "Keyless service account for the managed Google Cloud MCP server." value = google_service_account.opencode_mcp.email } From 7919e87dfe1544b78ae094e9776ee855c73d88ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 04:16:27 +0000 Subject: [PATCH 6/6] chore: apply pre-commit fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e5a17f4..47ef018 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,10 @@ No modules. | [gcloud\_mcp\_service\_account\_email](#output\_gcloud\_mcp\_service\_account\_email) | Keyless service account for the cluster-internal gcloud MCP server. | | [gcloud\_mcp\_workload\_identity\_provider](#output\_gcloud\_mcp\_workload\_identity\_provider) | WIF provider for the gcloud MCP Kubernetes ServiceAccount. | | [github\_workload\_identity\_provider](#output\_github\_workload\_identity\_provider) | GitHub Actions Workload Identity Provider resource name. | -| [opencode\_mcp\_service\_account\_email](#output\_opencode\_mcp\_service\_account\_email) | Keyless service account for the managed Google Cloud MCP server after bootstrap. | +| [opencode\_mcp\_service\_account\_email](#output\_opencode\_mcp\_service\_account\_email) | Keyless service account for the managed Google Cloud MCP server. | | [opencode\_mcp\_workload\_identity\_provider](#output\_opencode\_mcp\_workload\_identity\_provider) | WIF provider for the OpenCode Kubernetes ServiceAccount. | | [project\_id](#output\_project\_id) | Created GCP project ID. | | [sops\_kms\_resource](#output\_sops\_kms\_resource) | Cloud KMS resource for SOPS gcp\_kms recipients. | -| [state\_bucket\_name](#output\_state\_bucket\_name) | GCS bucket to configure as the OpenTofu backend. | +| [state\_bucket\_name](#output\_state\_bucket\_name) | GCS bucket to configure as the OpenTofu backend after bootstrap. | | [terraformer\_service\_account\_email](#output\_terraformer\_service\_account\_email) | GitHub Actions Workload Identity Federation service account email. |