Repository navigation
sec(ci): refuse authoritative GCP IAM policies in the secret scope guard - #1207
Conversation
check-gcp-secret-scope.sh finds Secret Manager roles by their text, but an
authoritative google_{project,folder,organization}_iam_policy can take its
policy_data from file(), a variable with no in-tree default or a module
output, so the role it grants never appears in a scanned file and the guard
reported such configurations clean. Fail closed instead: any non-comment
mention of those types, or of data "google_iam_policy", now exits 2, as the
guard already does for the JSON encoding.
CI's terraform fmt check now covers iac/ as well as terraform/, so every
root the guard scans is held to the layout its block tracking was written
for. The self-test reads SCAN_ROOTS and fails if a root has no fmt step.
Closes LeanerCloud#150
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review 2 (claude-fleet, cold): APPROVE e9e1c62 Findings (none blocking):
Against #150: the PR implements the issue's simplified remedy, and all three acceptance criteria hold.
Blast radius:
Drift: main has no changes to the guard, its tests, testdata, ci.yml or iac/ since the base. Attacked:
Verified locally:
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
VERDICT: APPROVE e9e1c62 Reviewed head e9e1c62 against origin/main 6776e27. Findings (none blocking):
Coverage: the issue's acceptance criteria are met. Attacked:
|
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rA2. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on |
LeanerCloud#1207, which this branch was stacked on, merged as squash cc132fb. Its content for scripts/check-gcp-secret-scope.sh and scripts/test-gcp-secret-scope.sh is byte-identical to this branch's copy of LeanerCloud#1207 (e9e1c62), so the conflicts are only this branch's own additions on top: kept this branch's side for both files. The diff against main is now exactly this PR's own change. Self-test 60/0; the guard passes on the tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
scripts/check-gcp-secret-scope.sh(as rewritten for #149 in #995) treats everysecretmanagermention outside a per-secret IAM resource as a finding. That works because a Secret Manager role normally has to appear as text somewhere in the scanned tree. The authoritative-policy shape breaks that assumption.google_{project,folder,organization}_iam_policytakespolicy_datathat can come fromfile("policy.json")(a file the guard doesn't scan), from a variable with no in-tree default, or from adata "google_iam_policy"exported through a module output. In those cases nosecretmanagertext reaches the guard, and it reported the configuration clean (exit 0). That shape is also the most dangerous one, because it replaces the scope's whole IAM policy.Part 2 of the issue (the unindented-body bypass) no longer reproduces on main. Since #995, block tracking only narrows the per-secret exemption and never decides whether a role is reported, so a body that starts in column 0 can only cause over-reporting. The issue's suggested fmt gate still had a gap: CI ran
terraform fmt -check -recursiveoverterraform/but not overiac/, which the guard also scans.Fix
The fix follows the issue's simplified remedy: fail closed instead of correlating
policy_data.google_project_iam_policy,google_folder_iam_policy,google_organization_iam_policyorgoogle_iam_policy, matched as a whole token. That covers quoted or bare block labels, any indentation, and references such asdata.google_iam_policy.x.policy_data,movedandimport. This mirrors the existing.tf.jsonhandling. No waiver applies, and the message points to this issue and to the_iam_memberalternatives. Lookalike types (google_secret_manager_secret_iam_policy,google_storage_bucket_iam_policy) and comments are not affected.Terraform Format Checknow also runsterraform fmt -check -recursive iac/.iac/is already formatted:terraform fmt -check -recursive iac/exits 0 locally on Terraform 1.15.8; CI pins 1.10.5. The self-test readsSCAN_ROOTSout of the guard and fails ifci.ymlhas no matchingterraform fmt -check -recursive <root>/line, so a root added later can't skip the gate.Exit 2line andLIMITATIONSnow say the authoritative-policy shape is rejected, not analyzed.LIMITATIONSalso notes the fmt gate and that modules sourced from outside the tree aren't scanned.Three existing fixtures (
iam-policy-data,policy-heredoc,dynamic-iam-policy) containgoogle_project_iam_policyand now expect exit 2 instead of 1. That is intended: both codes fail CI, and 2 is the more accurate answer.policy-heredocwas the only test of heredoc-body scanning, so to keep that covered the self-test also runs a copy retyped togoogle_secret_manager_secret_iam_policyand asserts that the role in the JSON heredoc is still reported (exit 1).Verification
All of this is fixture and self-test evidence from a local run on macOS (BWK awk). CI runs mawk on Linux, so I also ran it in an
ubuntu:24.04container (mawk): the self-test gives 53 passed, 0 failed, and the guard exits 0 on the real repo. No Go code changed, so Go build, vet, golangci-lint and gocyclo don't apply. Nothing here touches real cloud APIs.New fixtures run against main's guard and against this branch (
bash <guard> <fixture>):project-iam-policy-file(policy_data = file(...))folder-iam-policy(policy_data from a var with no default)org-iam-policy(jsonencodewith a role var)iam-policy-data-output(data source exported as an output)iam-policy-unquoted-labels(resource google_project_iam_policy project {)iam-policy-unindented(column-0 body)unindented-body(scope-wide grant, column-0 nested}before the role)per-secret-unindented-body(column-0 per-secret body followed by a project grant)iam-policy-lookalikes(negative direction)Full self-test, branch version, run against main's guard and main's
ci.yml:Results: 42 passed, 11 failed. The 10 authoritative-policy cases fail, plusCI has no 'terraform fmt -check -recursive iac/' step for scan root iac/.The same self-test on the branch:
bash scripts/test-gcp-secret-scope.shgivesResults: 53 passed, 0 failed.Real repo, no new false positives:
bash scripts/check-gcp-secret-scope.shgivesOK: ... in 227 Terraform file(s)(exit 0). The tree has nogoogle_*_iam_policyorgoogle_iam_policy.Mutation probes. Each one was applied to the branch, the self-test was run, and the file was restored. Every probe was caught:
folderfrom the type regex failsgoogle_folder_iam_policy fed by a variable exits 2google_iam_policymatch failsgoogle_iam_policy data source exported as an output exits 2lookalike IAM policy types and comments exit 0iac/fmt line fromci.ymlfails the fmt-coverage checkOther checks:
shellcheck scripts/check-gcp-secret-scope.sh scripts/test-gcp-secret-scope.sh: clean. OneSC2016is disabled inline because${REPO_ROOT}is matched literally there.actionlint .github/workflows/ci.yml: clean.terraform fmt -check -recursive terraform/andterraform fmt -check -recursive iac/: both exit 0.setup-git-secrets.shpatterns plus.gitallowed), run withgit secrets --scan --cachedoverscripts/and.github/in a freshgit initrepo with an isolated HOME: clean.bash scripts/test-git-secrets-allowlist.sh:42 PASS, 0 FAIL.git diff --check: clean.Remaining blind spots (regex-level guard, not an HCL parser)
The issue explicitly rejected a fully correct fix, which would mean parsing HCL and following values from where they are written to the resource that grants them. This PR is the regex-level hardening the issue asks for. These gaps remain:
-var, tfvars kept elsewhere, environment variables, external callers ofiac/modules) were already listed under LIMITATIONS and are unchanged.terraform-google-modules/iamin authoritative mode, are not scanned. This is now listed under LIMITATIONS./* ... */block comments are treated as code by both passes. That only causes over-reporting.data "google_project_iam_policy"and on anygoogle_iam_policydata source, including one that feeds a per-secret_iam_policy. That is deliberate fail-closed over-reporting: the guard can't tell wherepolicy_dataflows. None of these exist in the tree.ephemeral(Terraform 1.10+) is not in the scanner's list of construct openers. It can't grant IAM, so I left it alone and am noting it here.Out of scope
taint()in the main awk scan, at theallowed && s ~ /^...role...="roles\/secretmanager\.[A-Za-z]+"/exemption: narrow[A-Za-z]+to the expected roles, for example(secretAccessor|secretVersionAdder). This PR doesn't touch that code; the new pass is separate and runs before it. One input for ci(gcp): secret scope guard does not check the role on secret-level Secret Manager grants #505's open question about_policyresources: after this PR, a per-secret_iam_policyfed bydata "google_iam_policy"already exits 2.terraform-fmt-checkandterraform-fmttargets still cover onlyterraform/. The issue asked only for the CI gate.Frontend follow-up for the UI owner
None. This PR has no frontend impact.
Labels: could a maintainer please mirror the issue's triage labels onto this PR:
impact/internal,triaged,priority/p3,severity/low,urgency/eventually,effort/m,type/security.Closes #150
🤖 Generated with Claude Code