Google Cloud Spanner — user setup guide

What to create in your GCP project (regional or multi-region, GoogleSQL dialect — DBSheriff's queries are written for GoogleSQL, and databases created with the PostgreSQL interface are not supported) so DBSheriff can monitor it. Unlike every SQL-GRANT engine this product supports, Spanner has no internal user/login/password concept at all — access control is entirely IAM. "Creating an account" here means creating a service account and binding IAM roles to it, not CREATE USER/GRANT.

Every fact and command in this guide was run against a live Spanner instance — this is not written from GCP documentation alone.

0. What must be enabled on the project

Item Required? What it unlocks Without it How
The five service accounts of §1 with the IAM roles of §2 required everything: statements, sessions, kill, lock, posture, classification DBSheriff cannot connect or cannot act §2
spanner.googleapis.com + cloudresourcemanager.googleapis.com + logging.googleapis.com + iam.googleapis.com APIs enabled required the instance/database to exist at all, and the audit-log read below API calls fail gcloud services enable
Cloud Audit Data Access logging (DATA_READ/DATA_WRITE) for spanner.googleapis.com required for auth/statement attribution the only durable, principal-attributed record of who ran what — SPANNER_SYS itself carries no user/principal column at all, live-verified every live/completed query is attributed "unknown", and the governance collector's auth/statement tables stay empty project-level gcloud projects set-iam-policy audit config — off by default, unlike BigQuery
roles/logging.privateLogViewer for the lock service account required for the item above permission to read Data Access log content — a materially different, easy-to-miss grant from roles/logging.viewer, live-verified: the general viewer role gets "Permission denied for all log views" reading these entries session-to-principal attribution and the governance collector's auth/statement audit both stay empty even with Data Access logging on IAM, project-scoped
roles/iam.securityReviewer for the posture service account required for PS09 permission to read the project's IAM audit config, to check whether Data Access logging (the row above) is actually on PS09 cannot evaluate, skipped with a logged warning IAM, project-scoped

The floor. The five service accounts with database-scoped IAM roles gives you live query monitoring, classification, and most posture checks. Add Data Access logging + roles/logging.privateLogViewer (lock) + roles/iam.securityReviewer (posture) for real principal attribution, PS09, and the governance collector's auth/statement audit trail — all project-level, one-time settings, no instance restart.

1. The five service accounts

Account What it does IAM roles
spanner-monitor reads SPANNER_SYS.OLDEST_ACTIVE_QUERIES/QUERY_STATS_TOP_MINUTE, instance metadata roles/spanner.databaseReader (database scope), roles/spanner.viewer (instance scope)
spanner-kill terminates sessions via DeleteSession roles/spanner.databaseUser (database scope)
spanner-lock removes/restores the locked principal's own database-level IAM binding; best-effort session termination; reads Data Access logs for both kill's session cache and the governance collector roles/spanner.databaseAdmin (database scope), roles/logging.privateLogViewer (project scope)
spanner-posture (optional — monitor is used when absent) posture scan's IAM-policy checks (PS03/04/05) borrow the lock credential directly for getIamPolicy — see §2; posture's own role covers PS09 roles/spanner.databaseReader (database scope), roles/iam.securityReviewer (project scope)
spanner-classify reads schema + samples values for classification roles/spanner.databaseReader (database scope)

None holds roles/spanner.databaseAdmin/roles/owner/roles/editor outside its own database-scoped binding. Posture (PS05) flags any other principal holding one of those roles at the database level as an over-broad admin — DBSheriff's own accounts are exempt by service-account naming pattern (spanner-monitor/spanner-kill/spanner-lock/spanner-posture/ spanner-classify).

PROJECT=<your-project-id>
INSTANCE=<your-instance-id>
DATABASE=<your-database-id>

gcloud services enable spanner.googleapis.com cloudresourcemanager.googleapis.com \
  logging.googleapis.com iam.googleapis.com --project="${PROJECT}"

for role in monitor kill lock posture classify; do
  gcloud iam service-accounts create "spanner-${role}" \
    --project="${PROJECT}" --display-name="DBSheriff Spanner ${role}"
done

# Database-scoped roles (every account)
gcloud spanner databases add-iam-policy-binding "${DATABASE}" \
  --instance="${INSTANCE}" --project="${PROJECT}" \
  --member="serviceAccount:spanner-monitor@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/spanner.databaseReader"
gcloud spanner databases add-iam-policy-binding "${DATABASE}" \
  --instance="${INSTANCE}" --project="${PROJECT}" \
  --member="serviceAccount:spanner-kill@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/spanner.databaseUser"
gcloud spanner databases add-iam-policy-binding "${DATABASE}" \
  --instance="${INSTANCE}" --project="${PROJECT}" \
  --member="serviceAccount:spanner-lock@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/spanner.databaseAdmin"
gcloud spanner databases add-iam-policy-binding "${DATABASE}" \
  --instance="${INSTANCE}" --project="${PROJECT}" \
  --member="serviceAccount:spanner-posture@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/spanner.databaseReader"
gcloud spanner databases add-iam-policy-binding "${DATABASE}" \
  --instance="${INSTANCE}" --project="${PROJECT}" \
  --member="serviceAccount:spanner-classify@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/spanner.databaseReader"

# Instance-scoped (monitor only — config snapshot's get_instance())
gcloud spanner instances add-iam-policy-binding "${INSTANCE}" \
  --project="${PROJECT}" \
  --member="serviceAccount:spanner-monitor@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/spanner.viewer"

# Project-scoped (lock and posture only)
gcloud projects add-iam-policy-binding "${PROJECT}" \
  --member="serviceAccount:spanner-lock@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/logging.privateLogViewer"
gcloud projects add-iam-policy-binding "${PROJECT}" \
  --member="serviceAccount:spanner-posture@${PROJECT}.iam.gserviceaccount.com" \
  --role="roles/iam.securityReviewer"

# One JSON key per account — this is what goes into the Admin Console's
# Monitored Servers tab as the monitor/kill/lock/posture/classification
# "service account key" field for this server.
for role in monitor kill lock posture classify; do
  gcloud iam service-accounts keys create "spanner-${role}-key.json" \
    --iam-account="spanner-${role}@${PROJECT}.iam.gserviceaccount.com"
done

# Data Access logging for spanner.googleapis.com — OFF by default, unlike
# BigQuery. Requires reading, editing, and re-applying the project's IAM
# policy (auditConfigs is not a simple additive binding).
gcloud projects get-iam-policy "${PROJECT}" --format=json > /tmp/policy.json
# add to the JSON's top level:
#   "auditConfigs": [{"service": "spanner.googleapis.com",
#     "auditLogConfigs": [{"logType": "DATA_READ"}, {"logType": "DATA_WRITE"}]}]
gcloud projects set-iam-policy "${PROJECT}" /tmp/policy.json

Add the server from the Admin Console's Monitored Servers tab: server_name, engine spanner, host = project ID, dbname = instance ID, warehouse = database ID (Spanner needs three identifiers where most engines need at most two, so these generic fields are repurposed, as BigQuery repurposes dbname for its GCP project ID), and the five service-account key JSON files above as the monitor/kill/lock/posture/classification credentials. env_tier/burst_high_volume_threshold as with every other engine. Add producer-spanner-pool (and, for the full audit/posture pipeline, spanner-governance-collector) to ENABLED_PRODUCERS.

Monitoring of a newly added server starts within about five minutes. The producer and the collector read the list of servers every REGISTRY_RELOAD_INTERVAL_S seconds (300 by default); nothing needs a restart. Until then the producer logs no active Spanner servers (and, when it has others, nothing about the new one) — that is the wait, not an error.

2. Grant reference (why each one is needed)

3. What DBSheriff reads (no setup beyond §1-2, except Data Access logging)

Unlike every SQL-GRANT engine, there is no CREATE AUDIT POLICY/ ALTER SYSTEM step — SPANNER_SYS.OLDEST_ACTIVE_QUERIES (live queries) and SPANNER_SYS.QUERY_STATS_TOP_MINUTE/_10MINUTE/_HOUR (statement statistics aggregated by query shape, not per execution) are always-on system views, readable the moment roles/spanner.databaseReader is granted. Neither carries a principal/user column at all — this is a structural Spanner limitation, not a missing grant.

The only way to attribute a session or a statement to a real identity is Cloud Audit Data Access logging, which is off by default (unlike BigQuery, where it is always on) — enable it per §0/§1 above. Once enabled: CreateSession/DeleteSession entries give session-to-principal mapping (the live producer's own attribution cache, refreshed every 60s by default); ExecuteSql/ExecuteStreamingSql entries carry the literal SQL text, live-verified, and are a genuinely per-execution durable audit trail — finer-grained than QUERY_STATS_TOP_MINUTE's shape-level aggregation.

4. Verification

# service accounts + database-scoped bindings
gcloud spanner databases get-iam-policy "${DATABASE}" --instance="${INSTANCE}" --project="${PROJECT}"

# instance-scoped binding (monitor only)
gcloud spanner instances get-iam-policy "${INSTANCE}" --project="${PROJECT}"

# Data Access logging actually on
gcloud projects get-iam-policy "${PROJECT}" --format=json | grep -A5 spanner.googleapis.com

# lock credential can read Data Access log content (run with GOOGLE_APPLICATION_CREDENTIALS
# pointed at spanner-lock-key.json) — a "Permission denied for all log views" error here
# means roles/logging.privateLogViewer is missing or not yet propagated (~1-2 min after grant)
gcloud logging read 'protoPayload.methodName="google.spanner.v1.Spanner.CreateSession"' \
  --project="${PROJECT}" --limit=1 --freshness=1h

Expected producer log line on first successful poll: [<server>][poll] nothing new next poll in <N>s (or a captured statement, logged as [<server>][RISK] or [<server>][SAFE], if one was running), with user= resolving to a real service-account email rather than unknown once a session has existed long enough for its CreateSession event to be ingested (observed live: under 30 seconds, typically) — an attribution that never resolves past unknown at all means either Data Access logging is off, or the lock credential's roles/logging. privateLogViewer grant is missing.

5. Known limitations