Google SecOpsSIEM & SOARIaCCloud SecurityGoogle Cloud

Ingest CloudSQL logs into Google SecOps

Jorge Liauw Calo
Jorge Liauw CaloSecurity Engineer @ Google Cloud
•11 min read
Ingest CloudSQL logs into Google SecOps

Whenever I sit down with customers or fellow security engineers to onboard Google Cloud workloads into Google Security Operations (SecOps), Cloud SQL audit logging almost always sparks the same conversation: “We turned on the audit flag—so why aren’t any SQL queries showing up in SecOps?”

Before we jump into the how, let’s be clear about why we are doing this.

Standard cloud control-plane logs (Admin Activity) tell you if someone created, patched, or deleted a Cloud SQL instance. It doesn’t tell you anything about what is happening inside of your database. If an attacker compromises an application credential, exploits a vulnerability in an internet-facing service, or an insider abuses privileged access, the malicious activity happens at the SQL layer:

  • Failed login brute-forcing or unexpected account connections
  • Rogue CREATE USER, CREATE ROLE, or GRANT statements to establish persistence
  • Suspicious SELECT sweeps exfiltrating sensitive customer or financial tables
  • Unauthorized UPDATE, DELETE, or DROP statements tampering with production data

Without database audit logs flowing into Google SecOps, your SOC cannot detect malicious attempts targeting your data—and during an incident, your responders are left blind without a forensic query trail.

Getting Cloud SQL for MySQL and PostgreSQL audit logs all the way into Google SecOps requires four separate gates to line up. Miss even one, and the logs quietly drop along the way. Here is the short, field-tested walkthrough I share with teams to get both engines working the first time.


⚠️ Important First: Snapshot volatile database stats before changing any Flags!

If you are enabling audit logging during an active investigation, pause for 60 seconds before touching any Cloud SQL flags.

Enabling cloudsql_mysql_audit (MySQL) or cloudsql.enable_pgaudit (PostgreSQL) always triggers a database instance restart. As soon as the instance restarts, volatile in-memory session and statement views are wiped clean.

Run these read-only queries first and save the output locally before changing any flags:

For Cloud SQL MySQL (performance_schema):

-- 1. Snapshot active threads and sessions
SELECT * FROM performance_schema.threads;

-- 2. Snapshot user accounts and connection statistics
SELECT * FROM performance_schema.accounts;

-- 3. Snapshot recent SQL query digests before reboot clears memory
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY LAST_SEEN DESC
LIMIT 500;

For Cloud SQL PostgreSQL (pg_stat_activity & pg_stat_statements):

-- 1. Snapshot active sessions, client IPs, and currently running queries
SELECT pid, usename, datname, client_addr, state, backend_start, query_start, query
FROM pg_stat_activity;

-- 2. Snapshot historical query digests (if pg_stat_statements is loaded)
SELECT userid, dbid, calls, rows, query
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 500;

Step 1: Enable the Cloud SQL audit flags (Console, CLI & Terraform at Scale)

Heads-up: When using the CLI, gcloud sql instances patch --database-flags replaces all existing flags on the instance. Always check your current flags first and include them together so you don’t overwrite production settings!

Option A: Cloud SQL for MySQL (cloudsql_mysql_audit)

Enable the Cloud SQL for MySQL Audit Plugin via the Google Cloud Console (SQL → Instance → Edit → Flags and parameters → cloudsql_mysql_audit = ON) or via gcloud:

Google Cloud Console Cloud SQL Flags and parameters showing cloudsql_mysql_audit set to ON alongside cloudsql_iam_authentication
Enabling the cloudsql_mysql_audit = ON database flag in Google Cloud Console (alongside existing flags such as cloudsql_iam_authentication).
# 1. Check existing flags on your MySQL instance first
gcloud sql instances describe INSTANCE_NAME \
  --format="value(settings.databaseFlags)"

# 2. Enable MySQL auditing (include any existing flags separated by commas)
# Note: This triggers a database restart!
gcloud sql instances patch INSTANCE_NAME \
  --database-flags=cloudsql_mysql_audit=ON

