Back to blog
Cloud SecurityDatabasesGCPMonitoring & LoggingOperations & Compliance

PostgreSQL Logs All Statements on Cloud SQL: Why It's Risky and How to Fix It

Cloud SQL PostgreSQL instances with log_statement=all leak sensitive query data into Cloud Logging. Learn the risk and how to fix it with gcloud and Terraform.

TL;DR

This check flags Cloud SQL for PostgreSQL instances where log_statement is set to all, which writes every query, including ones containing passwords, tokens, and PII, into Cloud Logging. Set log_statement to ddl (or none) unless you have a short-lived reason to capture everything.

Statement logging is one of those settings that feels harmless until you read what actually lands in your logs. When a Cloud SQL PostgreSQL instance has log_statement set to all, every single SQL statement the server executes gets written to the PostgreSQL log and forwarded to Cloud Logging. That includes INSERT and UPDATE statements with raw values, ad-hoc queries run by an engineer during an incident, and anything passed inline rather than as a bound parameter.

The Lensix check sql_pgminstatement in the sql_checks module looks for exactly this configuration and raises a finding so you can decide whether the exposure is justified.


What this check detects

Cloud SQL exposes a database flag called log_statement. It controls which categories of SQL statements get logged. There are four valid values:

  • none — no statements are logged based on this flag
  • ddl — logs schema changes like CREATE, ALTER, and DROP
  • mod — logs DDL plus data-modifying statements (INSERT, UPDATE, DELETE)
  • all — logs every statement, including plain SELECT queries

The check fires when an instance has log_statement set to all. At that level, the database records the full text of every query it runs, and those query strings frequently contain sensitive values directly in the SQL.

Note: log_statement = all is different from log_min_duration_statement. The former logs queries by category regardless of speed, while the latter logs only queries slower than a threshold. People often confuse the two and end up logging far more than they intended.


Why it matters

The core problem is that SQL text is not a safe place to store secrets, and logging it copies those secrets somewhere new. A login flow that runs UPDATE users SET password_hash = '...' WHERE email = '[email protected]' writes the hash and the email into your logs. A query that filters on a national ID, a credit card token, or a session value leaks that value too. Once it is in Cloud Logging, it spreads to log sinks, BigQuery exports, and any SIEM you forward to.

This creates a few concrete risks:

  • Widened blast radius. Anyone with log read access (roles/logging.viewer) can now read data they were never granted access to in the database itself. Log readers are usually a much broader group than database users.
  • Compliance exposure. GDPR, HIPAA, and PCI DSS all treat logs as in-scope when they contain regulated data. Statement logging quietly pulls personal data and cardholder data into systems that may not have the same controls as your database.
  • Long retention of sensitive data. Database rows get updated and deleted, but logs are immutable and often retained for months or years. A value you deleted from the database may live on in a log export indefinitely.
  • Performance and cost. Logging every statement on a busy instance generates enormous log volume, adds write overhead, and can balloon your Cloud Logging bill.

Warning: If an attacker compromises a low-privilege account that can read logs but not the database, full statement logging effectively hands them a query history with embedded data. Logs become a shortcut around your database access controls.


How to fix it

The fix is to set log_statement to a value that captures what you actually need without dumping query payloads. For most workloads ddl is the right baseline. It records schema changes (which are useful for auditing) without logging the data flowing through your tables.

Step 1: Check the current value

gcloud sql instances describe INSTANCE_NAME \
  --project PROJECT_ID \
  --format="value(settings.databaseFlags)"

Look for a flag named log_statement with value all.

Step 2: Update the flag

Warning: Changing database flags on Cloud SQL may require a brief instance restart depending on the flag. log_statement is generally applied without a restart, but verify in a non-production environment first and run the change during a maintenance window if you cannot tolerate any disruption.

Note that --database-flags replaces the entire set of flags, so include any others you have already configured in the same command.

gcloud sql instances patch INSTANCE_NAME \
  --project PROJECT_ID \
  --database-flags log_statement=ddl

If you only want schema-change auditing and no statement logging at all, use none:

gcloud sql instances patch INSTANCE_NAME \
  --project PROJECT_ID \
  --database-flags log_statement=none

Step 3: Verify

gcloud sql instances describe INSTANCE_NAME \
  --project PROJECT_ID \
  --format="json(settings.databaseFlags)"

Confirm the value now reads ddl or none.

Console steps

  1. Open SQL in the Google Cloud Console and select the instance.
  2. Click Edit.
  3. Expand Flags.
  4. Find log_statement, change its value from all to ddl, and click Done.
  5. Click Save and confirm.

Tip: If you genuinely need full statement logging for a debugging session, enable it temporarily, capture what you need, and revert. Leaving it on permanently is what turns a useful diagnostic into a standing data-leak risk.


Fixing it with infrastructure as code

Setting the flag in code keeps the value pinned and prevents drift. Here is the Terraform form using the google_sql_database_instance resource:

resource "google_sql_database_instance" "postgres" {
  name             = "my-postgres-instance"
  database_version = "POSTGRES_15"
  region           = "us-central1"

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

    database_flags {
      name  = "log_statement"
      value = "ddl"
    }

    # Keep useful, lower-risk logging on instead
    database_flags {
      name  = "log_min_duration_statement"
      value = "1000" # log queries slower than 1s, no payload dump
    }
  }
}

The same idea in a Cloud SQL config managed by gcloud or the API: set databaseFlags explicitly rather than relying on whatever was clicked in the console.

Tip: Use log_min_duration_statement for performance troubleshooting instead of log_statement = all. It captures slow queries (the ones you actually care about) without logging every fast query that runs thousands of times a second.


How to prevent it from happening again

One fix is good. Stopping the setting from creeping back in is better. A few layers help here.

Policy as code in CI/CD

If your Cloud SQL instances are defined in Terraform, add an OPA or Conftest policy that rejects any plan setting log_statement to all:

package cloudsql

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_sql_database_instance"
  flag := resource.change.after.settings[_].database_flags[_]
  flag.name == "log_statement"
  flag.value == "all"
  msg := sprintf("Cloud SQL instance %s sets log_statement=all, which logs sensitive query data", [resource.address])
}

Wire that into your pipeline so a plan fails before it ever reaches apply.

Organization policy and continuous checks

For instances created outside Terraform, you need a control that watches running infrastructure. Run the Lensix sql_pgminstatement check on a schedule so any instance that drifts to log_statement = all surfaces as a finding within hours, not at the next audit.

Note: Console edits and emergency changes during incidents are the most common source of drift. People flip log_statement to all to debug a problem and forget to revert. Continuous scanning catches exactly this pattern.


Best practices

  • Default to ddl. It gives you a useful audit trail of schema changes without logging row data. Use none if you have a separate audit mechanism.
  • Never put secrets in SQL text. Use bound parameters in your application code so credentials and tokens are not embedded in query strings. This reduces leakage even if logging is misconfigured.
  • Restrict log access. Treat Cloud Logging as sensitive. Grant roles/logging.viewer narrowly and review who has it.
  • Use redaction on log sinks. If you must forward logs that may contain data, apply Cloud DLP or transformation rules to mask sensitive patterns before they reach downstream stores.
  • Prefer targeted diagnostics. When you need query visibility, reach for log_min_duration_statement or auto_explain rather than logging everything.
  • Set log retention deliberately. Long retention plus statement logging is a bad pairing. Match retention to what compliance actually requires.

Statement logging is a powerful diagnostic tool, but all is almost never the right standing configuration. Set a sane default, pin it in code, and let a continuous check catch the day someone flips it during a 2 a.m. incident and forgets to flip it back.