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..ed0b213 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 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):$(OPENTOFU_ENVIRONMENT_GATED),true: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/README.md b/README.md
index d72b481..47ef018 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,6 +107,8 @@ 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\_workload\_identity\_provider](#output\_opencode\_mcp\_workload\_identity\_provider) | WIF provider for the OpenCode Kubernetes ServiceAccount. |
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..ed181a0 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -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
+}