(Tip: For stricter compliance workloads, use FORCE_PLUS_PERMANENT instead of ON so MySQL refuses to start un-audited and blocks anyone from unloading the plugin at runtime. After the restart, run SHOW PLUGINS; inside MySQL to confirm cloudsql_mysql_audit is ACTIVE.)

Option B: Cloud SQL for PostgreSQL (cloudsql.enable_pgaudit & pgaudit.log)

For PostgreSQL, Cloud SQL uses the open-source pgAudit extension. You need two database flags: cloudsql.enable_pgaudit=on to load the extension, and pgaudit.log to specify which SQL classes to record (because pgaudit.log defaults to none):

# 1. Check existing flags on your PostgreSQL instance first
gcloud sql instances describe INSTANCE_NAME \
  --format="value(settings.databaseFlags)"

# 2. Enable pgAudit and log all operations (READ, WRITE, FUNCTION, ROLE, DDL, MISC)
# Optional (PostgreSQL 14+): add cloudsql.pgaudit_mask_literals=on to mask PII literals as $1, $2
# Note: Changing cloudsql.enable_pgaudit triggers a database restart!
gcloud sql instances patch INSTANCE_NAME \
  --database-flags=cloudsql.enable_pgaudit=on,pgaudit.log=all

(Tip: If all is too noisy on a read-heavy PostgreSQL database and you want a comma-separated subset like write,ddl,role, use gcloud’s alternate : delimiter syntax so the commas inside pgaudit.log aren’t parsed as separate flags: --database-flags="^:^cloudsql.enable_pgaudit=on:pgaudit.log=write,ddl,role".)

Enabling auditing across your organization with Terraform

Clicking through the Console works for a single database, but in a real organization with dozens of Cloud SQL instances and projects, you want this standardized via Terraform—across your MySQL fleet, your PostgreSQL fleet, and your Organization IAM Data Access Audit Logs:

# 1a. Enforce cloudsql_mysql_audit across your Cloud SQL MySQL fleet
resource "google_sql_database_instance" "mysql_audited" {
  for_each            = var.mysql_instances
  project             = var.project_id
  name                = each.key
  region              = each.value.region
  database_version    = "MYSQL_8_0"
  deletion_protection = true

  settings {
    tier            = each.value.tier
    disk_autoresize = true # Prevents audit log staging (4 MB/s cap) from filling disk

    database_flags {
      name  = "cloudsql_mysql_audit"
      value = "ON" # Or "FORCE_PLUS_PERMANENT"
    }
  }
}

# 1b. Enforce pgAudit across your Cloud SQL PostgreSQL fleet
resource "google_sql_database_instance" "postgres_audited" {
  for_each            = var.postgres_instances
  project             = var.project_id
  name                = each.key
  region              = each.value.region
  database_version    = "POSTGRES_16"
  deletion_protection = true

  settings {
    tier            = each.value.tier
    disk_autoresize = true # Prevents pgAudit staging (4 MB/s cap) from filling disk

    database_flags {
      name  = "cloudsql.enable_pgaudit"
      value = "on"
    }
    database_flags {
      name  = "pgaudit.log"
      value = "all" # Or "write,ddl,role"
    }
    database_flags {
      name  = "cloudsql.pgaudit_mask_literals"
      value = "on" # Masks string/numeric PII literals in queries as $1, $2 (PG 14+)
    }
  }
}

# 2. Enable Cloud SQL Data Access Audit Logs once across the entire GCP Organization
resource "google_organization_iam_audit_config" "cloudsql_data_access" {
  org_id  = var.org_id
  service = "cloudsql.googleapis.com"

  audit_log_config {
    log_type = "ADMIN_READ"
  }
  audit_log_config {
    log_type = "DATA_READ"
  }
  audit_log_config {
    log_type = "DATA_WRITE"
  }
}

By standardizing database_flags inside your shared Terraform Cloud SQL modules and setting google_organization_iam_audit_config at the organization root (or folder level via google_folder_iam_audit_config), every new Cloud SQL instance inherits database audit logging automatically.


Step 2: Configure auditing inside the database engine

