Back to blog
Best PracticesCloud SecurityDatabasesGCPIdentity & Access

MySQL Root Account May Be Insecure on GCP Cloud SQL

Learn why an insecure Cloud SQL MySQL root account is a critical risk, plus step-by-step gcloud and Terraform fixes to lock it down and prevent recurrence.

TL;DR

This check flags Cloud SQL for MySQL instances where the root account is missing a strong password or is reachable from unexpected hosts, leaving the database open to brute force and unauthorized access. Set a strong password for every root host entry, lock down authorized networks, and prefer IAM database authentication.

The MySQL root account is the keys-to-the-kingdom credential. On a fresh Cloud SQL instance it is the first user that exists, and if its password is weak, blank, or the account is accessible from open networks, anyone who can reach the instance has a direct path to read, alter, or wipe every database on it. This Lensix check, sql_mysqlrootpassword, looks for Cloud SQL MySQL instances whose root configuration is likely insecure so you can fix it before someone else finds it.


What this check detects

The check inspects GCP Cloud SQL MySQL instances and raises a finding when the root account appears to be in an insecure state. In practice that covers a few related conditions:

  • A root user that has no password set or a password that was never rotated from a default.
  • A root account that accepts connections from % (any host) rather than being scoped to specific hosts.
  • An instance where root-level access is combined with broad authorizedNetworks entries such as 0.0.0.0/0, which exposes the privileged account to the public internet.

Note: In MySQL, a user is identified by both a username and a host pattern. So root@'localhost' and root@'%' are distinct accounts with separate passwords. Cloud SQL creates a root user when an instance is provisioned, and depending on how you created it, the password may be one you supplied or one Cloud SQL generated. Either way, weak or wildcarded root entries are the risk this check is built to catch.


Why it matters

Cloud SQL instances are frequently exposed beyond their intended audience. A developer opens an authorized network to test from a laptop, a CI runner needs to reach the database, or a public IP is enabled for convenience. Each of those decisions widens the surface that the root account sits behind. When the root password is weak on top of that, the two problems compound into a critical exposure.

Here is how it tends to play out:

  1. An attacker scans GCP IP ranges and finds a Cloud SQL instance listening on port 3306 with a public IP.
  2. They run a credential stuffing or brute force tool against root@'%'. MySQL has no built-in lockout by default, so the attempts continue unthrottled.
  3. A weak password falls in minutes. Now the attacker has SUPER-equivalent privileges across every schema on the instance.
  4. They dump customer data, plant a ransom note in a new table, or drop databases entirely.

The business impact is severe and immediate: data exfiltration, regulatory exposure under GDPR, HIPAA, or PCI DSS, and downtime while you restore from backups. Because root can modify or delete the data, even your backups can be at risk if the attacker has time to work.

Warning: Cloud SQL does not throttle failed login attempts at the database layer the way some managed services do. If a privileged account is reachable from open networks, assume it will be attacked continuously. The only real defenses are a strong credential and a tight network boundary.


How to fix it

There are two things to address: the root password itself, and the network exposure around it. Fix both.

1. Set a strong root password

Use the gcloud CLI to reset the root password to a long, random value. Generate the password with a secure tool rather than typing one by hand.

# Generate a strong password and store it in a variable
NEW_ROOT_PW=$(openssl rand -base64 32)

# Set the root password on the instance
gcloud sql users set-password root \
  --host=% \
  --instance=my-mysql-instance \
  --password="$NEW_ROOT_PW"

echo "New root password: $NEW_ROOT_PW"

Tip: Do not leave the password in shell history or a plaintext file. Push it straight into Secret Manager so your applications read it from one source of truth:

echo -n "$NEW_ROOT_PW" | gcloud secrets create cloudsql-root-password --data-file=-

# Or add a new version if the secret already exists
echo -n "$NEW_ROOT_PW" | gcloud secrets versions add cloudsql-root-password --data-file=-

2. Tighten the host scope

If you have a root@'%' entry that you do not need, remove it so root can only connect from a controlled host. Check the existing users first.

gcloud sql users list --instance=my-mysql-instance

Danger: Deleting a user is irreversible and will immediately break any application or job that authenticates as that account. Confirm nothing depends on root@'%' before you remove it, and have your new credential flow tested first.

