Skip to content

feat(identity): add keyless gcloud MCP workload identity - #6

Merged
xnoto merged 6 commits into
mainfrom
feat/gcloud-mcp-identity
Sep 9, 2026
Merged

xnoto merged 6 commits into
mainfrom
feat/gcloud-mcp-identity

Conversation

@xnoto

@xnoto xnoto commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds desired state for a separate cluster-internal gcloud-mcp identity:

  • enables cloudasset.googleapis.com;
  • creates a dedicated keyless gcloud-mcp Google service account;
  • grants Cloud Asset, KMS, IAM security-review, and Service Usage read/inventory roles only;
  • adds a separate Kubernetes WIF provider and impersonation binding constrained to system:serviceaccount:mcp:gcloud-mcp; and
  • exposes non-secret service-account/provider outputs for the later GitOps consumer.

The local make apply path remains interactive. GitHub Actions adds unattended approval only when the reusable shared workflow has assigned the configured Environment and explicitly emits OPENTOFU_ENVIRONMENT_GATED=true.

Fixes #

None — owner-approved keyless local gcloud MCP preparation.

Type of change

  • Bug fix
  • Feature / enhancement
  • Documentation
  • Infrastructure (OpenTofu root or module)
  • GitOps desired state (manifests, kustomize, charts, SOPS/KSOPS secrets)
  • Container image
  • CI / reusable workflow
  • Refactor / cleanup
  • Breaking change

Validation

No local OpenTofu command was run. Do not inspect or reproduce raw sensitive plan output outside the PR checks.

Impact and rollout

Scope: “all” is implemented as project-wide, read-only Cloud Asset/IAM metadata inventory for makeitworkcloud; it does not add mutations, Secret Manager payload access, Cloud Storage object reads, service-account keys, or cross-project/org access.

Required order: merge the companion shared-workflows environment-gate PR first. Before this root can merge, confirm that tfroot-gcp's production GitHub Environment has required reviewers/protection rules. The root's merge/apply remains separately confirmation-gated.

Producer/consumer: this PR authors only GCP desired state. No ToolHive MCPServer, Kustomize workload, chart change, image selection, or deployment exists in this PR. A future GitOps PR must consume the applied identity and a verified immutable image digest.

Rollback: remove the new service account, IAM bindings, WIF provider, outputs, and API enablement through a reviewed follow-up OpenTofu PR; never delete or alter the existing OpenCode MCP identity as part of rollback.

Safety and secrets

  • Contains no plaintext secrets, decrypted SOPS values, state files, kubeconfigs, tokens, or private endpoints.
  • No local OpenTofu init/plan/apply/destroy/import/state operations were run or claimed — plans come from pull-request checks.
  • Breaking or irreversible effects are described above with rollback notes.

AI-assisted change: an OpenCode agent materially produced this change. Reviewers should verify the exact WIF subject, IAM role set, Cloud Asset API enablement, fail-closed apply marker, and CI-generated documentation before marking this draft ready.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

OpenTofu Test

OpenTofu test passed.

View the workflow run.

View run output
Terraform validate.......................................................Passed
Terraform validate with tflint...........................................Passed
Checkov..................................................................Passed
Terraform fmt............................................................Passed
Terraform docs...........................................................Passed
Detect hardcoded secrets.................................................Passed
check for case conflicts.................................................Passed
check for merge conflicts................................................Passed
check for broken symlinks............................(no files to check)Skipped
check vcs permalinks.....................................................Passed
detect destroyed symlinks................................................Passed
detect private key.......................................................Passed
fix end of files.........................................................Passed
mixed line ending........................................................Passed
trim trailing whitespace.................................................Passed
don't commit to branch..................................................Skipped
check for added large files..............................................Passed

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

OpenTofu Plan

OpenTofu plan passed.

View the workflow run.