Enabling the Cloud SQL flags alone still won’t emit query logs until you initialize auditing inside the database engine itself.

Inside MySQL: Create an audit rule

Tell MySQL what to audit using Cloud SQL’s built-in stored procedures:

-- Audit ALL users@hosts ('*'), ALL databases ('*'), ALL objects/tables ('*'),
-- ALL operations ('*'), Both successful and failed commands ('B'), and reload immediately (1):
CALL mysql.cloudsql_create_audit_rule('*', '*', '*', '*', 'B', 1, @outval, @outmsg);

-- Verify the rule was created (@outval = 0 and @outmsg = 'OK' means success):
SELECT @outval, @outmsg;

-- View all active audit rules:
CALL mysql.cloudsql_list_audit_rule('*', @outval, @outmsg);
SELECT @outval, @outmsg;

Inside PostgreSQL: Create the pgaudit extension

Cloud SQL does not support creating the pgaudit extension via Terraform—connect once with a psql client (using a database user created via Cloud Console or gcloud) and run:

-- Creates the pgAudit extension across all databases in the Cloud SQL instance:
CREATE EXTENSION IF NOT EXISTS pgaudit;

-- Verify the extension is active and confirm your pgaudit.log scope:
SELECT extname, extversion FROM pg_extension WHERE extname = 'pgaudit';
SHOW pgaudit.log;

Three quick production tips:

  • Disk buffer rate (4 MB/s): Both MySQL and PostgreSQL stage audit logs on the instance disk before uploading to Cloud Logging at up to 4 MB/s—always keep Automatic storage increases (disk_autoresize) enabled.
  • High-QPS read databases: On MySQL, we audited 'B' (Both successful and failed attempts, since failed commands spot brute-force and unauthorized table probing), and you can swap the 4th parameter from '*' to 'dml,ddl,dcl' if SELECT volume is too high. On PostgreSQL, you can tune pgaudit.log=write,ddl,role globally or audit everything for a specific role via ALTER ROLE app_admin SET pgaudit.log = 'all';.
  • MySQL Read Replicas: MySQL audit rules replicate from the primary automatically, but run CALL mysql.cloudsql_reload_audit_rule(1); on each replica to activate them.

Step 3: Enable IAM Data Access audit logs & verify in Cloud Logging

If you didn’t apply the organization-wide Terraform config above, enable Data Access audit logs in the Google Cloud Console (GCP drops data_access logs by default for both MySQL and PostgreSQL):

  1. Go to IAM & Admin → Audit Logs.
  2. Filter for Cloud SQL (cloudsql.googleapis.com) and select it.
  3. In the right-hand panel under Permission types, enable Admin read, Data read, and Data write, then click Save.
Google Cloud Console IAM Audit Logs configuration showing Admin read, Data read, and Data write enabled for Cloud SQL
Enabling Admin read, Data read, and Data write for Cloud SQL under IAM & Admin → Audit Logs.

Next, open Logs Explorer (make sure you have the Private Logs Viewer role, roles/logging.privateLogViewer) and verify your MysqlAuditEntry and PgAuditEntry logs are landing in Cloud Logging:

resource.type="cloudsql_database"
logName:"cloudaudit.googleapis.com%2Fdata_access"
protoPayload.serviceName="cloudsql.googleapis.com"
(protoPayload.request.@type="type.googleapis.com/google.cloud.sql.audit.v1.MysqlAuditEntry"
OR protoPayload.request.@type="type.googleapis.com/google.cloud.sql.audit.v1.PgAuditEntry")

Step 4: Update the Google SecOps Custom Export Filter

Here is the final step that catches almost everyone off guard.

When you open Google Security Operations → Ingestion in the Google Cloud Console, you will likely see Google Cloud logging already set to Enabled:

Google Security Operations Ingestion settings showing Google Cloud logging, Cloud Asset Metadata, and Security Command Center Premium Findings enabled
Even with Google Cloud logging set to Enabled, the default export filter excludes data_access logs. (Keep Cloud Asset Metadata enabled too—it sends GCP_SQL_CONTEXT to enrich your instances!)

Why your SQL queries still don’t show up: Out of the box, Google SecOps’s default export filter only forwards cloudaudit.googleapis.com/activity, cloudaudit.googleapis.com/system_event, and dns.googleapis.com/dns_queries. It intentionally omits cloudaudit.googleapis.com/data_access so organizations don’t accidentally ingest terabytes of routine Cloud Storage or BigQuery reads.

To forward both MySQL (MysqlAuditEntry) and PostgreSQL (PgAuditEntry) database audit logs—here combined alongside the default GCP logs and Model Armor telemetry—without pulling in unrelated data_access noise across GCP, open Custom export filter settings under the Ingestion tab and apply this filter:

log_id("dns.googleapis.com/dns_queries")
OR log_id("cloudaudit.googleapis.com/activity")
OR log_id("cloudaudit.googleapis.com/system_event")
OR log_id("modelarmor.googleapis.com/sanitize_operations")
OR log_id("modelarmor.googleapis.com/templates")
OR (log_id("cloudaudit.googleapis.com/data_access") AND protoPayload.serviceName="modelarmor.googleapis.com")
OR (log_id("cloudaudit.googleapis.com/data_access") AND resource.type="cloudsql_database" AND protoPayload.request.@type="type.googleapis.com/google.cloud.sql.audit.v1.MysqlAuditEntry")
OR (log_id("cloudaudit.googleapis.com/data_access") AND resource.type="cloudsql_database" AND protoPayload.request.@type="type.googleapis.com/google.cloud.sql.audit.v1.PgAuditEntry")

Here is where you configure and save the filter inside Custom export filter settings:

Google Security Operations Custom export filter settings showing scoped Model Armor and Cloud SQL data_access export rules
Configuring scoped data_access export clauses under Google Security Operations → Ingestion → Custom export filter settings.

(Tip: Want to keep SELECT reads in Cloud Logging for short-term debugging, while only exporting logins, writes, DDL, and privilege changes to Google SecOps? Append AND NOT protoPayload.request.cmd="select" inside the MysqlAuditEntry clause and AND NOT protoPayload.request.auditClass="READ" inside the PgAuditEntry clause. You can also estimate log volume beforehand with my Google SecOps Data Ingestion Calculator.)


Step 5: Verify & Hunt inside Google SecOps

Run a test query (including a deliberate failed query or privilege check) on your Cloud SQL MySQL or PostgreSQL database, wait a minute or two, and run this UDM search in Google SecOps → Search (both engines emit cloudsql.instances.query):

metadata.log_type = "GCP_CLOUDAUDIT"
metadata.product_name = "Google Cloud SQL"
metadata.product_event_type = "cloudsql.instances.query"

Inside each event, Google SecOps normalizes the Cloud Audit envelope into UDM alongside the engine-specific SQL forensics details:

  • principal.user.userid — the database user who executed the statement on either MySQL or PostgreSQL (e.g. root, postgres, or appuser)
  • MySQL (MysqlAuditEntry): target.resource.attribute.labels["sql_operation_type"] (select, insert, delete, grant, create_user, connect) and target.resource.attribute.labels["request_query"] (the SQL statement with passwords masked)
  • PostgreSQL (PgAuditEntry): protoPayload.request.auditClass (READ, WRITE, DDL, ROLE), protoPayload.request.command (SELECT, ALTER TABLE, GRANT), protoPayload.request.database, and protoPayload.request.statement (with literals masked as $1, $2 when cloudsql.pgaudit_mask_literals=on is enabled)

Whether your teams run Cloud SQL for MySQL or Cloud SQL for PostgreSQL, your database activity—authenticated users, client connections, executed SQL statements, and privilege changes—is now flowing into Google SecOps so your SOC can detect and investigate malicious database activity in real time! 🛡️

Jorge Liauw Calo
Written By

Jorge Liauw Calo

Security Engineer at Google Cloud (Google Cybershield) with 13+ years of experience in Cybersecurity across highly regulated industries including Semiconductor, Banking, Fintech, and Insurance. Active member of the Google Cloud Community BeNeLux and Google Cloud Security Community Amsterdam.