feat(identity): add keyless gcloud MCP workload identity - #6
Merged
Merged
Conversation
Contributor
OpenTofu TestOpenTofu test passed. View run output |
Contributor
OpenTofu PlanOpenTofu plan passed. View run outputOpenTofu 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds desired state for a separate cluster-internal
gcloud-mcpidentity:cloudasset.googleapis.com;gcloud-mcpGoogle service account;system:serviceaccount:mcp:gcloud-mcp; andThe local
make applypath remains interactive. GitHub Actions adds unattended approval only when the reusable shared workflow has assigned the configured Environment and explicitly emitsOPENTOFU_ENVIRONMENT_GATED=true.Fixes #
None — owner-approved keyless local gcloud MCP preparation.
Type of change
Validation
opentofu / testandopentofu / planpassed at7919e87. This draft must be revalidated after the shared-workflows gate merges because this run resolvedshared-workflows@mainat pre-gate SHA25e454a.github-actions[bot]regenerated the Terraform docs block, and its replacement confirmation run passed.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'sproductionGitHub 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
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.