diff --git a/README.md b/README.md index b1ec3f3..d72b481 100644 --- a/README.md +++ b/README.md @@ -68,14 +68,19 @@ No modules. | [google_apikeys_key.this](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/apikeys_key) | resource | | [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.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.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.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.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 | ## Inputs @@ -99,6 +104,8 @@ No modules. | Name | Description | | ---- | ----------- | | [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. | | [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. | diff --git a/identity.tf b/identity.tf index 0092f02..27d0912 100644 --- a/identity.tf +++ b/identity.tf @@ -14,6 +14,31 @@ resource "google_project_iam_member" "terraformer" { member = "serviceAccount:${google_service_account.terraformer.email}" } +# This service account is reserved for OpenCode's direct connection to the +# Google-managed Cloud CLI MCP server. It has no key and can only be +# impersonated by the named Kubernetes ServiceAccount through WIF. +resource "google_service_account" "opencode_mcp" { + project = google_project.this.project_id + account_id = "opencode-mcp" + display_name = "OpenCode managed MCP" + + depends_on = [google_project_service.this] +} + +resource "google_project_iam_member" "opencode_mcp" { + for_each = toset([ + "roles/browser", + "roles/cloudkms.viewer", + "roles/iam.securityReviewer", + "roles/mcp.toolUser", + "roles/serviceusage.serviceUsageViewer", + ]) + + project = google_project.this.project_id + role = each.value + member = "serviceAccount:${google_service_account.opencode_mcp.email}" +} + resource "google_iam_workload_identity_pool" "github" { project = google_project.this.project_id workload_identity_pool_id = "github" @@ -47,3 +72,38 @@ resource "google_service_account_iam_member" "github_workload_identity_user" { role = "roles/iam.workloadIdentityUser" member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${local.github_repository}" } + +# Kubernetes publishes the public ServiceAccount signing keys at this issuer. +# The provider accepts only the OpenCode workload identity, preventing other +# cluster workloads from impersonating the Google MCP service account. +resource "google_iam_workload_identity_pool" "kubernetes" { + project = google_project.this.project_id + workload_identity_pool_id = "kubernetes" + display_name = "k3s workloads" + description = "k3s ServiceAccount identities for Make IT Work Cloud workloads." + + depends_on = [google_project_service.this] +} + +resource "google_iam_workload_identity_pool_provider" "opencode_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 = "opencode" + display_name = "OpenCode k3s workload" + + attribute_mapping = { + "google.subject" = "assertion.sub" + } + + attribute_condition = "assertion.sub == \"system:serviceaccount:opencode:opencode-mcp\"" + + oidc { + issuer_uri = "https://api.makeitwork.cloud" + } +} + +resource "google_service_account_iam_member" "opencode_mcp_workload_identity_user" { + service_account_id = google_service_account.opencode_mcp.name + role = "roles/iam.workloadIdentityUser" + member = "principal://iam.googleapis.com/${google_iam_workload_identity_pool.kubernetes.name}/subject/system:serviceaccount:opencode:opencode-mcp" +} diff --git a/main.tf b/main.tf index 8ce80cd..c23e1fd 100644 --- a/main.tf +++ b/main.tf @@ -4,6 +4,7 @@ locals { project_services = toset([ "apikeys.googleapis.com", "cloudbilling.googleapis.com", + "cloudcli.googleapis.com", "cloudkms.googleapis.com", "cloudresourcemanager.googleapis.com", "generativelanguage.googleapis.com", diff --git a/outputs.tf b/outputs.tf index 9e95736..56b6c3f 100644 --- a/outputs.tf +++ b/outputs.tf @@ -22,3 +22,13 @@ output "github_workload_identity_provider" { description = "GitHub Actions Workload Identity Provider resource name." value = google_iam_workload_identity_pool_provider.github.name } + +output "opencode_mcp_service_account_email" { + description = "Keyless service account for the managed Google Cloud MCP server." + value = google_service_account.opencode_mcp.email +} + +output "opencode_mcp_workload_identity_provider" { + description = "WIF provider for the OpenCode Kubernetes ServiceAccount." + value = google_iam_workload_identity_pool_provider.opencode_kubernetes.name +}