Chapter 33
Common GCP Security Failures
Scope. This chapter is an anti-pattern index. Every failure below is prevented by a control an earlier chapter owns, and none of those controls is re-explained here. Each section gives four things: what the failure looks like in a real estate, one command that detects it, the section that prevents it, and why teams keep doing it anyway. The last of those is the only genuinely new material, and it is the part written most carefully — because a failure that keeps recurring after everyone knows better is a failure with a cause nobody has addressed. Prerequisites. Chapters 2–5, 8–14, 16–17, 22–26, and Chapter 31. Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0; every detection command resolved with
--help; see sources at end.
Every failure in this chapter is well known, documented by Google, detected by Security Command Center, and preventable by a constraint. They persist anyway, at scale, in estates run by competent teams. That is the observation the chapter is built on.
The reason is almost never ignorance. It is that the insecure path is shorter, the secure path requires something that does not exist yet, and the failure produces no signal until someone looks. Each section's last paragraph is an attempt at the honest version of that, because a recommendation that ignores the incentive gets ignored in turn.
Cloud Asset Inventory is the detection tool for most of these (§29.6), and its search query language has one property worth knowing before the commands below: it tokenizes on punctuation, so / splits an identifier into words. That makes some searches possible that look impossible.
A note on the two basic roles. §33.4 discusses them, and this book never prints their identifiers — they are written Owner, Editor, Viewer, and Browser throughout, and the detection command in §33.4 is written to work without naming them either.
33.1 Public Storage Buckets §
The failure. A bucket with allUsers or allAuthenticatedUsers in its IAM policy, or a legacy object ACL granting the same. Usually it holds exports, backups, or "temporary" data that became permanent.
Detect it across the estate:
gcloud asset search-all-iam-policies \
--scope=organizations/123456789012 \
--query='memberTypes:(allUsers OR allAuthenticatedUsers)' \
--asset-types='storage.googleapis.com/Bucket'
Prevented by §11.10 and, estate-wide, constraints/storage.publicAccessPrevention with constraints/storage.uniformBucketLevelAccess (§31.7). §10.12 is the book's one permitted public binding and it is a Cloud Run invoker, not a bucket.
Why teams keep doing it. Making a bucket public is one click and it works instantly; the alternative — a signed URL, a load balancer with a backend bucket, an authenticated client — requires code changes in whatever consumes the data. The public bucket is usually created to unblock someone on a deadline, and nothing ever revisits it because it keeps working. Uniform bucket-level access is also reversible for ninety days (§32.5), so the window in which someone can undo a hasty decision closes silently.
33.2 Public Databases §
The failure. A Cloud SQL instance with a public IP address and an authorized network of 0.0.0.0/0, or with broad authorized ranges that amount to the same thing.
Detect it:
gcloud sql instances list \
--project=rc-saas-prod-data-01 \
--format='table(name, settings.ipConfiguration.ipv4Enabled,
settings.ipConfiguration.authorizedNetworks[].value)'
Prevented by §12.6, and estate-wide by constraints/sql.restrictPublicIp and constraints/sql.restrictAuthorizedNetworks (§31.7), both of which also have .managed. forms.
Why teams keep doing it. Private IP on Cloud SQL requires private services access, which requires an allocated range, which requires the network team. A public IP with an authorized network requires nothing and appears secure — it is an allowlist, after all. The failure comes later, when the allowlist is widened for a contractor, a VPN change, or a CI runner with a dynamic address, and 0.0.0.0/0 becomes the value that makes the ticket close.
33.3 Overprivileged IAM §
The failure. Principals holding roles far broader than they use, accumulated because roles are granted when something breaks and never removed when it stops being needed.
Detect it with the recommender, which measures actual usage:
gcloud recommender recommendations list \
--project=rc-saas-prod-app-01 \
--location=global \
--recommender=google.iam.policy.Recommender
Prevented by §3.32 and the least-privilege practice in §3.9. Deny policies (§3.17) bound it from the other direction.
Why teams keep doing it. Diagnosing a permission error is genuinely hard: the message names one permission, granting that permission often reveals another, and the fastest path to a working system is a broader role. The grant is made under pressure, it works, and nothing ever fails to tell you it is too broad. There is no error message for excess permission — which is precisely why the recommender exists, and why its output has to be acted on rather than merely produced.
33.4 Owner and Editor Roles §
The failure. Human users and service accounts holding the Owner or Editor basic roles at project or organization level. Owner includes the ability to set IAM policy, which makes it self-perpetuating: an Owner can restore their own access after it is removed.
Detect it. The search query language tokenizes on punctuation, so a role identifier splits into words and can be matched without writing it out:
gcloud asset search-all-iam-policies \
--scope=organizations/123456789012 \
--query='policy:(owner OR editor)'
The stronger query targets the capability rather than the name:
gcloud asset search-all-iam-policies \
--scope=organizations/123456789012 \
--query='policy.role.permissions:resourcemanager.projects.setIamPolicy'
Prevented by §3.10, and by constraints/iam.managed.preventPrivilegedBasicRolesForDefaultServiceAccounts for the service-account case (§31.1).
Why teams keep doing it. Owner is what the person who created the project already has, so granting it to a colleague is the natural way to share. Editor is worse because it looks moderate — it sounds like "can edit things", and it in fact permits creating and deleting resources across most services. Neither name communicates its scope, and the console presents them at the top of the role list.
33.5 Service Account Key Sprawl §
The failure. Hundreds of user-managed service account keys, most created years ago, most unused, none rotated, several in repositories or laptops nobody can enumerate.
Detect the estate-wide population:
gcloud asset search-all-resources \
--scope=organizations/123456789012 \
--asset-types='iam.googleapis.com/ServiceAccountKey'
Then per account, distinguishing the two kinds:
gcloud iam service-accounts keys list \
--iam-account=sa-app-prod@rc-saas-prod-app-01.iam.gserviceaccount.com \
--managed-by=user
--managed-by takes user, system, or any, and defaults to any. Without --managed-by=user the output is dominated by Google-managed keys that are rotated automatically and are not the problem.
Prevented by §3.25 and, estate-wide, constraints/iam.managed.disableServiceAccountKeyCreation with constraints/iam.disableServiceAccountKeyUpload (§31.4).
Why teams keep doing it. A key works everywhere, immediately, with no configuration: it is a file, and every client library reads it. Workload Identity Federation requires setting up a pool, a provider, an attribute condition, and a binding before anything works at all — which is an afternoon against thirty seconds. The key also never expires, so nothing ever forces a revisit.
33.6 Embedded Credentials §
The failure. Secrets in places nobody searches: VM metadata and startup scripts, container image layers, Cloud Run environment variables, build substitutions, and Terraform variable files.
Detect what a metadata search can reach:
gcloud asset search-all-resources \
--scope=organizations/123456789012 \
--asset-types='compute.googleapis.com/Instance' \
--read-mask='*'
The --read-mask='*' matters — instance metadata is only returned in the full resource representation, so a default search will not show it.
Prevented by §13.6, with Sensitive Data Protection (Chapter 15) for content inspection and §22.x for repository scanning.
Why teams keep doing it. A startup script that needs a password has to get it from somewhere, and putting it in the script is one line while fetching it from Secret Manager requires the instance to have an identity, a role binding, and network reachability to the API. Every one of those can fail at boot in a way that is hard to debug on a machine you cannot log into. The environment variable is chosen because it cannot fail.
33.7 Default Service Accounts §
The failure. Workloads running as the Compute Engine default service account, PROJECT_NUMBER-compute@developer.gserviceaccount.com, which historically received the Editor basic role at project creation and is shared by everything in the project.
Detect it:
gcloud asset search-all-iam-policies \
--scope=organizations/123456789012 \
--query='policy:compute@developer'
Prevented by §3.8, and by constraints/iam.automaticIamGrantsForDefaultServiceAccounts (§31.1), which is in the automatic baseline for organizations created on or after 2024-05-03.
Why teams keep doing it. It is the default. Creating an instance, a Cloud Run service, or a node pool without specifying --service-account attaches it silently, and everything works — which is the whole problem, because the reason everything works is that the account can do almost anything. The failure is invisible until an incident, at which point the blast radius is every resource in the project.
33.8 Public VM Administration §
The failure. Instances with external IP addresses and port 22 or 3389 reachable, administered by SSH from the internet.
Detect the instances:
gcloud compute instances list \
--project=rc-saas-prod-app-01 \
--filter='networkInterfaces.accessConfigs.natIP:*' \
--format='table(name, zone, networkInterfaces[].accessConfigs[].natIP)'
Prevented by §8.22 and §4.16, with constraints/compute.vmExternalIpAccess (§31.3) estate-wide. IAP TCP forwarding (§8.21) is the replacement, and OS Login (§8.19) is what makes the authorization auditable.
Why teams keep doing it. The secure path has a hard prerequisite: an instance with no external IP cannot reach the internet without Cloud NAT, and cannot be reached without a firewall rule for 35.235.240.0/20. Both are network-team work. Meanwhile an external IP is a checkbox, and it makes the machine work immediately for both directions. §31.3's ordering — NAT first, then the constraint — exists because teams that enforce the constraint first experience it as an outage and roll it back.
33.9 Open Firewall Rules §
The failure. A rule permitting 0.0.0.0/0 to 22, 3389, or every port. Frequently named allow-all-temp and several years old.
Detection has to cover both systems, because the book's default is Cloud NGFW policies and most estates still have legacy rules:
gcloud compute firewall-rules list \
--project=rc-saas-prod-app-01 \
--filter='sourceRanges:0.0.0.0/0 AND direction=INGRESS' \
--format='table(name, network, allowed[].map().firewall_rule().list())'
gcloud compute network-firewall-policies export-rules POLICY_NAME \
--global --destination=rules.yaml
gcloud compute network-firewall-policies rules has no list subcommand — only create, delete, describe, and update. Enumerating a policy's rules means exporting them.
Prevented by §5.13 and §5.14, with the org-level baseline that denies SSH and RDP from anywhere.
Why teams keep doing it. The rule is created during an incident or a migration, when narrowing it means knowing the source addresses and nobody does yet. It is named temp, it works, and the incident ends. Nothing subsequently fails because the rule is too broad, and the person who created it has no mechanism reminding them it exists. A firewall rule with an expiry would solve most of this and there is not one.
33.10 Excessive Network Trust §
The failure. A flat network where anything can reach anything: one large VPC, no segmentation, an egress allow-all, and the implicit belief that being inside the perimeter means being trusted.
Detect the over-broad rules by their effect:
gcloud recommender insights list \
--project=rc-saas-prod-app-01 \
--location=global \
--insight-type=google.compute.firewall.Insight
Prevented by §5.27's Shared VPC design, §5.14's hierarchical policies, and Chapter 20's perimeters. Chapter 35 is the argument for why network position should not confer trust at all.
Why teams keep doing it. Segmentation costs something on every subsequent change: a new service needs a rule, a rule needs a review, a review needs someone who understands the topology. A flat network costs nothing per change and its cost arrives all at once, during lateral movement in an incident. The trade is real, and the honest answer is that segmentation has to be automated (§26.28) or it will be abandoned.
33.11 Missing Audit Logs §
The failure. Data Access audit logs disabled, so there is no record of who read what. This is the default for every service except BigQuery.
Detect it by reading the audit configuration directly:
gcloud projects get-iam-policy rc-saas-prod-data-01 \
--format='yaml(auditConfigs)'
Prevented by §17.3, configured in the IAM policy's auditConfigs. No organization policy constraint enables Data Access logging (§31.10), so this cannot be enforced the way other baselines are.
Why teams keep doing it. Data Access logs are off by default and they are expensive — they are the highest-volume log class in most estates, and enabling them across an organization is a visible line on the bill with no visible benefit until an investigation needs them. The decision is usually deferred rather than made, and the cost of the deferral is only realized when an incident asks a question that has no answer.
33.12 Insecure CI/CD §
The failure. A pipeline with more privilege than anything it deploys: a build service account that can deploy to production, deploy credentials available to fork-originated builds, and unpinned third-party actions.
Detect the over-privileged build identity:
gcloud asset search-all-iam-policies \
--scope=organizations/123456789012 \
--query='policy:cloudbuild.gserviceaccount.com'
Prevented by §25.13's separation of build and deploy identities, §22.x's fork handling, and §26.24's plan/apply split.
Why teams keep doing it. The pipeline is the thing everyone depends on and nobody owns. It is built once, under time pressure, by whoever needed it; it is granted broad permissions because narrowing them means debugging a pipeline failure with a ten-minute feedback loop; and it is then never revisited because touching it risks blocking every team at once. Its privilege is the sum of every permission ever needed by anything it has deployed.
33.13 Unverified Container Images §
The failure. Deployments referencing mutable tags, no digest pinning, no signature verification, and a Binary Authorization policy either absent or permanently in dry-run.
Detect the policy's actual state:
gcloud container binauthz policy export
Prevented by §25.10's gate, §25.12's provenance verification, §24.5's immutable tags, and §9.27's GKE enforcement.
Why teams keep doing it. A tag is readable and a digest is not: billing:v2.1.4 is meaningful to a human reviewing a manifest and billing@sha256:9f2c... is not. Teams choose the readable form for the same reason they choose readable variable names, and the mutability that makes tags dangerous is exactly the property that makes a rolling latest deployment convenient. Binary Authorization stays in dry-run because leaving dry-run breaks the first deployment that lacks an attestation, and that is always someone's release.
33.14 Secrets in Terraform State §
The failure. Plaintext credentials in a state file — generated passwords, private keys, service account key material — readable by anyone with access to the state bucket, and preserved in every prior object version.
Detect it directly:
gcloud storage cat gs://rc-saas-bootstrap-tfstate-01/envs/prod/default.tfstate \
| grep -iE '"(password|private_key|secret|token)"'
Prevented by §13.13's write-only arguments and ephemeral resources, with §26.17 treating the state file as an asset with its own IAM, CMEK, and audit logging.
Why teams keep doing it. Nothing signals the mistake. A random_password resource and a google_secret_manager_secret_version with secret_data both work perfectly; the plan is clean, the apply succeeds, the application connects. The secret's presence in state is discovered only when someone reads the file, which is usually never — or during an incident, which is too late. sensitive = true makes it worse by producing a redacted plan output that looks like protection and is not (§26.8).
33.15 Secrets in Git §
The failure. A credential committed to a repository. The distinguishing property is that removing it does not help: once pushed, it exists in every clone, every fork, and every cached view.
Prevented by §13.14, with repository-native push protection and Sensitive Data Protection (Chapter 15) for content inspection.
The response order is fixed and it is the section's real content:
- Revoke first. The credential is compromised from the moment it was pushed, regardless of who has looked.
- Rotate second, and verify the new value works before the old one is destroyed.
- Decide about history last, knowing that rewriting it invalidates every clone and does not recall what was already fetched.
Why teams keep doing it. The commit is almost always accidental — a .env file not in .gitignore, a test fixture with a real value, a config file committed during a rushed debugging session. No developer decides to commit a secret. That means prevention has to be mechanical, because intent is not the failing part, and it also means blame is the wrong response: a team that punishes the commit gets slower disclosure, not fewer commits.
33.16 Uncontrolled Project Creation §
The failure. Anyone in the organization can create a project and attach it to the billing account. Those projects are outside every folder-scoped policy, invisible to the security team, and funded.
Detect the population that can do it:
gcloud organizations get-iam-policy 123456789012 \
--flatten='bindings[].members' \
--filter='bindings.role=roles/resourcemanager.projectCreator' \
--format='value(bindings.members)'
Prevented by §2.28, by removing the organization-wide default grants and routing creation through a project factory (§2.26, §26.27).
The billing account is the stronger control. A project with no billing account can do almost nothing, so roles/billing.user is the permission that actually gates useful project creation — and it is the one to restrict.
Why teams keep doing it. Project creation is granted broadly during the first week of a cloud program, when the priority is unblocking teams and nobody has built a factory yet. It is never narrowed, because narrowing it means someone has to operate the factory and answer requests. The projects created in that window are the ones with no labels, no owner, and no policy — and they are still running.
33.17 Missing Organization Policies §
The failure. An organization with few or no constraints set, usually because it predates the automatic baseline and nobody has applied one since.
Detect the gap:
gcloud asset analyze-org-policies \
--scope=organizations/123456789012 \
--constraint=constraints/compute.vmExternalIpAccess
Both --scope and --constraint are required, so this answers one constraint at a time; the estate-wide gap analysis is gcloud org-policies list --show-unset against the baseline in Chapter 31.
Prevented by Chapter 31 in its entirety.
Why teams keep doing it. Organization policy is set at the organization node, which almost nobody has permission to modify, and it affects every project at once. That combination — a rare permission and an estate-wide blast radius — makes it the change everyone defers. An organization created before 2024-05-03 gets none of them automatically, so the estates least likely to have a baseline are the oldest and largest ones.
33.18 Unprotected Production Projects §
The failure. A production project that can be deleted by anyone with sufficient role, with no lien, no deletion protection on its stateful resources, and no test of the recovery path.
Prevented by §2.29, with deletion_protection and prevent_destroy on stateful resources (§26.21).
A deleted project enters a recovery window rather than disappearing immediately, and gcloud projects undelete restores it — but not everything inside survives, and the window is finite. Verify the current period against Google's documentation before relying on a specific number.
Liens are the strongest control and the least usable. They block deletion at the API, and the CLI surface for managing them is not generally available — which means the control exists and cannot be operated from a stable command surface. Treat that as the reason to rely on deletion protection and IAM instead.
Why teams keep doing it. Nobody believes they will delete production, so protecting against it feels like insuring against a mistake too stupid to make. The mistake, when it happens, is never a person deciding to delete production — it is a script with a wrong variable, a Terraform configuration whose resource block was removed (§26.21), or a cleanup job whose filter matched more than intended.
33.19 Poor Backup Design §
The failure. Backups that share a failure domain with what they protect: same project, same region, same credentials, same key. Plus the universal one — no restore has ever been tested.
Detect what protection exists:
gcloud backup-dr backup-vaults list --location=us-central1 \
--project=rc-saas-shared-sec-01
Prevented by §8.27 and §29.9, with backup vaults providing enforced retention that the source project's compromise cannot shorten.
The three failure domains that matter, and a backup design must separate at least the first:
| Shared with the source | What it fails to survive |
|---|---|
| Project and IAM | A compromised or deleted project |
| Region | A regional outage |
| Encryption key | A destroyed or disabled key |
Why teams keep doing it. Backups are configured once, at the beginning, when the system is small and the backup is a checkbox on the database. Separating the failure domain requires a second project, cross-project IAM, and a key in a third place — none of which the checkbox does. And because a backup that is never restored looks identical to a backup that works, there is no feedback signal at all until the day it matters.
33.20 Security Theater vs. Effective Controls §
The failure. A control that exists, reports, and prevents nothing. This is the chapter's closing section because it explains why several of the others survive an audit.
Six specific instances, each a real mechanism from earlier chapters:
| The theater | The reality |
|---|---|
| A dry-run policy left in dry-run | Logs a violation and permits it (§2.30) |
| An SCC finding muted rather than fixed | Removes the alert, not the exposure (§29.10) |
Binary Authorization with a broad admissionWhitelistPatterns | Every matching image bypasses evaluation entirely (§25.10) |
| Cloud Armor left in preview mode | Records what it would have blocked (Chapter 19) |
A perimeter with ANY_IDENTITY ingress | Reduces to whatever access level is attached (§20.5) |
| An alert with no receiving channel | Fires into nothing (§18.4) |
Detect the first, which is the most common:
gcloud org-policies describe compute.vmExternalIpAccess \
--organization=123456789012 --effective
What these six have in common is that the artifact looks identical either way. A dry-run policy appears in the policy list; a muted finding appears as handled; a preview WAF rule appears in the rule set. Every one of them passes a checklist that asks "is the control configured" and fails the only question that matters, which is "what happens when someone tries".
Why teams keep doing it. Each of these is a deliberate, correct intermediate state — you should deploy a policy in dry-run, and you should run Cloud Armor in preview first. The failure is not entering the state; it is that nothing schedules leaving it. There is no expiry on a dry-run, no review date on a mute, and no alert when a control has been in report-only mode for a year. The control that is missing is the one that says "this was supposed to be temporary".
The test to apply to any control is whether you have watched it deny something. If nobody can point at a blocked request, a rejected deployment, or a failed apply, the control is unproven — and an unproven control and an ineffective one are indistinguishable from the outside.
Chapter Summary §
- These failures persist not from ignorance but because the insecure path is shorter and produces no signal until someone looks.
- Cloud Asset Inventory search tokenizes on punctuation, which makes some identifier searches possible that look impossible.
- Uniform bucket-level access is reversible for ninety days, so a hasty public-bucket decision quietly becomes permanent.
- Public Cloud SQL persists because private IP needs an allocated range and a network team, while an authorized network needs nothing.
- There is no error message for excess permission, which is why the IAM recommender's output must be acted on rather than produced.
- Neither basic role's name communicates its scope, and Owner's
setIamPolicymakes it self-perpetuating. gcloud iam service-accounts keys listdefaults to--managed-by=any; withoutuserthe output is dominated by Google-managed keys.- A service account key never expires, so nothing ever forces a revisit.
- Instance metadata is returned only with
--read-mask='*', so a default asset search will not reveal a secret in a startup script. - The Compute Engine default service account is attached silently when
--service-accountis omitted, and everything works because it can do almost anything. - Removing external IPs has a hard prerequisite — Cloud NAT and the IAP firewall rule — which is why enforcing the constraint first feels like an outage.
gcloud compute network-firewall-policies ruleshas nolist; enumerate withexport-rules.- A firewall rule with an expiry would solve most of §33.9 and there is not one.
- Data Access audit logs are off by default except for BigQuery, and no organization policy constraint enables them.
- A pipeline's privilege is the sum of every permission ever needed by anything it has deployed.
- A tag is readable and a digest is not, which is why mutable references survive every argument against them.
- Nothing signals a secret in Terraform state: the plan is clean, the apply succeeds, and
sensitive = truelooks like protection. - On a secret in Git: revoke first, rotate second, decide about history last — and do not respond with blame, which slows disclosure without reducing commits.
- The billing account, not
roles/resourcemanager.projectCreator, is the control that actually gates useful project creation. - Liens block deletion at the API but have no generally available CLI surface; rely on deletion protection and IAM.
- A backup that is never restored looks identical to one that works, so there is no feedback signal until it matters.
- Every security-theater instance is a legitimate temporary state that nothing scheduled an exit from.
- The test for any control: has anyone watched it deny something? An unproven control is indistinguishable from an ineffective one.
Security Checklist §
| Control | Why it matters | How to verify (CLI + Console) |
|---|---|---|
| No public buckets in the estate | The most-exploited cloud misconfiguration | search-all-iam-policies --query='memberTypes:(allUsers OR allAuthenticatedUsers)' (§33.1) |
| No Cloud SQL instance with a public IP | An allowlist widens; a private path does not | gcloud sql instances list --format='value(name,settings.ipConfiguration.ipv4Enabled)' |
| IAM recommender output triaged on a schedule | Excess permission produces no error | gcloud recommender recommendations list --recommender=google.iam.policy.Recommender --location=global |
| No basic roles held by humans or service accounts | Owner can restore its own access | gcloud asset search-all-iam-policies --scope=organizations/ORG_ID --query='policy:(owner OR editor)' |
| User-managed key count is zero, or enumerated with owners | Keys never expire | gcloud iam service-accounts keys list --iam-account=SA --managed-by=user |
| Instance metadata scanned for credentials | It is invisible without a full read mask | gcloud asset search-all-resources --asset-types='compute.googleapis.com/Instance' --read-mask='*' |
| No workload uses a default service account | It is attached silently and can do almost anything | gcloud asset search-all-iam-policies --scope=organizations/ORG_ID --query='policy:compute@developer' |
| No production instance has an external IP | Removes an entire class of exposure | gcloud compute instances list --filter='networkInterfaces.accessConfigs.natIP:*' |
| Both legacy rules and NGFW policies audited | The book's default is policies; estates have both | gcloud compute firewall-rules list --filter='sourceRanges:0.0.0.0/0'; network-firewall-policies export-rules |
| Firewall insights reviewed | Over-broad rules are visible from their usage | gcloud recommender insights list --insight-type=google.compute.firewall.Insight --location=global |
| Data Access audit logs enabled where data lives | No constraint can enforce this | gcloud projects get-iam-policy PROJECT_ID --format='yaml(auditConfigs)' |
| Build identity cannot deploy | A compromised build must not reach production | gcloud asset search-all-iam-policies --query='policy:cloudbuild.gserviceaccount.com' |
| Binary Authorization enforced, not dry-run | Dry-run admits everything it logs | gcloud container binauthz policy export |
| State file grepped for plaintext credentials | Nothing else signals the mistake | gcloud storage cat gs://BUCKET/PREFIX/default.tfstate | grep -i password |
| Push protection enabled on every repository | Prevention must be mechanical; intent is not the failing part | Repository settings |
| Project Creator and Billing User grants enumerated | Billing is the control that matters | gcloud organizations get-iam-policy ORG_ID --filter='bindings.role=roles/billing.user' |
| Baseline constraints applied and gap-analyzed | Old organizations receive none automatically | gcloud org-policies list --organization=ORG_ID --show-unset |
| Production projects carry deletion protection | The mistake is a script, not a decision | Resource deletion_protection; prevent_destroy in the repository |
| Backups separated from the source's project and key | A compromised project must not reach its own backups | gcloud backup-dr backup-vaults list --location=REGION |
| A dated restore test exists | An untested restore is indistinguishable from a working one | The restore test record |
| Every dry-run, mute, and preview mode has an exit date | Nothing schedules leaving a temporary state | The exception register with review dates |
| Every control has been observed denying something | An unproven control is indistinguishable from an ineffective one | Denied-request logs per control |
Sources §
- https://cloud.google.com/asset-inventory/docs/query-syntax — the search query language and its tokenization rules (last validated 2026-09-04)
- https://cloud.google.com/asset-inventory/docs/supported-asset-types — the asset type identifiers the detection commands filter on (last validated 2026-09-04)
- https://cloud.google.com/asset-inventory/docs/searching-iam-policies —
search-all-iam-policiesand its query fields (last validated 2026-09-04) - https://cloud.google.com/recommender/docs/recommenders — the recommender IDs, including the IAM policy recommender (last validated 2026-09-04)
- https://cloud.google.com/recommender/docs/insights/insight-types — insight type IDs, including the firewall insight (last validated 2026-09-04)
- https://cloud.google.com/iam/docs/service-account-types — user-managed versus Google-managed keys (last validated 2026-09-04)
- https://cloud.google.com/compute/docs/access/service-accounts — the Compute Engine default service account and its address form (last validated 2026-09-04)
- https://cloud.google.com/logging/docs/audit — which audit log types are enabled by default (last validated 2026-09-04)
- https://cloud.google.com/resource-manager/docs/creating-managing-projects — project deletion, the recovery window, and undelete (last validated 2026-09-04)
- https://cloud.google.com/resource-manager/docs/project-liens — liens and their command-surface availability (last validated 2026-09-04)
- https://cloud.google.com/iam/docs/roles-overview — what the basic roles actually permit (last validated 2026-09-04)
- https://cloud.google.com/binary-authorization/docs/key-concepts — policy structure, evaluation, and the admission whitelist (last validated 2026-09-04)
- https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules —
ANY_IDENTITYand what an ingress rule actually permits (last validated 2026-09-04)