Repository navigation
fix(gcp): narrow runtime SA recommender access to a single-permission custom role - #1035
Conversation
… custom role The Cloud Run runtime SA held the predefined roles/recommender.viewer, which grants get/list on every recommender and insight type in the project, while the GKE workload SA (which runs the same scheduled recommendation collector) had no Recommender grant at all. The pinned cloud-commitments-go GCP provider only calls ListRecommendations on projects/<id>/locations/<region>/recommenders/google.compute.commitment.UsageCommitmentRecommender for commitment recommendations. Replace the broad grant in both modules with a project custom role cudlyRecommendationReader containing only recommender.usageCommitmentRecommendations.list, the same contract the CLI setup wizard provisions (cloud-commitments-cli#2124). Creating the role relies on roles/iam.roleAdmin, which ci-cd-permissions already grants to the deploy SA; there is no fallback to the predefined role. Drop the stale recommender_viewer import from gcp-import-dev-state.sh and add a static test asserting both runtime SAs get exactly that one permission, project-scoped, and no predefined roles/recommender.* role. Closes LeanerCloud#788 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 4 billable files and costs up to $1.00. Or wait 38 minutes for your next included review. View limit details
Comment |
Independent review verdict: CLEAN at 9ac4f37, with one owner decisionReviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief, acting as a security reviewer. Read-only, with no credentials and no plan or apply. The PR body was treated as a claim. Coordinator: claude-cc-reviewer. Blocking findings: none. Recommender calls vs the new single permission (pinned cloud-commitments-go
Binding:
Import script: it only drops the obsolete Deploy (merge held for an explicit owner OK):
Not verified live: that What was attacked and how:
Gate:
|
|
Merge driver status (claude-cc-pr-merge, 2026-10-17T15:25:27Z): code gate met, merge BLOCKED on an owner decision from @cristim: the Cloud SQL Performance recommender call starts returning 403 under the narrowed role (add the permission or drop the call), plus the same dev-deploy OK. |
|
Refreshing CI (claude-cc-pr-merge-2): main changed scripts/ after this PR's CI ran, so CI must re-run on the current merge ref before merging. Reopening now. |
|
Gate record for 9ac4f37 (claude-cc-pr-merge-2):
|
Root cause
terraform/modules/compute/gcp/cloud-run/main.tfgranted the Cloud Run runtime SA the predefinedroles/recommender.viewer. That role carries get/list on every recommender and insight type in the project. The runtime needs one of them.terraform/modules/compute/gcp/gke/main.tfhad no Recommender grant, even though the GKE deployment runs the same collector (google_cloud_scheduler_job.recommendations→/api/scheduled/collect_recommendations). A GCP account inapplication_defaultauth mode (internal/credentials/resolver.goResolveGCPTokenSourceWithOpts) collects with the runtime SA, which on GKE is the workload SA through Workload Identity.Fix
google_project_iam_member.recommender_viewer. Addedgoogle_project_iam_custom_role.cudly_recommendation_reader(role_id = "cudlyRecommendationReader", permissions = exactlyrecommender.usageCommitmentRecommendations.list, GA) andgoogle_project_iam_member.recommendation_reader, which binds it togoogle_service_account.cloud_runat project scope.google_service_account.workload.var.compute_platformselects eithercloud-runorgke, never both, so the two modules can't both create the role in one environment. The role ID and permission match the CLI setup wizard'scudlyRecommendationReader(cloud-commitments-cli#2124 / #2158). This follows the existing practice of keeping identicalcudlyCommitmentWriterdefinitions so the modules converge.scripts/gcp-import-dev-state.sh: removed the import of the deletedrecommender_vieweraddress. Importing it now would fail.terraform/modules/compute/gcp/runtime_recommender_iam_test.go. It checks that both modules have no predefinedroles/recommender.*on any code line, that each definescudlyRecommendationReaderonce with exactly["recommender.usageCommitmentRecommendations.list"], and that each has exactly onegoogle_project_iam_memberbinding it to that module's runtime SA withproject = var.project_id.Recommender API calls made by the runtime → permission
Source: pinned
github.com/LeanerCloud/cloud-commitments-go/providers/gcp@v0.0.0-20261015143912-740dfbd72456. Every Recommender call isListRecommendations. Nothing in the module callsGetRecommendation,ListInsights/GetInsightorMark*.services/computeengine/client.go:437-444ListRecommendationsprojects/P/locations/R/recommenders/google.compute.commitment.UsageCommitmentRecommenderrecommender.usageCommitmentRecommendations.listservices/cloudsql/client.go:194-202ListRecommendations.../recommenders/google.cloudsql.instance.PerformanceRecommenderrecommender.cloudsqlInstancePerformanceRecommendations.listservices/memorystore/client.go:189-197ListRecommendations.../recommenders/google.memorystore.redis.PerformanceRecommenderservices/cloudstoragerecommendations.gocollectRegionhandles per-(region, service) errors by logging a WARN without failing the region. The all-attempted-failed guard only trips when every call fails, and the Compute call succeeds.Behavior change to review: the runtime SA can no longer list Cloud SQL Performance recommendations. That recommender gives performance advice (open tables, transaction ID use), not commitment recommendations. CLI#2158 made the same call and leaves it unprovisioned. Fixing the Cloud SQL and Memorystore recommender IDs is tracked as a cloud-commitments-go follow-up. Until then, expect one extra per-region WARN for Cloud SQL where
roles/recommender.viewerused to let that call succeed. If you'd rather keep Cloud SQL perf recs flowing for now, the alternative is addingrecommender.cloudsqlInstancePerformanceRecommendations.listto this role. I haven't done that because it would cement the wrong recommender ID.Bootstrap requirement
Creating
google_project_iam_custom_roleneedsiam.roles.{get,create,update,delete}on the deployer SA.terraform/environments/gcp/ci-cd-permissions/service_account.tfalready grantsroles/iam.roleAdminforcudlyCommitmentWriter, so the bootstrap doesn't change. If an environment's deploy SA was bootstrapped without it, the apply 403s oniam.roles.get/create. That's the intended signal to re-apply ci-cd-permissions, and there's deliberately no fallback toroles/recommender.viewer. If someone already ran the CLI wizard against the host project,cudlyRecommendationReaderexists and create returns 409. Import it withterraform import 'module.compute_cloud_run[0].google_project_iam_custom_role.cudly_recommendation_reader' projects/P/roles/cudlyRecommendationReader. The definitions are identical.Verification (static only, nothing applied)
origin/main: Cloud Run reports the predefined role and the missing custom role, and GKE reports the missing custom role. It passes after the fix....getpermission, the GKE binding reverted toroles/recommender.viewer, a binding to the wrong SA (scheduler[0]), a binding with a non-var.project_idproject, and a renamed role ID.go test ./terraform/... ./iac/... ./internal/iacfiles/...passes (withAWS_CONFIG_FILE=/dev/null). golangci-lint v2.10.1 on the package reports 0 issues.terraform fmt -check -recursive terraform/is clean.terraform init -backend=false+validatepass onmodules/compute/gcp/cloud-run,modules/compute/gcp/gkeandenvironments/gcp, using a temp data dir that was deleted afterwards. Lockfiles are unchanged.scripts/check-gcp-secret-scope.shpasses.scripts/test-gcp-secret-scope.shgives 40/40.--scanof the changed files in an isolated throwaway repo is clean.scripts/test-git-secrets-allowlist.shgives 42/42.git merge-treeagainst the sibling fix(gcp): exclude uninitialized replicas from GKE and Cloud Run readiness #1024 branch (fix/issue-648, probe edits in the same modules) is clean.recommender.usageCommitmentRecommendations.list(Google's IAM docs list it under project-level CUD viewer roles), or that an SA with only this role plusroles/compute.viewergets recommendations back. Operators: make sure the deploy SA holdsroles/iam.roleAdmin(re-applyci-cd-permissionsif not), then re-apply the GCP environment.Out of scope
roles/compute.viewerorcudlyCommitmentWriter, unlike Cloud Run. Soapplication_defaultcollection on GKE still stops at thecompute.regions.list403 ("skipping recommendations") before it reaches the Recommender call. Achieving parity there is a separate change.roles/recommender.viewer:iac/federation/gcp-target,iac/federation/gcp-sa-impersonation,internal/iacfiles/templates/gcp-sa-impersonation-cli.sh.tmpl.Closes #788
🤖 Generated with Claude Code