View run output
OpenTofu will perform the following actions:

  # google_iam_workload_identity_pool.kubernetes will be created
  + resource "google_iam_workload_identity_pool" "kubernetes" {
      + deletion_policy           = "DELETE"
      + description               = "k3s ServiceAccount identities for Make IT Work Cloud workloads."
      + display_name              = "k3s workloads"
      + id                        = (known after apply)
      + mode                      = (known after apply)
      + name                      = (known after apply)
      + project                   = "makeitworkcloud"
      + state                     = (known after apply)
      + workload_identity_pool_id = "kubernetes"
    }

  # google_iam_workload_identity_pool_provider.gcloud_mcp_kubernetes will be created
  + resource "google_iam_workload_identity_pool_provider" "gcloud_mcp_kubernetes" {
      + attribute_condition                = "assertion.sub == \"system:serviceaccount:mcp:gcloud-mcp\""
      + attribute_mapping                  = {
          + "google.subject" = "assertion.sub"
        }
      + deletion_policy                    = "DELETE"
      + display_name                       = "gcloud MCP k3s workload"
      + id                                 = (known after apply)
      + name                               = (known after apply)
      + project                            = "makeitworkcloud"
      + state                              = (known after apply)
      + workload_identity_pool_id          = "kubernetes"
      + workload_identity_pool_provider_id = "gcloud-mcp"

      + oidc {
          + issuer_uri = "https://api.makeitwork.cloud"
        }
    }

  # google_iam_workload_identity_pool_provider.opencode_kubernetes will be created
  + resource "google_iam_workload_identity_pool_provider" "opencode_kubernetes" {
      + attribute_condition                = "assertion.sub == \"system:serviceaccount:opencode:opencode-mcp\""
      + attribute_mapping                  = {
          + "google.subject" = "assertion.sub"
        }
      + deletion_policy                    = "DELETE"
      + display_name                       = "OpenCode k3s workload"
      + id                                 = (known after apply)
      + name                               = (known after apply)
      + project                            = "makeitworkcloud"
      + state                              = (known after apply)
      + workload_identity_pool_id          = "kubernetes"
      + workload_identity_pool_provider_id = "opencode"

      + oidc {
          + issuer_uri = "https://api.makeitwork.cloud"
        }
    }

  # google_project_iam_member.gcloud_mcp["roles/cloudasset.viewer"] will be created
  + resource "google_project_iam_member" "gcloud_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/cloudasset.viewer"
    }

  # google_project_iam_member.gcloud_mcp["roles/cloudkms.viewer"] will be created
  + resource "google_project_iam_member" "gcloud_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/cloudkms.viewer"
    }

  # google_project_iam_member.gcloud_mcp["roles/iam.securityReviewer"] will be created
  + resource "google_project_iam_member" "gcloud_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/iam.securityReviewer"
    }

  # google_project_iam_member.gcloud_mcp["roles/serviceusage.serviceUsageConsumer"] will be created
  + resource "google_project_iam_member" "gcloud_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/serviceusage.serviceUsageConsumer"
    }

  # google_project_iam_member.gcloud_mcp["roles/serviceusage.serviceUsageViewer"] will be created
  + resource "google_project_iam_member" "gcloud_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/serviceusage.serviceUsageViewer"
    }

  # google_project_iam_member.opencode_mcp["roles/browser"] will be created
  + resource "google_project_iam_member" "opencode_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/browser"
    }

  # google_project_iam_member.opencode_mcp["roles/cloudkms.viewer"] will be created
  + resource "google_project_iam_member" "opencode_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/cloudkms.viewer"
    }

  # google_project_iam_member.opencode_mcp["roles/iam.securityReviewer"] will be created
  + resource "google_project_iam_member" "opencode_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/iam.securityReviewer"
    }

  # google_project_iam_member.opencode_mcp["roles/mcp.toolUser"] will be created
  + resource "google_project_iam_member" "opencode_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/mcp.toolUser"
    }

  # google_project_iam_member.opencode_mcp["roles/serviceusage.serviceUsageViewer"] will be created
  + resource "google_project_iam_member" "opencode_mcp" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + project = "makeitworkcloud"
      + role    = "roles/serviceusage.serviceUsageViewer"
    }

  # google_project_service.this["cloudasset.googleapis.com"] will be created
  + resource "google_project_service" "this" {
      + deletion_policy    = "DELETE"
      + disable_on_destroy = false
      + id                 = (known after apply)
      + project            = "makeitworkcloud"
      + service            = "cloudasset.googleapis.com"
    }

  # google_project_service.this["cloudcli.googleapis.com"] will be created
  + resource "google_project_service" "this" {
      + deletion_policy    = "DELETE"
      + disable_on_destroy = false
      + id                 = (known after apply)
      + project            = "makeitworkcloud"
      + service            = "cloudcli.googleapis.com"
    }

  # google_service_account.gcloud_mcp will be created
  + resource "google_service_account" "gcloud_mcp" {
      + account_id      = "gcloud-mcp"
      + deletion_policy = "DELETE"
      + disabled        = false
      + display_name    = "OpenCode gcloud MCP"
      + email           = "gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + id              = (known after apply)
      + member          = "serviceAccount:gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + name            = (known after apply)
      + project         = "makeitworkcloud"
      + unique_id       = (known after apply)
    }

  # google_service_account.opencode_mcp will be created
  + resource "google_service_account" "opencode_mcp" {
      + account_id      = "opencode-mcp"
      + deletion_policy = "DELETE"
      + disabled        = false
      + display_name    = "OpenCode managed MCP"
      + email           = "opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + id              = (known after apply)
      + member          = "serviceAccount:opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
      + name            = (known after apply)
      + project         = "makeitworkcloud"
      + unique_id       = (known after apply)
    }

  # google_service_account_iam_member.gcloud_mcp_workload_identity_user will be created
  + resource "google_service_account_iam_member" "gcloud_mcp_workload_identity_user" {
      + etag               = (known after apply)
      + id                 = (known after apply)
      + member             = (known after apply)
      + role               = "roles/iam.workloadIdentityUser"
      + service_account_id = (known after apply)
    }

  # google_service_account_iam_member.opencode_mcp_workload_identity_user will be created
  + resource "google_service_account_iam_member" "opencode_mcp_workload_identity_user" {
      + etag               = (known after apply)
      + id                 = (known after apply)
      + member             = (known after apply)
      + role               = "roles/iam.workloadIdentityUser"
      + service_account_id = (known after apply)
    }

Plan: 19 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + gcloud_mcp_service_account_email        = "gcloud-mcp@makeitworkcloud.iam.gserviceaccount.com"
  + gcloud_mcp_workload_identity_provider   = (known after apply)
  + opencode_mcp_service_account_email      = "opencode-mcp@makeitworkcloud.iam.gserviceaccount.com"
  + opencode_mcp_workload_identity_provider = (known after apply)

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so OpenTofu can't
guarantee to take exactly these actions if you run "tofu apply" now.

@xnoto
xnoto marked this pull request as ready for review September 9, 2026 06:46
@xnoto
xnoto merged commit c307b46 into main Sep 9, 2026
6 checks passed
@xnoto
xnoto deleted the feat/gcloud-mcp-identity branch September 9, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant