[{"data":1,"prerenderedAt":1491},["ShallowReactive",2],{"post:\u002F2026\u002F05\u002F13\u002Fgoogle-cloud-organization-policy-constraints-that-actually-matter\u002F":3},{"post":4,"newer":1449,"older":1462,"related":1471,"series":1489},{"id":5,"title":6,"body":7,"canonical":1433,"categories":1434,"date":1436,"description":1437,"extension":1438,"featured":1439,"hero":1440,"image":1440,"meta":1441,"navigation":169,"path":1442,"readingTime":394,"seo":1443,"series":1440,"seriesOrder":1440,"sites":1444,"source":1440,"stem":1445,"tags":1446,"updated":1440,"url":1447,"__hash__":1448},"blog\u002Fblog\u002F2026\u002F05\u002F13\u002Fgoogle-cloud-organization-policy-constraints-that-actually-matter.md","Google Cloud: Organization Policy Constraints That Actually Matter",{"type":8,"value":9,"toc":1424},"minimark",[10,14,34,39,42,132,135,195,199,212,267,289,306,328,332,341,401,404,426,430,433,443,484,504,515,523,529,606,612,694,713,716,720,723,748,759,777,1295,1298,1304,1323,1327,1341,1345,1420],[11,12,13],"p",{},"Google Cloud ships a long catalog of Organization Policy constraints, and the temptation the first time you're handed org-level admin is to enable a defensible-looking chunk of them and call it a security baseline. I did that once, broke three teams' CI pipelines in the same afternoon, and spent the following weeks learning which constraints actually stop a bad day and which ones just make an audit report look thorough.",[11,15,16,17,21,22,26,27,26,30,33],{},"Two things have changed since then that shape the advice below. First, Google now enforces a small security baseline on every organization created on or after May 3, 2024. Second, most of the constraints worth caring about now have ",[18,19,20],"strong",{},"managed"," versions (",[23,24,25],"code",{},"iam.managed.*",", ",[23,28,29],{},"compute.managed.*",[23,31,32],{},"sql.managed.*","), and those support dry-run mode and tag-based exemptions. The legacy versions mostly don't.",[35,36,38],"h2",{"id":37},"know-what-you-already-have","Know what you already have",[11,40,41],{},"If your organization is new enough, the baseline is already on. Google's secure-by-default page lists exactly seven constraints enforced on organizations created on or after May 3, 2024 (and possibly on some created between February and April 2024):",[43,44,45,58],"table",{},[46,47,48],"thead",{},[49,50,51,55],"tr",{},[52,53,54],"th",{},"Constraint",[52,56,57],{},"Effect",[59,60,61,72,82,92,102,112,122],"tbody",{},[49,62,63,69],{},[64,65,66],"td",{},[23,67,68],{},"iam.managed.disableServiceAccountKeyCreation",[64,70,71],{},"No new service account keys",[49,73,74,79],{},[64,75,76],{},[23,77,78],{},"iam.disableServiceAccountKeyUpload",[64,80,81],{},"No uploading external public keys to service accounts",[49,83,84,89],{},[64,85,86],{},[23,87,88],{},"iam.automaticIamGrantsForDefaultServiceAccounts",[64,90,91],{},"Default service accounts don't get Editor automatically",[49,93,94,99],{},[64,95,96],{},[23,97,98],{},"iam.allowedPolicyMemberDomains",[64,100,101],{},"Only identities from your own organization can be granted roles",[49,103,104,109],{},[64,105,106],{},[23,107,108],{},"essentialcontacts.managed.allowedContactDomains",[64,110,111],{},"Essential Contacts limited to your domains",[49,113,114,119],{},[64,115,116],{},[23,117,118],{},"compute.managed.restrictProtocolForwardingCreationForTypes",[64,120,121],{},"Protocol forwarding restricted to internal IPs",[49,123,124,129],{},[64,125,126],{},[23,127,128],{},"storage.uniformBucketLevelAccess",[64,130,131],{},"Buckets can't use per-object ACLs",[11,133,134],{},"Older organizations have none of these unless someone set them. Either way, check the effective state before adding anything:",[136,137,142],"pre",{"className":138,"code":139,"language":140,"meta":141,"style":141},"language-bash shiki shiki-themes github-dark","gcloud org-policies list --organization=123456789012\n\ngcloud org-policies describe iam.managed.disableServiceAccountKeyCreation \\\n  --effective --organization=123456789012\n","bash","",[23,143,144,164,171,187],{"__ignoreMap":141},[145,146,149,153,157,160],"span",{"class":147,"line":148},"line",1,[145,150,152],{"class":151},"svObZ","gcloud",[145,154,156],{"class":155},"sU2Wk"," org-policies",[145,158,159],{"class":155}," list",[145,161,163],{"class":162},"sDLfK"," --organization=123456789012\n",[145,165,167],{"class":147,"line":166},2,[145,168,170],{"emptyLinePlaceholder":169},true,"\n",[145,172,174,176,178,181,184],{"class":147,"line":173},3,[145,175,152],{"class":151},[145,177,156],{"class":155},[145,179,180],{"class":155}," describe",[145,182,183],{"class":155}," iam.managed.disableServiceAccountKeyCreation",[145,185,186],{"class":162}," \\\n",[145,188,190,193],{"class":147,"line":189},4,[145,191,192],{"class":162},"  --effective",[145,194,163],{"class":162},[35,196,198],{"id":197},"the-handful-that-earn-their-keep","The handful that earn their keep",[11,200,201,204,205,207,208,211],{},[18,202,203],{},"Service account key creation."," ",[23,206,68],{}," stops the most common way a service account credential ends up committed to a repository or pasted into a chat, because there's no long-lived key to leak in the first place. Workloads use an attached service account or Workload Identity Federation instead. If you're on an older org still using the legacy ",[23,209,210],{},"iam.disableServiceAccountKeyCreation",", move to the managed one: Google recommends the managed equivalents for more flexible policies and better insight from Policy Intelligence tools.",[136,213,217],{"className":214,"code":215,"language":216,"meta":141,"style":141},"language-yaml shiki shiki-themes github-dark","# disable-sa-keys.yaml\nname: organizations\u002F123456789012\u002Fpolicies\u002Fiam.managed.disableServiceAccountKeyCreation\nspec:\n  rules:\n  - enforce: true\n","yaml",[23,218,219,225,238,246,253],{"__ignoreMap":141},[145,220,221],{"class":147,"line":148},[145,222,224],{"class":223},"sAwPA","# disable-sa-keys.yaml\n",[145,226,227,231,235],{"class":147,"line":166},[145,228,230],{"class":229},"s4JwU","name",[145,232,234],{"class":233},"s95oV",": ",[145,236,237],{"class":155},"organizations\u002F123456789012\u002Fpolicies\u002Fiam.managed.disableServiceAccountKeyCreation\n",[145,239,240,243],{"class":147,"line":173},[145,241,242],{"class":229},"spec",[145,244,245],{"class":233},":\n",[145,247,248,251],{"class":147,"line":189},[145,249,250],{"class":229},"  rules",[145,252,245],{"class":233},[145,254,256,259,262,264],{"class":147,"line":255},5,[145,257,258],{"class":233},"  - ",[145,260,261],{"class":229},"enforce",[145,263,234],{"class":233},[145,265,266],{"class":162},"true\n",[11,268,269,272,273,276,277,280,281,284,285,288],{},[18,270,271],{},"External IPs on VMs."," Someone forgets ",[23,274,275],{},"--no-address"," on a one-off VM, and the constraint refuses the create instead of leaving a box exposed until the next audit. The managed ",[23,278,279],{},"compute.managed.vmExternalIpAccess"," is a boolean: when enforced, it denies creating or updating VMs with IPv4 external addresses (it doesn't touch IPv6). Exceptions such as a bastion host are handled with tags rather than an instance list. The legacy ",[23,282,283],{},"compute.vmExternalIpAccess"," is a list constraint that names each allowed instance as ",[23,286,287],{},"projects\u002FPROJECT_ID\u002Fzones\u002FZONE\u002Finstances\u002FINSTANCE",", which turns every new exception into a policy edit.",[11,290,291,204,294,297,298,301,302,305],{},[18,292,293],{},"Public IP on Cloud SQL.",[23,295,296],{},"sql.managed.restrictPublicIp"," (legacy: ",[23,299,300],{},"sql.restrictPublicIp",") closes the same gap for databases, where a public IP is a much bigger blast radius than a stray VM. Both versions are explicitly not retroactive: instances that already have a public IP keep working, so pair the constraint with a one-time sweep. One side effect the Cloud SQL docs call out: ",[23,303,304],{},"gcloud sql connect"," stops working, because it connects over the public IP. Teams need the Cloud SQL Auth Proxy over private IP instead.",[11,307,308,204,311,314,315,319,320,323,324,327],{},[18,309,310],{},"Public buckets.",[23,312,313],{},"storage.publicAccessPrevention"," is the one on this list that ",[316,317,318],"em",{},"is"," retroactive. It blocks ACLs and IAM grants to ",[23,321,322],{},"allUsers"," and ",[23,325,326],{},"allAuthenticatedUsers",", and the constraint description says public access is revoked for existing buckets and objects once it's enabled. That's exactly what you want, but it's also why this one absolutely goes through the rollout below. Somebody's static site or public download bucket will stop working.",[35,329,331],{"id":330},"the-ones-that-look-good-and-do-less","The ones that look good and do less",[11,333,334,336,337,340],{},[23,335,98],{}," gets enabled everywhere because it reads well in a compliance deck. It stops identities outside your organization's Workspace or Cloud Identity account from being granted roles. It's worth having, and new organizations get it by default, but in my experience it mostly fires on typo'd email addresses during role grants. It's further down the list than its audit-friendly name suggests. If you do set it, use the managed ",[23,338,339],{},"iam.managed.allowedPolicyMembers",", which takes your organization's principal set as a parameter:",[136,342,344],{"className":214,"code":343,"language":216,"meta":141,"style":141},"name: organizations\u002F123456789012\u002Fpolicies\u002Fiam.managed.allowedPolicyMembers\nspec:\n  rules:\n  - enforce: true\n    parameters:\n      allowedPrincipalSets:\n      - \u002F\u002Fcloudresourcemanager.googleapis.com\u002Forganizations\u002F123456789012\n",[23,345,346,355,361,367,377,384,392],{"__ignoreMap":141},[145,347,348,350,352],{"class":147,"line":148},[145,349,230],{"class":229},[145,351,234],{"class":233},[145,353,354],{"class":155},"organizations\u002F123456789012\u002Fpolicies\u002Fiam.managed.allowedPolicyMembers\n",[145,356,357,359],{"class":147,"line":166},[145,358,242],{"class":229},[145,360,245],{"class":233},[145,362,363,365],{"class":147,"line":173},[145,364,250],{"class":229},[145,366,245],{"class":233},[145,368,369,371,373,375],{"class":147,"line":189},[145,370,258],{"class":233},[145,372,261],{"class":229},[145,374,234],{"class":233},[145,376,266],{"class":162},[145,378,379,382],{"class":147,"line":255},[145,380,381],{"class":229},"    parameters",[145,383,245],{"class":233},[145,385,387,390],{"class":147,"line":386},6,[145,388,389],{"class":229},"      allowedPrincipalSets",[145,391,245],{"class":233},[145,393,395,398],{"class":147,"line":394},7,[145,396,397],{"class":233},"      - ",[145,399,400],{"class":155},"\u002F\u002Fcloudresourcemanager.googleapis.com\u002Forganizations\u002F123456789012\n",[11,402,403],{},"Read the fine print before enforcing it. The constraint description warns that it can block folder creation (because of automatic Folder Admin and Folder Editor grants) and project creation (because of the automatic Owner grant) if those principals aren't covered.",[11,405,406,407,410,411,26,414,417,418,421,422,425],{},"API restriction is the other one that sounds stronger than it is. People reach for ",[23,408,409],{},"serviceuser.services",", but it only works as a deny list, and it can only restrict three services: ",[23,412,413],{},"compute.googleapis.com",[23,415,416],{},"deploymentmanager.googleapis.com",", and ",[23,419,420],{},"dns.googleapis.com",". The real allowlist is ",[23,423,424],{},"gcp.restrictServiceUsage"," (Restrict Resource Service Usage). It works, and it supports dry run, but in a fast-moving engineering org it mostly generates tickets from teams that needed one more API for a legitimate reason. It doesn't meaningfully shrink the attack surface if IAM is already scoped correctly. I use it on regulated folders and nowhere else.",[35,427,429],{"id":428},"the-rollout-order-that-avoids-breaking-things","The rollout order that avoids breaking things",[11,431,432],{},"The lesson from that first afternoon: never flip a constraint org-wide as your first move. Every constraint I've kept in production went through the same three steps.",[11,434,435,438,439,442],{},[18,436,437],{},"1. Dry run."," Put the policy in ",[23,440,441],{},"dryRunSpec"," and watch the violations against real traffic for at least a week. Dry run only works with custom constraints, managed constraints, and a few legacy ones (restrict service usage, restrict endpoint usage, TLS versions, TLS cipher suites). Anything else returns an error, which is another reason to use the managed versions.",[136,444,446],{"className":214,"code":445,"language":216,"meta":141,"style":141},"# vm-external-ip.dryrun.yaml\nname: organizations\u002F123456789012\u002Fpolicies\u002Fcompute.managed.vmExternalIpAccess\ndryRunSpec:\n  rules:\n  - enforce: true\n",[23,447,448,453,462,468,474],{"__ignoreMap":141},[145,449,450],{"class":147,"line":148},[145,451,452],{"class":223},"# vm-external-ip.dryrun.yaml\n",[145,454,455,457,459],{"class":147,"line":166},[145,456,230],{"class":229},[145,458,234],{"class":233},[145,460,461],{"class":155},"organizations\u002F123456789012\u002Fpolicies\u002Fcompute.managed.vmExternalIpAccess\n",[145,463,464,466],{"class":147,"line":173},[145,465,441],{"class":229},[145,467,245],{"class":233},[145,469,470,472],{"class":147,"line":189},[145,471,250],{"class":229},[145,473,245],{"class":233},[145,475,476,478,480,482],{"class":147,"line":255},[145,477,258],{"class":233},[145,479,261],{"class":229},[145,481,234],{"class":233},[145,483,266],{"class":162},[136,485,487],{"className":138,"code":486,"language":140,"meta":141,"style":141},"gcloud org-policies set-policy vm-external-ip.dryrun.yaml --update-mask=dryRunSpec\n",[23,488,489],{"__ignoreMap":141},[145,490,491,493,495,498,501],{"class":147,"line":148},[145,492,152],{"class":151},[145,494,156],{"class":155},[145,496,497],{"class":155}," set-policy",[145,499,500],{"class":155}," vm-external-ip.dryrun.yaml",[145,502,503],{"class":162}," --update-mask=dryRunSpec\n",[11,505,506,507,510,511,514],{},"Dry-run results land in the policy audit log (",[23,508,509],{},"cloudaudit.googleapis.com%2Fpolicy","). This Logs Explorer query shows only the requests the policy ",[316,512,513],{},"would"," have denied:",[136,516,521],{"className":517,"code":519,"language":520,"meta":141},[518],"language-text","logName:\"cloudaudit.googleapis.com%2Fpolicy\"\nprotoPayload.metadata.dryRunResult=\"DENIED\"\nprotoPayload.metadata.liveResult=\"ALLOWED\"\n","text",[23,522,519],{"__ignoreMap":141},[11,524,525,528],{},[18,526,527],{},"2. Tag the legitimate exceptions."," Bastion hosts, NAT instances, and a handful of demo environments all have real reasons for an external IP. Skipping this step guarantees a break. With a managed boolean constraint, the exception is a tag plus a conditional rule. Create the tag and bind it to the projects that are allowed. A project binding exempts every VM in that project, so for a lone bastion in a shared project, bind the tag to the instance instead:",[136,530,532],{"className":138,"code":531,"language":140,"meta":141,"style":141},"gcloud resource-manager tags keys create external-ip --parent=organizations\u002F123456789012\ngcloud resource-manager tags values create allowed --parent=123456789012\u002Fexternal-ip\n\ngcloud resource-manager tags bindings create \\\n  --tag-value=123456789012\u002Fexternal-ip\u002Fallowed \\\n  --parent=\u002F\u002Fcloudresourcemanager.googleapis.com\u002Fprojects\u002Fnetwork-edge-prod\n",[23,533,534,556,575,579,594,601],{"__ignoreMap":141},[145,535,536,538,541,544,547,550,553],{"class":147,"line":148},[145,537,152],{"class":151},[145,539,540],{"class":155}," resource-manager",[145,542,543],{"class":155}," tags",[145,545,546],{"class":155}," keys",[145,548,549],{"class":155}," create",[145,551,552],{"class":155}," external-ip",[145,554,555],{"class":162}," --parent=organizations\u002F123456789012\n",[145,557,558,560,562,564,567,569,572],{"class":147,"line":166},[145,559,152],{"class":151},[145,561,540],{"class":155},[145,563,543],{"class":155},[145,565,566],{"class":155}," values",[145,568,549],{"class":155},[145,570,571],{"class":155}," allowed",[145,573,574],{"class":162}," --parent=123456789012\u002Fexternal-ip\n",[145,576,577],{"class":147,"line":173},[145,578,170],{"emptyLinePlaceholder":169},[145,580,581,583,585,587,590,592],{"class":147,"line":189},[145,582,152],{"class":151},[145,584,540],{"class":155},[145,586,543],{"class":155},[145,588,589],{"class":155}," bindings",[145,591,549],{"class":155},[145,593,186],{"class":162},[145,595,596,599],{"class":147,"line":255},[145,597,598],{"class":162},"  --tag-value=123456789012\u002Fexternal-ip\u002Fallowed",[145,600,186],{"class":162},[145,602,603],{"class":147,"line":386},[145,604,605],{"class":162},"  --parent=\u002F\u002Fcloudresourcemanager.googleapis.com\u002Fprojects\u002Fnetwork-edge-prod\n",[11,607,608,611],{},[18,609,610],{},"3. Enforce folder by folder."," Start with a low-stakes folder (a sandbox or dev folder, never the one with the CI service accounts in it). Then move the policy up the hierarchy as the violation log goes quiet. A policy with a conditional rule must also have at least one unconditional rule, and for a boolean constraint the conditional rule has to be the opposite of the default:",[136,613,615],{"className":214,"code":614,"language":216,"meta":141,"style":141},"# vm-external-ip.yaml\nname: folders\u002F456789012345\u002Fpolicies\u002Fcompute.managed.vmExternalIpAccess\nspec:\n  rules:\n  - condition:\n      title: external-ip-allowed\n      expression: resource.matchTag(\"123456789012\u002Fexternal-ip\", \"allowed\")\n    enforce: false\n  - enforce: true\n",[23,616,617,622,631,637,643,652,662,672,683],{"__ignoreMap":141},[145,618,619],{"class":147,"line":148},[145,620,621],{"class":223},"# vm-external-ip.yaml\n",[145,623,624,626,628],{"class":147,"line":166},[145,625,230],{"class":229},[145,627,234],{"class":233},[145,629,630],{"class":155},"folders\u002F456789012345\u002Fpolicies\u002Fcompute.managed.vmExternalIpAccess\n",[145,632,633,635],{"class":147,"line":173},[145,634,242],{"class":229},[145,636,245],{"class":233},[145,638,639,641],{"class":147,"line":189},[145,640,250],{"class":229},[145,642,245],{"class":233},[145,644,645,647,650],{"class":147,"line":255},[145,646,258],{"class":233},[145,648,649],{"class":229},"condition",[145,651,245],{"class":233},[145,653,654,657,659],{"class":147,"line":386},[145,655,656],{"class":229},"      title",[145,658,234],{"class":233},[145,660,661],{"class":155},"external-ip-allowed\n",[145,663,664,667,669],{"class":147,"line":394},[145,665,666],{"class":229},"      expression",[145,668,234],{"class":233},[145,670,671],{"class":155},"resource.matchTag(\"123456789012\u002Fexternal-ip\", \"allowed\")\n",[145,673,675,678,680],{"class":147,"line":674},8,[145,676,677],{"class":229},"    enforce",[145,679,234],{"class":233},[145,681,682],{"class":162},"false\n",[145,684,686,688,690,692],{"class":147,"line":685},9,[145,687,258],{"class":233},[145,689,261],{"class":229},[145,691,234],{"class":233},[145,693,266],{"class":162},[136,695,697],{"className":138,"code":696,"language":140,"meta":141,"style":141},"gcloud org-policies set-policy vm-external-ip.yaml --update-mask=spec\n",[23,698,699],{"__ignoreMap":141},[145,700,701,703,705,707,710],{"class":147,"line":148},[145,702,152],{"class":151},[145,704,156],{"class":155},[145,706,497],{"class":155},[145,708,709],{"class":155}," vm-external-ip.yaml",[145,711,712],{"class":162}," --update-mask=spec\n",[11,714,715],{},"Changes can take up to 15 minutes to be enforced, so don't read a successful VM create in the first few minutes as proof the policy is broken.",[35,717,719],{"id":718},"where-the-hierarchy-actually-bites","Where the hierarchy actually bites",[11,721,722],{},"The failure mode that costs the most debugging time isn't a constraint being wrong. It's a constraint set at the wrong node in the resource hierarchy and inherited somewhere nobody expected. A policy set on the organization applies to every folder and project below it unless something downstream sets its own policy. So when a team reports a violation that \"shouldn't be possible,\" I don't start with the project's own policy. I start with the effective one, because the constraint that's actually biting them is almost always three folders up from where they're looking:",[136,724,726],{"className":138,"code":725,"language":140,"meta":141,"style":141},"gcloud org-policies describe compute.managed.vmExternalIpAccess \\\n  --effective --project=team-a-dev\n",[23,727,728,741],{"__ignoreMap":141},[145,729,730,732,734,736,739],{"class":147,"line":148},[145,731,152],{"class":151},[145,733,156],{"class":155},[145,735,180],{"class":155},[145,737,738],{"class":155}," compute.managed.vmExternalIpAccess",[145,740,186],{"class":162},[145,742,743,745],{"class":147,"line":166},[145,744,192],{"class":162},[145,746,747],{"class":162}," --project=team-a-dev\n",[11,749,750,751,754,755,758],{},"The two cleanup commands are easy to confuse. ",[23,752,753],{},"gcloud org-policies delete CONSTRAINT --project=PROJECT_ID"," removes the project's own policy, so the project goes back to whatever it inherits. ",[23,756,757],{},"gcloud org-policies reset CONSTRAINT --project=PROJECT_ID"," does something different: it resets the policy to the constraint's default, which for most constraints means not enforced, whatever the parent says.",[11,760,761,762,765,766,769,770,773,774,776],{},"To see the whole estate at once, this script prints the effective enforcement of the constraints above for every project the caller can see. It needs ",[23,763,764],{},"roles\u002Forgpolicy.policyViewer"," at the organization plus ",[23,767,768],{},"resourcemanager.projects.list",", for example Browser (",[23,771,772],{},"roles\u002Fbrowser",") on the organization. It uses nothing beyond ",[23,775,152],{}," and Bash:",[136,778,780],{"className":138,"code":779,"language":140,"meta":141,"style":141},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# effective-org-policies.sh\n# Prints the effective enforcement of key boolean constraints for every\n# project visible to the caller, as CSV on stdout.\n# Usage: .\u002Feffective-org-policies.sh [PROJECT_FILTER] > effective.csv\n#   PROJECT_FILTER is an optional gcloud --filter, e.g. \"parent.id=456789012345\"\nset -euo pipefail\n\nERR_FILE=$(mktemp)\ntrap 'rm -f \"$ERR_FILE\"' EXIT\n\nCONSTRAINTS=(\n  iam.managed.disableServiceAccountKeyCreation\n  iam.disableServiceAccountKeyCreation\n  compute.managed.vmExternalIpAccess\n  sql.managed.restrictPublicIp\n  sql.restrictPublicIp\n  storage.publicAccessPrevention\n  storage.uniformBucketLevelAccess\n)\nFILTER=\"${1:-lifecycleState=ACTIVE}\"\n\nprintf 'project,%s\\n' \"$(IFS=,; echo \"${CONSTRAINTS[*]}\")\"\n\nfor project in $(gcloud projects list --filter=\"$FILTER\" --format='value(projectId)'); do\n  row=\"$project\"\n  for constraint in \"${CONSTRAINTS[@]}\"; do\n    # spec.rules may hold conditional rules; list every enforce value\n    if value=$(gcloud org-policies describe \"$constraint\" --effective \\\n        --project=\"$project\" --format='value(spec.rules[].enforce)' 2>\"$ERR_FILE\"); then\n      [[ -z \"$value\" ]] && value=\"unset\"\n    elif grep -q NOT_FOUND \"$ERR_FILE\"; then\n      value=\"unset\"   # no policy set anywhere in the hierarchy\n    else\n      value=\"error\"   # permission denied, API disabled, and so on\n    fi\n    row+=\",${value\u002F\u002F;\u002F|}\"\n  done\n  echo \"$row\"\ndone\n",[23,781,782,787,792,797,802,807,812,823,827,845,857,862,873,879,885,891,897,903,909,915,920,951,956,993,998,1042,1057,1085,1091,1123,1155,1179,1204,1218,1224,1237,1243,1270,1276,1289],{"__ignoreMap":141},[145,783,784],{"class":147,"line":148},[145,785,786],{"class":223},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[145,788,789],{"class":147,"line":166},[145,790,791],{"class":223},"# effective-org-policies.sh\n",[145,793,794],{"class":147,"line":173},[145,795,796],{"class":223},"# Prints the effective enforcement of key boolean constraints for every\n",[145,798,799],{"class":147,"line":189},[145,800,801],{"class":223},"# project visible to the caller, as CSV on stdout.\n",[145,803,804],{"class":147,"line":255},[145,805,806],{"class":223},"# Usage: .\u002Feffective-org-policies.sh [PROJECT_FILTER] > effective.csv\n",[145,808,809],{"class":147,"line":386},[145,810,811],{"class":223},"#   PROJECT_FILTER is an optional gcloud --filter, e.g. \"parent.id=456789012345\"\n",[145,813,814,817,820],{"class":147,"line":394},[145,815,816],{"class":162},"set",[145,818,819],{"class":162}," -euo",[145,821,822],{"class":155}," pipefail\n",[145,824,825],{"class":147,"line":674},[145,826,170],{"emptyLinePlaceholder":169},[145,828,829,832,836,839,842],{"class":147,"line":685},[145,830,831],{"class":233},"ERR_FILE",[145,833,835],{"class":834},"snl16","=",[145,837,838],{"class":233},"$(",[145,840,841],{"class":151},"mktemp",[145,843,844],{"class":233},")\n",[145,846,848,851,854],{"class":147,"line":847},10,[145,849,850],{"class":162},"trap",[145,852,853],{"class":155}," 'rm -f \"$ERR_FILE\"'",[145,855,856],{"class":155}," EXIT\n",[145,858,860],{"class":147,"line":859},11,[145,861,170],{"emptyLinePlaceholder":169},[145,863,865,868,870],{"class":147,"line":864},12,[145,866,867],{"class":233},"CONSTRAINTS",[145,869,835],{"class":834},[145,871,872],{"class":233},"(\n",[145,874,876],{"class":147,"line":875},13,[145,877,878],{"class":155},"  iam.managed.disableServiceAccountKeyCreation\n",[145,880,882],{"class":147,"line":881},14,[145,883,884],{"class":155},"  iam.disableServiceAccountKeyCreation\n",[145,886,888],{"class":147,"line":887},15,[145,889,890],{"class":155},"  compute.managed.vmExternalIpAccess\n",[145,892,894],{"class":147,"line":893},16,[145,895,896],{"class":155},"  sql.managed.restrictPublicIp\n",[145,898,900],{"class":147,"line":899},17,[145,901,902],{"class":155},"  sql.restrictPublicIp\n",[145,904,906],{"class":147,"line":905},18,[145,907,908],{"class":155},"  storage.publicAccessPrevention\n",[145,910,912],{"class":147,"line":911},19,[145,913,914],{"class":155},"  storage.uniformBucketLevelAccess\n",[145,916,918],{"class":147,"line":917},20,[145,919,844],{"class":233},[145,921,923,926,928,931,934,937,940,942,945,948],{"class":147,"line":922},21,[145,924,925],{"class":233},"FILTER",[145,927,835],{"class":834},[145,929,930],{"class":155},"\"",[145,932,933],{"class":162},"${1",[145,935,936],{"class":834},":-",[145,938,939],{"class":233},"lifecycleState",[145,941,835],{"class":155},[145,943,944],{"class":233},"ACTIVE",[145,946,947],{"class":162},"}",[145,949,950],{"class":155},"\"\n",[145,952,954],{"class":147,"line":953},22,[145,955,170],{"emptyLinePlaceholder":169},[145,957,959,962,965,968,971,973,976,979,982,984,987,990],{"class":147,"line":958},23,[145,960,961],{"class":162},"printf",[145,963,964],{"class":155}," 'project,%s\\n'",[145,966,967],{"class":155}," \"$(",[145,969,970],{"class":233},"IFS",[145,972,835],{"class":834},[145,974,975],{"class":155},",; ",[145,977,978],{"class":162},"echo",[145,980,981],{"class":155}," \"${",[145,983,867],{"class":233},[145,985,986],{"class":155},"[",[145,988,989],{"class":834},"*",[145,991,992],{"class":155},"]}\")\"\n",[145,994,996],{"class":147,"line":995},24,[145,997,170],{"emptyLinePlaceholder":169},[145,999,1001,1004,1007,1010,1013,1015,1018,1020,1023,1025,1028,1030,1033,1036,1039],{"class":147,"line":1000},25,[145,1002,1003],{"class":834},"for",[145,1005,1006],{"class":233}," project ",[145,1008,1009],{"class":834},"in",[145,1011,1012],{"class":233}," $(",[145,1014,152],{"class":151},[145,1016,1017],{"class":155}," projects",[145,1019,159],{"class":155},[145,1021,1022],{"class":162}," --filter=",[145,1024,930],{"class":155},[145,1026,1027],{"class":233},"$FILTER",[145,1029,930],{"class":155},[145,1031,1032],{"class":162}," --format=",[145,1034,1035],{"class":155},"'value(projectId)'",[145,1037,1038],{"class":233},"); ",[145,1040,1041],{"class":834},"do\n",[145,1043,1045,1048,1050,1052,1055],{"class":147,"line":1044},26,[145,1046,1047],{"class":233},"  row",[145,1049,835],{"class":834},[145,1051,930],{"class":155},[145,1053,1054],{"class":233},"$project",[145,1056,950],{"class":155},[145,1058,1060,1063,1066,1068,1070,1072,1074,1077,1080,1083],{"class":147,"line":1059},27,[145,1061,1062],{"class":834},"  for",[145,1064,1065],{"class":233}," constraint ",[145,1067,1009],{"class":834},[145,1069,981],{"class":155},[145,1071,867],{"class":233},[145,1073,986],{"class":155},[145,1075,1076],{"class":834},"@",[145,1078,1079],{"class":155},"]}\"",[145,1081,1082],{"class":233},"; ",[145,1084,1041],{"class":834},[145,1086,1088],{"class":147,"line":1087},28,[145,1089,1090],{"class":223},"    # spec.rules may hold conditional rules; list every enforce value\n",[145,1092,1094,1097,1100,1102,1104,1106,1108,1110,1113,1116,1118,1121],{"class":147,"line":1093},29,[145,1095,1096],{"class":834},"    if",[145,1098,1099],{"class":233}," value",[145,1101,835],{"class":834},[145,1103,838],{"class":233},[145,1105,152],{"class":151},[145,1107,156],{"class":155},[145,1109,180],{"class":155},[145,1111,1112],{"class":155}," \"",[145,1114,1115],{"class":233},"$constraint",[145,1117,930],{"class":155},[145,1119,1120],{"class":162}," --effective",[145,1122,186],{"class":162},[145,1124,1126,1129,1131,1133,1135,1137,1140,1143,1145,1148,1150,1152],{"class":147,"line":1125},30,[145,1127,1128],{"class":162},"        --project=",[145,1130,930],{"class":155},[145,1132,1054],{"class":233},[145,1134,930],{"class":155},[145,1136,1032],{"class":162},[145,1138,1139],{"class":155},"'value(spec.rules[].enforce)'",[145,1141,1142],{"class":834}," 2>",[145,1144,930],{"class":155},[145,1146,1147],{"class":233},"$ERR_FILE",[145,1149,930],{"class":155},[145,1151,1038],{"class":233},[145,1153,1154],{"class":834},"then\n",[145,1156,1158,1161,1164,1166,1169,1171,1174,1176],{"class":147,"line":1157},31,[145,1159,1160],{"class":233},"      [[ ",[145,1162,1163],{"class":834},"-z",[145,1165,1112],{"class":155},[145,1167,1168],{"class":233},"$value",[145,1170,930],{"class":155},[145,1172,1173],{"class":233}," ]] && value",[145,1175,835],{"class":834},[145,1177,1178],{"class":155},"\"unset\"\n",[145,1180,1182,1185,1188,1191,1194,1196,1198,1200,1202],{"class":147,"line":1181},32,[145,1183,1184],{"class":834},"    elif",[145,1186,1187],{"class":151}," grep",[145,1189,1190],{"class":162}," -q",[145,1192,1193],{"class":155}," NOT_FOUND",[145,1195,1112],{"class":155},[145,1197,1147],{"class":233},[145,1199,930],{"class":155},[145,1201,1082],{"class":233},[145,1203,1154],{"class":834},[145,1205,1207,1210,1212,1215],{"class":147,"line":1206},33,[145,1208,1209],{"class":233},"      value",[145,1211,835],{"class":834},[145,1213,1214],{"class":155},"\"unset\"",[145,1216,1217],{"class":223},"   # no policy set anywhere in the hierarchy\n",[145,1219,1221],{"class":147,"line":1220},34,[145,1222,1223],{"class":834},"    else\n",[145,1225,1227,1229,1231,1234],{"class":147,"line":1226},35,[145,1228,1209],{"class":233},[145,1230,835],{"class":834},[145,1232,1233],{"class":155},"\"error\"",[145,1235,1236],{"class":223},"   # permission denied, API disabled, and so on\n",[145,1238,1240],{"class":147,"line":1239},36,[145,1241,1242],{"class":834},"    fi\n",[145,1244,1246,1249,1252,1255,1258,1261,1264,1267],{"class":147,"line":1245},37,[145,1247,1248],{"class":233},"    row",[145,1250,1251],{"class":834},"+=",[145,1253,1254],{"class":155},"\",${",[145,1256,1257],{"class":233},"value",[145,1259,1260],{"class":834},"\u002F\u002F",[145,1262,1263],{"class":155},";",[145,1265,1266],{"class":834},"\u002F",[145,1268,1269],{"class":155},"|}\"\n",[145,1271,1273],{"class":147,"line":1272},38,[145,1274,1275],{"class":834},"  done\n",[145,1277,1279,1282,1284,1287],{"class":147,"line":1278},39,[145,1280,1281],{"class":162},"  echo",[145,1283,1112],{"class":155},[145,1285,1286],{"class":233},"$row",[145,1288,950],{"class":155},[145,1290,1292],{"class":147,"line":1291},40,[145,1293,1294],{"class":834},"done\n",[11,1296,1297],{},"Sample output:",[136,1299,1302],{"className":1300,"code":1301,"language":520,"meta":141},[518],"project,iam.managed.disableServiceAccountKeyCreation,iam.disableServiceAccountKeyCreation,compute.managed.vmExternalIpAccess,sql.managed.restrictPublicIp,sql.restrictPublicIp,storage.publicAccessPrevention,storage.uniformBucketLevelAccess\nnetwork-edge-prod,True,unset,False|True,True,unset,True,True\nteam-a-dev,True,unset,True,True,unset,unset,True\nlegacy-reporting,unset,unset,unset,unset,unset,unset,unset\n",[23,1303,1301],{"__ignoreMap":141},[11,1305,1306,1307,1310,1311,1314,1315,1318,1319,1322],{},"A value like ",[23,1308,1309],{},"False|True"," means a conditional rule is in play (here, the tag exemption), ",[23,1312,1313],{},"unset"," on every column is the project to go look at first, and ",[23,1316,1317],{},"error"," means the lookup itself failed (usually permissions). For \"what would break if I enforced this,\" Policy Simulator for organization policies answers the question before you commit. It needs ",[23,1320,1321],{},"roles\u002Fpolicysimulator.orgPolicyAdmin",", and it handles custom and managed constraints but not legacy ones.",[35,1324,1326],{"id":1325},"the-honest-summary","The honest summary",[11,1328,1329,1330,1335,1336,1340],{},"A handful of constraints scoped to real credential and exposure risks, rolled out gradually with dry-run data, does more for your security posture than enabling most of the catalog at once. The value is in the few that close a specific, recurring failure mode (keys, external IPs, public databases, public buckets), not in the count of constraints turned on. If you want to find the service account keys that already exist before you lock creation down, the ",[1331,1332,1334],"a",{"href":1333},"\u002F2026\u002F03\u002F04\u002Fnode-js-gcloud-rotating-service-account-keys-before-they-expire\u002F","service account key rotation script"," and the ",[1331,1337,1339],{"href":1338},"\u002F2026\u002F01\u002F14\u002Fpython-gcloud-audit-iam-bindings-across-a-gcp-organization\u002F","organization-wide IAM audit"," are the companion pieces.",[35,1342,1344],{"id":1343},"references","References",[1346,1347,1348,1357,1364,1371,1378,1385,1392,1399,1406,1413],"ul",{},[1349,1350,1351],"li",{},[1331,1352,1356],{"href":1353,"rel":1354},"https:\u002F\u002Fdocs.cloud.google.com\u002Fresource-manager\u002Fdocs\u002Fsecure-by-default-organizations",[1355],"nofollow","Secure-by-default organization policies",[1349,1358,1359],{},[1331,1360,1363],{"href":1361,"rel":1362},"https:\u002F\u002Fdocs.cloud.google.com\u002Forganization-policy\u002Freference\u002Forg-policy-constraints",[1355],"Organization policy constraints reference",[1349,1365,1366],{},[1331,1367,1370],{"href":1368,"rel":1369},"https:\u002F\u002Fdocs.cloud.google.com\u002Forganization-policy\u002Fcreate-organization-policies",[1355],"Create organization policies (YAML, describe, reset, delete)",[1349,1372,1373],{},[1331,1374,1377],{"href":1375,"rel":1376},"https:\u002F\u002Fdocs.cloud.google.com\u002Forganization-policy\u002Ftest-policies",[1355],"Test organization policies in dry-run mode",[1349,1379,1380],{},[1331,1381,1384],{"href":1382,"rel":1383},"https:\u002F\u002Fdocs.cloud.google.com\u002Fresource-manager\u002Fdocs\u002Forganization-policy\u002Ftags-organization-policy",[1355],"Scope organization policies with tags",[1349,1386,1387],{},[1331,1388,1391],{"href":1389,"rel":1390},"https:\u002F\u002Fdocs.cloud.google.com\u002Fresource-manager\u002Fdocs\u002Ftags\u002Ftags-creating-and-managing",[1355],"Create and manage tags",[1349,1393,1394],{},[1331,1395,1398],{"href":1396,"rel":1397},"https:\u002F\u002Fdocs.cloud.google.com\u002Forganization-policy\u002Frestrict-service-accounts",[1355],"Restrict service account usage",[1349,1400,1401],{},[1331,1402,1405],{"href":1403,"rel":1404},"https:\u002F\u002Fdocs.cloud.google.com\u002Fresource-manager\u002Fdocs\u002Forganization-policy\u002Frestricting-domains",[1355],"Restrict identities with domain-restricted sharing",[1349,1407,1408],{},[1331,1409,1412],{"href":1410,"rel":1411},"https:\u002F\u002Fdocs.cloud.google.com\u002Fpolicy-intelligence\u002Fdocs\u002Ftest-organization-policies",[1355],"Policy Simulator for organization policies",[1349,1414,1415],{},[1331,1416,1419],{"href":1417,"rel":1418},"https:\u002F\u002Fdocs.cloud.google.com\u002Fsql\u002Fdocs\u002Fmysql\u002Forg-policy\u002Fconfigure-org-policy",[1355],"Cloud SQL organization policies",[1421,1422,1423],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}",{"title":141,"searchDepth":166,"depth":166,"links":1425},[1426,1427,1428,1429,1430,1431,1432],{"id":37,"depth":166,"text":38},{"id":197,"depth":166,"text":198},{"id":330,"depth":166,"text":331},{"id":428,"depth":166,"text":429},{"id":718,"depth":166,"text":719},{"id":1325,"depth":166,"text":1326},{"id":1343,"depth":166,"text":1344},"techcolumnist",[1435],"engineering","2026-05-13T14:00:00Z","Which Google Cloud organization policy constraints prevent real incidents, with policy YAML, the dry-run workflow, tag-based exemptions, and an audit script.","md",false,null,{},"\u002Fblog\u002F2026\u002F05\u002F13\u002Fgoogle-cloud-organization-policy-constraints-that-actually-matter",{"title":6,"description":1437},[1433],"blog\u002F2026\u002F05\u002F13\u002Fgoogle-cloud-organization-policy-constraints-that-actually-matter",[152],"\u002F2026\u002F05\u002F13\u002Fgoogle-cloud-organization-policy-constraints-that-actually-matter\u002F","6e79SGSm7QAXBxOUcKSFByRxANoZdWNTzcULYYi0K-0",{"title":1450,"description":1451,"date":1452,"url":1453,"categories":1454,"tags":1456,"image":1440,"readingTime":386,"canonical":1433,"sites":1461,"series":1440,"seriesOrder":1440},"Data Warehouse: Building a Reporting Layer Without a BI Team","A practical build for an IT operations reporting layer with no BI team: one Postgres database, raw\u002Fstaging\u002Fmarts schemas, an API loader script, and freshness checks.","2026-05-20T14:00:00Z","\u002F2026\u002F05\u002F20\u002Fdata-warehouse-building-a-reporting-layer-without-a-bi-team\u002F",[1435,1455],"strategy",[1457,1458,1459,1460],"data-warehouse","reporting","sql","python",[1433],{"title":1463,"description":1464,"date":1465,"url":1466,"categories":1467,"tags":1468,"image":1440,"readingTime":394,"canonical":1433,"sites":1470,"series":1440,"seriesOrder":1440},"Google Cloud: Choosing Between GKE and Cloud Run for an Internal Tool","How to pick GKE Autopilot or Cloud Run for an internal tool: billing models, sidecars, IAP, VPC egress, and team fluency, with deploy scripts for both.","2026-05-06T14:00:00Z","\u002F2026\u002F05\u002F06\u002Fgoogle-cloud-choosing-between-gke-and-cloud-run-for-an-internal-tool\u002F",[1435],[152,1469],"kubernetes",[1433],[1472,1462,1480],{"title":1473,"description":1474,"date":1475,"url":1476,"categories":1477,"tags":1478,"image":1440,"readingTime":386,"canonical":1433,"sites":1479,"series":1440,"seriesOrder":1440},"Google Cloud: IAM Least Privilege in Practice, Not Just in Theory","Running least privilege in a real Google Cloud org: finding basic roles, working the IAM recommender, custom roles, temporary access, and service account risk.","2026-09-09T14:00:00Z","\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory\u002F",[1435],[152],[1433],{"title":1481,"description":1482,"date":1483,"url":1333,"categories":1484,"tags":1486,"image":1440,"readingTime":189,"canonical":1433,"sites":1488,"series":1440,"seriesOrder":1440},"Node.js: gcloud – Rotating Service Account Keys Before They Expire","A Node.js script that finds aging Google Cloud service account keys, mints one replacement, and disables the old keys once it is deployed.","2026-03-04T14:00:00Z",[1485,1435],"scripts",[1487,152],"nodejs",[1433],{"doc":1440,"posts":1490},[],1790052513920]