Back to blog
Best PracticesCloud SecurityDatabasesGCPNetworking

Firewall Allows Public PostgreSQL on GCP: Risks and Fixes

Learn why a GCP firewall rule allowing public PostgreSQL on port 5432 is dangerous, how to remediate it with gcloud and Terraform, and how to prevent it in CI.

TL;DR

This check flags GCP firewall rules that allow PostgreSQL traffic on port 5432 from 0.0.0.0/0. An open database port invites credential brute-forcing and data theft. Restrict the source range to known IPs or a private network and lock it down with a Cloud SQL private connection.

Exposing a PostgreSQL port to the entire internet is one of those mistakes that looks harmless in a sprint and turns into a breach report a month later. Lensix raises vpc_openpostgresql when it finds a VPC firewall rule that permits inbound traffic on TCP port 5432 from a public source range. This post walks through what the check looks at, why a public database port is dangerous, and exactly how to close the gap.


What this check detects

The vpc_openpostgresql check inspects your GCP firewall rules and looks for an ingress rule that matches all three of these conditions:

  • Direction is INGRESS and action is allow
  • The allowed protocol and port include TCP 5432 (PostgreSQL's default port)
  • The sourceRanges field contains a public CIDR, most commonly 0.0.0.0/0

A rule like this is the trigger:

{
  "name": "allow-postgres",
  "network": "default",
  "direction": "INGRESS",
  "allowed": [
    { "IPProtocol": "tcp", "ports": ["5432"] }
  ],
  "sourceRanges": ["0.0.0.0/0"]
}

Note: The check also catches broad ranges that are not strictly 0.0.0.0/0. A rule scoped to something like 0.0.0.0/1 or a large supernet still exposes the port to most of the public internet, so treat any non-specific range as a finding.

It is worth knowing that GCP firewall rules apply to the target instances they select, either via tags, service accounts, or to every instance in the network when no target is set. A rule with no target attached is the worst case: it opens 5432 on every VM in the VPC.


Why it matters

PostgreSQL was never designed to sit on the open internet. Once port 5432 is reachable from anywhere, a few things happen quickly.

Automated scanning finds it within minutes

Mass scanners like Shodan and Censys continuously sweep the IPv4 space for open database ports. A newly exposed 5432 is typically indexed within hours. Attackers then run credential stuffing and brute-force attempts against common usernames such as postgres.

Weak or default credentials get cracked

If the database uses a weak password, no rate limiting on auth attempts means an attacker can grind through a wordlist. Once they are in, they can read every table, dump customer data, drop tables, or plant ransomware-style extortion notes. There is a well-documented pattern of attackers wiping exposed databases and leaving a ransom message demanding crypto for the "backup" they took.

Danger: An exposed PostgreSQL instance with the COPY ... FROM PROGRAM feature available to a superuser can lead to remote code execution on the database host. This turns a data exposure into full host compromise.

Compliance and business impact

If that database holds personal data, an open port is a finding against PCI DSS, SOC 2, HIPAA, and GDPR controls that require network segmentation and least-privilege access. Beyond the regulatory fines, a public-facing database is the kind of root cause that shows up in post-incident reviews and erodes customer trust.


How to fix it

The fix is to stop allowing 5432 from public ranges. Pick the approach that matches how the database is accessed.

Step 1: Find the offending rule

gcloud compute firewall-rules list \
  --filter="allowed.ports=5432 AND direction=INGRESS" \
  --format="table(name, network, sourceRanges.list(), targetTags.list())"

Inspect the full rule before changing anything:

gcloud compute firewall-rules describe allow-postgres

Step 2: Restrict the source range

If the rule needs to exist, replace the public range with the specific CIDRs that legitimately need access, for example a bastion host, a corporate VPN egress IP, or an internal subnet.

Warning: Updating a firewall rule takes effect immediately. If anything depends on the public access path, including a misconfigured app server, it will lose connectivity. Confirm the real client IPs first, ideally from VPC flow logs, before you tighten the range.

gcloud compute firewall-rules update allow-postgres \
  --source-ranges="10.0.0.0/8,203.0.113.10/32"

Step 3: Or delete the rule entirely

If nothing should reach PostgreSQL from outside the VPC, remove the rule.

Danger: Deleting a firewall rule is not reversible through an undo. Export the rule definition first so you can recreate it if you cut off a path you actually needed.

# Save a copy first
gcloud compute firewall-rules describe allow-postgres --format=json > allow-postgres.bak.json

# Then delete
gcloud compute firewall-rules delete allow-postgres

Step 4: Prefer private connectivity for Cloud SQL

If this is a Cloud SQL for PostgreSQL instance, the right long-term answer is to disable the public IP and use private services access or the Cloud SQL Auth Proxy. That removes the need for any 5432 firewall rule at all.

# Remove the public IP from a Cloud SQL instance
gcloud sql instances patch my-pg-instance --no-assign-ip

# Connect apps through the Auth Proxy instead
./cloud-sql-proxy --private-ip my-project:us-central1:my-pg-instance

Tip: The Cloud SQL Auth Proxy handles IAM-based authentication and TLS for you, so you avoid managing static IP allowlists entirely. For self-managed PostgreSQL on a VM, an Identity-Aware Proxy (IAP) TCP tunnel gives you the same broker-style access without opening 5432 to the world.

Step 5: Define the rule as code (Terraform)

If you manage firewalls with Terraform, encode the safe version so drift gets corrected on the next apply.

resource "google_compute_firewall" "postgres_internal" {
  name      = "allow-postgres-internal"
  network   = google_compute_network.main.id
  direction = "INGRESS"

  allow {
    protocol = "tcp"
    ports    = ["5432"]
  }

  # Only internal subnets and the bastion, never 0.0.0.0/0
  source_ranges = ["10.0.0.0/8"]
  target_tags   = ["postgres"]
}

How to prevent it from happening again

Closing one rule is easy. Keeping it closed across dozens of projects and engineers is the real work. Push the guardrail left so a public 5432 never reaches production.

Block it in CI with a policy check

Run a policy-as-code scan on Terraform plans before they merge. Here is an OPA/Conftest rule that fails any plan opening 5432 to a public range:

package main

deny[msg] {
  rc := input.resource_changes[_]
  rc.type == "google_compute_firewall"
  after := rc.change.after
  after.direction == "INGRESS"
  rule := after.allow[_]
  rule.ports[_] == "5432"
  after.source_ranges[_] == "0.0.0.0/0"
  msg := sprintf("Firewall '%s' exposes PostgreSQL 5432 to the public internet", [after.name])
}

Wire it into the pipeline:

terraform plan -out plan.tfplan
terraform show -json plan.tfplan > plan.json
conftest test plan.json --policy policy/

Enforce with org policy

GCP's compute.vmExternalIpAccess constraint and hierarchical firewall policies let security teams set guardrails that individual projects cannot override. A hierarchical firewall policy at the folder level can deny ingress on database ports from 0.0.0.0/0 regardless of what a project-level rule tries to allow.

Tip: Pair the CI gate with continuous monitoring. Lensix re-evaluates vpc_openpostgresql on every scan, so if someone creates an open rule through the console or an out-of-band script, you get alerted instead of waiting for the next Terraform run.


Best practices

  • Never expose database ports to the internet. This applies to 5432, 3306 (MySQL), 1433 (SQL Server), 6379 (Redis), and 27017 (MongoDB) equally.
  • Default to private connectivity. Use Cloud SQL private IP, VPC peering, or a proxy. Public IPs on databases should be the rare, deliberately justified exception.
  • Use specific source ranges and target tags. Scope each rule to the smallest set of IPs and the smallest set of instances that need it. Avoid untargeted rules that apply network-wide.
  • Front access with a bastion or IAP. Force all admin traffic through a controlled entry point you can log and audit.
  • Enable VPC flow logs. They tell you who is actually connecting, which makes tightening a rule a data-driven decision instead of a guess.
  • Rotate and strengthen credentials. Network controls are layer one, not the whole defense. Strong passwords and IAM database authentication matter even on a private port.

The cheapest fix is the one that never ships. A firewall rule that opens 5432 to the world should fail in CI long before it touches a production VPC.

Treat any public PostgreSQL port as an active incident, not a backlog item. Close it, confirm with VPC flow logs that you did not break a real path, and add the policy gate so the next engineer cannot reintroduce it.