DEV Community

Honey Kiran
Honey Kiran

Posted on Originally published at honeykiran.substack.com

Your GCP Project Is Insecure by Default

A fresh GCP project: everything reachable, everything convenient, everything on.

A fresh GCP project: everything reachable, everything convenient, everything on.

Google’s defaults are optimized for one thing: making your first deploy work in five minutes. This post is the day-two list.

Google has been tightening the screws — new GCS buckets default to uniform bucket-level access with public access prevention enforced, and post-May-2024 organizations get several secure-by-default org policies. But plenty still ships open — ten of them, each with its one-line fix.

The attack surface you inherit the moment the project is created.

The attack surface you inherit the moment the project is created.

  • 1: Default VPC: SSH/RDP open to 0.0.0.0/0 — High — firewall-rules delete
  • 2: GKE control plane gets a public endpoint — High — --enable-private-endpoint
  • 3: Cloud SQL gets a public IP — High — --no-assign-ip
  • 4: Default Compute SA is project Editor — High — org policy: automaticIamGrants…
  • 5: Long-lived SA keys can be minted — Med-High — org policy: disableServiceAccountKeyCreation
  • 6: Cloud SQL allows unencrypted connections — Medium — --ssl-mode=ENCRYPTED_ONLY
  • 7: BigQuery datasets inherit project viewers/editors — Medium — authoritative IAM policy
  • 8: SSH via metadata keys; OS Login off — Medium — org policy: requireOsLogin
  • 9: Data Access audit logs are off — Medium — audit_log_config per service
  • 10: Legacy buckets: per-object ACLs — Medium — uniform bucket-level access on

1. Your default VPC lets the whole internet knock on SSH and RDP

Every new project gets a default VPC pre-populated with firewall rules, including default-allow-ssh (TCP 22 from 0.0.0.0/0), default-allow-rdp (TCP 3389 from 0.0.0.0/0), and default-allow-icmp. Every VM launched there is port-scannable from day one.

Why it ships this way: so your very first VM is reachable with zero networking knowledge.

The one line:

gcloud compute firewall-rules delete default-allow-ssh default-allow-rdp
Enter fullscreen mode Exit fullscreen mode

Severity: High. Scope SSH to your IPs — or use IAP tunneling and delete the rules.

2. GKE gives your control plane a public IP

gcloud container clusters create with no networking flags produces a cluster whose Kubernetes API endpoint is reachable from the entire internet. Your control plane is now in every attacker’s scan rotation.

Why it ships this way: so kubectl works from your laptop immediately, no VPN required.

The one line:

gcloud container clusters create my-cluster --region=REGION \
  --enable-private-nodes --enable-private-endpoint \
  --enable-master-authorized-networks 
Enter fullscreen mode Exit fullscreen mode

3. Cloud SQL gets a public IP

New Cloud SQL instances get a public IPv4 address by default. With authorized networks left wide open, your database is one credential-guess from the internet.

Why it ships this way: so connecting from anywhere just works.

The one line:

gcloud sql instances patch INSTANCE_NAME --no-assign-ip
Enter fullscreen mode Exit fullscreen mode

Severity: High. (Requires private IP to be configured first.) For a guardrail that survives forgetfulness, enforce the sql.restrictPublicIp org policy.

4. The default Compute Engine service account is a project Editor

PROJECT_NUMBER-compute@developer.gserviceaccount.com is automatically granted roles/editor on the project at creation, unless an org policy disables the automatic grant. Every VM, node pool, and Cloud Run revision without an explicit service account inherits it — one metadata SSRF from project-wide read/write.

Why it ships this way: so workloads can call GCP APIs with zero IAM setup.

The one line:

gcloud resource-manager org-policies enable-enforce \
  iam.automaticIamGrantsForDefaultServiceAccounts --organization=ORG_ID
Enter fullscreen mode Exit fullscreen mode

Severity: High. Then give every workload its own least-privilege service account. (Auto-enforced for orgs created after 3 May 2024; older orgs must opt in.)

5. Anyone with key-admin rights can mint long-lived service account keys

The iam.disableServiceAccountKeyCreation constraint is not enforced by default on older organizations. JSON keys are forever-credentials: they never expire and they leak into repos.

Why it ships this way: keys are the oldest auth path, and half the ecosystem’s tutorials still start with “download the JSON key.”

The one line:

gcloud resource-manager org-policies enable-enforce \
  iam.disableServiceAccountKeyCreation --organization=ORG_ID
Enter fullscreen mode Exit fullscreen mode

Severity: Medium-High. Use Workload Identity Federation for CI and Workload Identity for GKE instead.

6. Cloud SQL allows unencrypted connections

PostgreSQL defaults to ALLOW_UNENCRYPTED_AND_ENCRYPTED; MySQL doesn’t require SSL unless told to. Credentials and query data can cross the network in plaintext.

Why it ships this way: requiring certs would break every unconfigured client — including yours.

The one line:

# PostgreSQL
gcloud sql instances patch INSTANCE_NAME --ssl-mode=ENCRYPTED_ONLY
# MySQL
gcloud sql instances patch INSTANCE_NAME --require-ssl
Enter fullscreen mode Exit fullscreen mode

Severity: Medium.

7. BigQuery hands every new dataset to project viewers and editors