# Remove the wildcard root account once you confirm it is unused
gcloud sql users delete root \
  --host=% \
  --instance=my-mysql-instance

3. Lock down authorized networks and prefer private IP

Remove any 0.0.0.0/0 entries from the instance and connect over a private IP or the Cloud SQL Auth Proxy instead of exposing a public endpoint.

# Restrict authorized networks to a specific CIDR
gcloud sql instances patch my-mysql-instance \
  --authorized-networks=203.0.113.10/32

# Or disable the public IP entirely and rely on private IP
gcloud sql instances patch my-mysql-instance \
  --no-assign-ip

Warning: Disabling the public IP or changing authorized networks can interrupt active connections from clients that relied on the old configuration. Roll this out during a maintenance window and update your connection strings to use the Cloud SQL Auth Proxy or private IP first.

4. Prefer IAM database authentication

For human and service access, IAM database authentication lets you grant connection rights through GCP identities and short-lived tokens instead of static passwords. Enable it on the instance and add IAM users.

gcloud sql instances patch my-mysql-instance \
  --database-flags=cloudsql_iam_authentication=on

gcloud sql users create [email protected] \
  --instance=my-mysql-instance \
  --type=cloud_iam_service_account

With IAM auth in place, reserve the root password for emergency break-glass use only, and keep it in Secret Manager.


How to prevent it from happening again

Manual fixes drift. The way to keep root accounts secure is to define the desired state in code and enforce it before instances ever reach production.

Provision instances with Terraform

Define the instance, its private networking, and the root user in Terraform so every new database starts from a secure baseline. Pull the password from Secret Manager rather than hardcoding it.

resource "google_sql_database_instance" "mysql" {
  name             = "my-mysql-instance"
  database_version = "MYSQL_8_0"
  region           = "us-central1"

  settings {
    tier = "db-custom-2-7680"

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.vpc.id
    }

    database_flags {
      name  = "cloudsql_iam_authentication"
      value = "on"
    }
  }

  deletion_protection = true
}

resource "google_sql_user" "root" {
  name     = "root"
  instance = google_sql_database_instance.mysql.name
  host     = "10.0.0.0/8"
  password = data.google_secret_manager_secret_version.root_pw.secret_data
}

Tip: Setting the host on the root user to a private CIDR instead of % means even a misconfigured network rule cannot expose root to the public internet.

Add a policy-as-code gate in CI/CD

Block insecure configurations at pull request time. An Open Policy Agent rule against your Terraform plan can reject any Cloud SQL resource that exposes a public IP or uses a wildcard host.

package cloudsql

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_sql_database_instance"
  resource.change.after.settings[_].ip_configuration[_].ipv4_enabled == true
  msg := sprintf("Cloud SQL instance %q must not enable a public IP", [resource.name])
}

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_sql_user"
  resource.change.after.host == "%"
  msg := sprintf("SQL user %q must not use wildcard host '%%'", [resource.name])
}

Run Lensix continuously

Policy gates only catch what flows through your pipeline. Click-ops changes, console edits, and drift still happen. Keep sql_mysqlrootpassword running on a schedule so an insecure root account gets surfaced within minutes of being introduced, regardless of how it got there.


Best practices

  • Never use root for applications. Create least-privilege users scoped to the specific schemas and operations each service needs. Root is for administration only.
  • Rotate the root password on a schedule. Automate rotation through Secret Manager so a leaked credential has a short useful life.
  • Disable public IP by default. Use private IP plus the Cloud SQL Auth Proxy. A database that cannot be reached from the internet cannot be brute forced from the internet.
  • Enable IAM database authentication. Short-lived IAM tokens beat static passwords for both humans and workloads.
  • Turn on deletion protection and require SSL. Set require_ssl and keep automated backups with point-in-time recovery enabled so a compromise does not become permanent data loss.
  • Audit access. Send Cloud SQL logs to Cloud Logging and alert on repeated failed root logins, which is an early signal that someone is probing the instance.

The root account is worth treating with the same care as your most privileged cloud IAM role, because in the context of a database, that is exactly what it is. A strong password, a tight network boundary, and IAM-based access turn a high-value target into a hardened one.