Back to blog
Best PracticesCloud SecurityDatabasesGCPNetworking

GCP Firewall Allows Public MySQL: Risks and Remediation

Learn why a GCP firewall rule that exposes MySQL port 3306 to the public internet is dangerous, and how to remediate and prevent it with CLI, Terraform, and policy-as-code.

TL;DR

This check flags GCP firewall rules that expose MySQL (port 3306) to the public internet (0.0.0.0/0). Open database ports invite brute-force attacks, ransomware, and data theft. Lock the source range down to known internal IPs or use private connectivity, and delete the offending rule.

A database should never answer the door for the entire internet. Yet exposed MySQL instances remain one of the most common ways attackers get a free foothold into cloud environments. This Lensix check, vpc_openmysql, looks at your GCP VPC firewall rules and raises a flag whenever it finds one that permits inbound traffic to TCP port 3306 from a public source range.

If you are running MySQL anywhere in your project, whether on a Compute Engine VM or fronting a self-managed cluster, this is a check you want passing.


What this check detects

The check inspects every firewall rule in your GCP VPC networks and identifies any rule that meets all of the following conditions:

  • Direction is ingress (inbound traffic).
  • Action is allow.
  • The rule covers TCP port 3306, either directly, as part of a port range, or via an "all ports" allowance.
  • The source range includes a public CIDR, most commonly 0.0.0.0/0, which means any IPv4 address on the internet.

Port 3306 is the default listening port for MySQL and MariaDB. When a firewall rule opens it to 0.0.0.0/0, anyone who can reach your VM's external IP can attempt to connect to the database.

Note: GCP firewall rules are evaluated at the VPC network level and apply to instances based on target tags, service accounts, or by applying to all instances in the network. A single overly permissive rule can expose dozens of VMs at once, which is why these rules deserve careful review.


Why it matters

An open MySQL port is not a theoretical risk. Automated scanners sweep the entire IPv4 space constantly, and a freshly exposed 3306 port is typically discovered within minutes. Here is what tends to follow.

Credential brute-forcing

MySQL authenticates with a username and password. Attackers run dictionary and credential-stuffing attacks against exposed instances around the clock. Weak or default passwords (or worse, a root account with no password) fall quickly.

Ransomware and data wiping

There is a well-documented pattern where attackers connect to an exposed database, dump or delete its contents, and leave a ransom note in a new table demanding payment in cryptocurrency to "restore" data that is often already gone. Exposed MySQL and MongoDB instances have been hit by waves of these attacks for years.

Data exfiltration and compliance exposure

If the database holds customer records, a single successful login can mean a full data breach. That triggers notification obligations under regulations like GDPR, CCPA, and HIPAA, plus the reputational and financial fallout that follows.

Warning: Even if MySQL itself requires strong authentication, exposing the port still hands attackers an oracle for fingerprinting your version, probing for known CVEs, and consuming connection slots through denial-of-service attempts. A protected port is one that is not reachable in the first place.


How to fix it

The fix is to stop allowing port 3306 from public sources. You have a few paths depending on whether other rules depend on the same firewall rule.

Step 1: Find the offending rule

List firewall rules that allow port 3306 and inspect their source ranges:

gcloud compute firewall-rules list \
  --filter="allowed[].ports:3306" \
  --format="table(name, network, sourceRanges.list(), allowed[].map().firewall_rule().list(), targetTags.list())"

Look for any rule where sourceRanges contains 0.0.0.0/0 or another broad public range. Note the rule name.

Step 2: Decide on the correct source

Determine where legitimate MySQL traffic actually comes from. In most architectures the answer is internal: application servers in the same VPC, a bastion host, or a peered network. Get that CIDR or tag ready.

Step 3: Restrict the source range

If the rule should exist but only for internal traffic, update its source ranges to your private CIDR or your office VPN egress IP:

gcloud compute firewall-rules update allow-mysql \
  --source-ranges="10.0.0.0/16"

To restrict by which instances the rule applies to, scope it with target tags so only your database VMs are affected:

gcloud compute firewall-rules update allow-mysql \
  --source-ranges="10.0.0.0/16" \
  --target-tags="mysql-server"

Step 4: Delete the rule if it is not needed

Danger: Deleting a firewall rule can cut off legitimate traffic and cause application outages. Confirm that no production service relies on this public path before running the command below. Test against the affected instances first.

gcloud compute firewall-rules delete allow-public-mysql

Step 5: Prefer private connectivity

If you need MySQL reachable from outside the VPC, do not expose 3306 to the internet. Use one of these instead:

  • Cloud SQL with Private IP and private services access, so the database has no public endpoint at all.
  • An IAP-protected bastion or SSH tunnel for administrative access, keeping the database firewall internal-only.
  • A VPN or Cloud Interconnect between your on-prem network and the VPC for cross-site database access.

Tip: For ad hoc admin access without opening any inbound port, use Identity-Aware Proxy TCP forwarding: gcloud compute start-iap-tunnel mysql-vm 3306 --local-host-port=localhost:3306. You then connect to localhost:3306 and the traffic is brokered through IAP with IAM authorization.


How to prevent it from happening again

Manual cleanup fixes today's problem. To stop the rule from reappearing, push enforcement left into your IaC and CI/CD pipeline.

Define firewall rules as code

Manage firewall rules in Terraform so every change is reviewed. A safe rule scopes the source and targets explicitly:

resource "google_compute_firewall" "mysql_internal" {
  name    = "allow-mysql-internal"
  network = google_compute_network.main.name

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

  source_ranges = ["10.0.0.0/16"]
  target_tags   = ["mysql-server"]
  direction     = "INGRESS"
}

Block bad rules in CI

Add a policy-as-code gate so a pull request that opens 3306 to the world fails before it merges. With Open Policy Agent and conftest, a rule like this catches it:

deny[msg] {
  rule := input.resource.google_compute_firewall[name]
  rule.source_ranges[_] == "0.0.0.0/0"
  port := rule.allow[_].ports[_]
  port == "3306"
  msg := sprintf("Firewall '%s' exposes MySQL port 3306 to the public internet", [name])
}

Enforce org-wide guardrails

Use a GCP Organization Policy to restrict which protocols and ports can be opened to public ranges, so a misconfiguration in one project cannot expose a database. Combine this with continuous monitoring in Lensix so any drift, including changes made directly in the console, gets caught quickly.

Tip: Run the conftest check as a required status check on your main branch. A pull request cannot merge until the policy passes, which turns "remember not to expose the database" into something the pipeline enforces automatically.


Best practices

  • Default deny. Build firewall rules from a deny-all baseline and open only what is required, scoped to specific source ranges and target tags.
  • Never use 0.0.0.0/0 for database ports. This applies to 3306, 5432 (PostgreSQL), 27017 (MongoDB), 6379 (Redis), and 1433 (SQL Server). Databases belong on private networks.
  • Prefer managed databases with private IP. Cloud SQL with private services access removes the public attack surface entirely.
  • Use IAP or bastion hosts for admin access rather than opening ports to office IPs that change.
  • Tag your rules by purpose and review them periodically. Broad, untagged rules accumulate over time and are easy to forget.
  • Enforce strong authentication on MySQL regardless. Defense in depth means the network control and the credential control both have to fail before an attacker wins.
  • Log and alert on firewall changes through Cloud Audit Logs so an unexpected public rule triggers an alert rather than sitting unnoticed.

An exposed MySQL port is a quick win for attackers and an easy fix for you. Lock the source range down, move to private connectivity where you can, and let policy-as-code keep the rule from coming back.