Create a dataset in a project with viewers, and BigQuery automatically grants them bigquery.dataViewer on it — dataEditor for editors, dataOwner for owners. Your “restricted” dataset inherits the project’s guest list at birth.

Why it ships this way: frictionless collaboration — the data team queries immediately, no IAM ticket.

The one line (Terraform, authoritative):

resource "google_bigquery_dataset_iam_policy" "locked_down" {
  dataset_id  = google_bigquery_dataset.sensitive.dataset_id
  policy_data = data.google_iam_policy.dataset_owners_only.policy_data
}
Enter fullscreen mode Exit fullscreen mode

Severity: Medium. Authoritative IAM replaces the inherited entries.

8. SSH works via project-wide metadata keys — OS Login is off

By default, anyone who can set instance metadata can add SSH keys valid across your VMs — access managed outside IAM entirely. The compute.requireOsLogin constraint — which ties SSH to IAM identities and 2FA — is not enforced by default.

Why it ships this way: gcloud compute ssh works in thirty seconds with no identity plumbing.

The one line:

gcloud resource-manager org-policies enable-enforce \
  compute.requireOsLogin --organization=ORG_ID
Enter fullscreen mode Exit fullscreen mode

Severity: Medium. SSH becomes an auditable, revocable IAM decision.

9. Data Access audit logs are off

Admin Activity logs (who changed what) are always on. But Data Access logs — who read your objects, tables, and secrets — are off by default for most services. If data walks out the door, you won’t have the receipt.

Why it ships this way: volume — data access logs are enormous and billable.

The one line (Terraform):

resource "google_project_iam_audit_config" "storage_data_access" {
  project = var.project_id
  service = "storage.googleapis.com"
  audit_log_config { log_type = "DATA_READ" }
  audit_log_config { log_type = "DATA_WRITE" }
}
Enter fullscreen mode Exit fullscreen mode

Severity: Medium — a detection gap, not an open door. Enable selectively.

10. Legacy buckets can still run per-object ACLs

New buckets default to uniform bucket-level access with public access prevention — genuinely good. But older buckets can still use fine-grained ACLs: one object with an allUsers grant that never appears in the bucket’s IAM policy. Auditing “is anything public” means enumerating every object’s ACL.

Why it ships this way: backward compatibility with a decade of per-object ACL usage.

The one line:

gsutil uniformbucketlevelaccess set on gs://BUCKET_NAME/
Enter fullscreen mode Exit fullscreen mode

Severity: Medium. One policy to read instead of a million ACLs to enumerate.

All ten fixes at a glance — the same project, before and after the checklist:

Before and after: a fresh GCP project vs the same project after the ten one-line fixes.

Before and after: a fresh GCP project vs the same project after the ten one-line fixes.

The pattern

Fix it at the org level once — every future project inherits the posture, not the convenience.

Fix it at the org level once — every future project inherits the posture, not the convenience.

None of these defaults are bugs. They’re one trade repeated ten times: the five-minute happy path over the secure path, because the happy path is what gets evaluated in a trial. The tax comes due silently, months later, when the demo project becomes production.

The fix is equally uniform: run the checklist below once per project, then encode the answers as org policies so the next project inherits the secure posture.

The fresh-project audit checklist

  • [ ] Delete or narrow default-allow-ssh, default-allow-rdp in the default VPC
  • [ ] Confirm GKE clusters use private endpoints (convert any that don’t)
  • [ ] Confirm Cloud SQL instances have no public IP (--no-assign-ip)
  • [ ] Strip roles/editor from the default Compute SA; enforce iam.automaticIamGrantsForDefaultServiceAccounts
  • [ ] Enforce iam.disableServiceAccountKeyCreation at the org level
  • [ ] Set Cloud SQL SSL mode to ENCRYPTED_ONLY / --require-ssl
  • [ ] Replace inherited BigQuery dataset access with authoritative IAM
  • [ ] Enforce compute.requireOsLogin
  • [ ] Enable Data Access audit logs on services holding sensitive data
  • [ ] Convert legacy buckets to uniform bucket-level access
  • [ ] Set the guardrail org policies: sql.restrictPublicIp, compute.vmExternalIpAccess, storage.publicAccessPrevention

Honey Kiran · DevOps & Platform Engineering

I write about SRE, monitoring, Terraform, and the unsexy work that keeps systems alive at 3am.

New posts regularly — send this to the teammate who just clicked “create project” without reading the defaults.

Disclaimer

  • This post describes my personal experience with my own systems and accounts, in my own words. It is a first-person account of what I observed and what I do in my own work — not a statement of any company's intentions, policies, or internal decisions. Where I do not know something, the post says so.

  • I am not affiliated with, endorsed by, or speaking for any company, product, or vendor named in this post. All product and company names are trademarks of their respective owners and are mentioned only to describe the experience accurately.

  • Any error messages, documentation excerpts, or figures quoted here are reproduced from my own records or from the public sources linked in the post, believed accurate at the time of writing. Product behavior changes over time; verify against current official documentation before relying on any detail.

  • Nothing in this post is professional, financial, legal, or security advice for your specific situation. Examples are illustrative — validate in your own environment before running anything in production.

  • Corrections are welcome. If any company named here publishes an official statement, or if any detail is wrong, I will update this post and note the correction. The goal is an accurate public record, not a verdict.

© 2026 Honey Kiran. All rights reserved.

Top comments (0)