DBSheriff for BigQuery — creating the service accounts and granting access

DBSheriff connects to your project with four service accounts, each with only the roles its job needs. Run the commands with the gcloud CLI as a project owner (gcloud auth login, then gcloud config set project <project-id>).

0. What must be enabled in the project

The Without it column says what DBSheriff loses when an item is missing, so you can decide what to enable. Nothing optional or recommended stops the product: detection, response and the evidence pack run on the required items alone.

Item Required? What it unlocks Without it How
APIs: BigQuery, Cloud Resource Manager, IAM, Cloud Logging, Data Catalog required jobs, IAM bindings, audit logs, policy tags the corresponding calls fail: no jobs (BigQuery), no IAM posture (IAM / Resource Manager), no audit trail (Logging), no tag write-back (Data Catalog) §1
Data-access audit logs required (always on for BigQuery, cannot be disabled) every finished job with principal, address, key and statistics, 10 to 30 seconds after it ends (Google Cloud Logging's delivery time: 8 to 25 seconds measured) the log is not read: finished jobs come from the jobs list without caller address, user agent or authentication method, and the Availability finding A10 (audit log unreadable) is raised within about ten minutes nothing to do; grant roles/logging.privateLogViewer (§2)
Admin-activity audit logs (_Required bucket) required (always on) IAM, service-account, key, dataset and policy changes — nothing to do
INFORMATION_SCHEMA.JOBS readable by the posture account required hourly job statistics and cost no hourly job statistics or cost; bursts are judged from the audit log only roles/bigquery.jobUser + resourceViewer (§2)
INFORMATION_SCHEMA.PARTITIONS readable by the monitor account required partition-pruning-missed findings (Query Health tab) a full scan of a partitioned table looks the same as a well-pruned one to the Query Health pipeline roles/bigquery.jobUser + dataViewer on dbsheriff-bq-monitor (§2)
Data Catalog taxonomy (created by DBSheriff on first use) optional dbsheriff_sensitivity policy-tag write-back on approval approvals are recorded in DBSheriff only; no policy tag on the columns roles/datacatalog.categoryAdmin on the response account
roles/iam.denyAdmin at folder or organisation optional, and only possible if your project sits under one the IAM deny-policy rung of the lock ladder (Google does not allow this role on a project directly) the lock ladder stops at key disable and role removal; the deny-policy rung is skipped folder/org binding — a standalone project with no organisation or folder parent (gcloud projects describe <project-id> --format="value(parent.type,parent.id)" prints nothing) has no way to grant this at all
VPC Service Controls perimeter (organisation) optional network boundary posture and the trusted-network mirror PS21 (network boundary) raises a WARNING finding that no perimeter bounds the project, and says so when the access policy could not be read; the trusted-network mirror stays empty Access Context Manager Reader for the posture account
Log retention / sink for _Default recommended who-read-what trail beyond 30 days (PS28, PS13) who-read-what evidence older than 30 days is gone; PS28 / PS13 report it §5

1. Enable the APIs

P=<project-id>
gcloud services enable bigquery.googleapis.com cloudresourcemanager.googleapis.com \
  iam.googleapis.com logging.googleapis.com datacatalog.googleapis.com --project=$P

2. The four service accounts

Account What it does Roles on the project
dbsheriff-bq-monitor lists every user's jobs, reads the data-access audit log, runs the Query Health scanner's partition-pruning check roles/bigquery.resourceViewer, roles/logging.viewer, roles/logging.privateLogViewer, roles/bigquery.jobUser, roles/bigquery.dataViewer
dbsheriff-bq-response cancels jobs, revokes dataset access, runs the lock ladder, writes policy tags roles/bigquery.admin, roles/resourcemanager.projectIamAdmin, roles/iam.serviceAccountKeyAdmin, roles/iam.serviceAccountAdmin, roles/datacatalog.categoryAdmin; plus roles/iam.denyAdmin granted at the folder or organisation (Google does not allow it on a project)
dbsheriff-bq-posture reads IAM, service accounts, keys, datasets, log settings and job statistics for posture and evidence roles/bigquery.metadataViewer, roles/bigquery.jobUser, roles/bigquery.resourceViewer, roles/iam.securityReviewer, roles/logging.viewer, roles/logging.privateLogViewer
dbsheriff-classify reads column metadata, samples values, reads policy tags (optional) roles/bigquery.jobUser, roles/datacatalog.viewer, and roles/bigquery.dataViewer on the datasets to scan

Why privateLogViewer: BigQuery's data-access audit entries are "private logs". With logging.viewer alone the API answers 200 with no entries and no error. DBSheriff notices that: when the log has delivered nothing for five minutes while the jobs list shows finished jobs of other accounts, it raises the Availability finding A10, audit log unreadable and reads finished jobs from the jobs list until the log delivers again. roles/iam.securityReviewer already contains the permission this grant adds (logging.privateLogEntries.list), so an account that holds logging.viewer and securityReviewer reads the data-access entries without privateLogViewer. After a missing role is granted, Google can take a minute or two to apply it, and the finding closes when the log delivers its next new entry (on a project nobody is using, with the next job).

gcloud iam service-accounts create dbsheriff-bq-monitor  --project=$P --display-name="DBSheriff monitor"
gcloud iam service-accounts create dbsheriff-bq-response --project=$P --display-name="DBSheriff response"
gcloud iam service-accounts create dbsheriff-bq-posture  --project=$P --display-name="DBSheriff posture"
gcloud iam service-accounts create dbsheriff-classify    --project=$P --display-name="DBSheriff classification"

M=serviceAccount:dbsheriff-bq-monitor@$P.iam.gserviceaccount.com
for r in roles/bigquery.resourceViewer roles/logging.viewer roles/logging.privateLogViewer \
         roles/bigquery.jobUser roles/bigquery.dataViewer; do
  gcloud projects add-iam-policy-binding $P --member=$M --role=$r; done
# jobUser + dataViewer: the Query Health scanner runs a small read-only
# INFORMATION_SCHEMA.PARTITIONS lookup per partitioned table it sees
# referenced in a job, to compute what fraction of the table's partitions
# a query actually touched. Running any query needs bigquery.jobs.create
# (jobUser); reading INFORMATION_SCHEMA.PARTITIONS needs dataViewer.
# Without both, the partition-pruning check (P17) never fires.

R=serviceAccount:dbsheriff-bq-response@$P.iam.gserviceaccount.com
for r in roles/bigquery.admin roles/resourcemanager.projectIamAdmin roles/iam.serviceAccountKeyAdmin \
         roles/iam.serviceAccountAdmin roles/datacatalog.categoryAdmin; do
  gcloud projects add-iam-policy-binding $P --member=$R --role=$r; done
# deny-policy rung of the lock ladder (folder or organisation level):
# gcloud resource-manager folders add-iam-policy-binding <folder-id> --member=$R --role=roles/iam.denyAdmin

S=serviceAccount:dbsheriff-bq-posture@$P.iam.gserviceaccount.com
for r in roles/bigquery.metadataViewer roles/bigquery.jobUser roles/bigquery.resourceViewer \
         roles/iam.securityReviewer roles/logging.viewer roles/logging.privateLogViewer; do
  gcloud projects add-iam-policy-binding $P --member=$S --role=$r; done

C=serviceAccount:dbsheriff-classify@$P.iam.gserviceaccount.com
for r in roles/bigquery.jobUser roles/datacatalog.viewer; do
  gcloud projects add-iam-policy-binding $P --member=$C --role=$r; done
# read access only on the datasets you want scanned (project-wide dataViewer if all of them):
bq add-iam-policy-binding --member=$C --role=roles/bigquery.dataViewer $P:<dataset>
# Dataset-level IAM is allowlist-gated on some project tiers (notably the
# no-billing "sandbox" tier) — if the command above fails with "This
# feature requires allowlisting", grant roles/bigquery.dataViewer at the
# project level instead (the loop above, with dataViewer added to it).

3. Keys

for sa in dbsheriff-bq-monitor dbsheriff-bq-response dbsheriff-bq-posture dbsheriff-classify; do
  gcloud iam service-accounts keys create ./$sa-key.json --iam-account=$sa@$P.iam.gserviceaccount.com
done

The JSON content of each key file (not the path) is pasted into the DBSheriff Admin Console: monitor → Key; posture → the Posture private key PEM (Snowflake) / service account key JSON (BigQuery, Spanner) / … box (under Advanced: Posture credential); classify → the Classification private key PEM (Snowflake) / service account key JSON (BigQuery, Spanner) box (under Advanced: Classification credential). The response account's key goes into Advanced: Kill & Lock credentials, in the Kill private key PEM (Snowflake) / service-account JSON (Spanner, BigQuery) box (and the matching Lock box, which falls back to the kill key when blank). Until it is set, cancel, lock and the policy-tag write-back run with the monitor key, which does not have the response account's roles and cannot do them. Rotate keys at least every 90 days (DBSheriff flags older ones), or run DBSheriff on Google Cloud with Workload Identity and leave the key fields blank (a host deployed from deploy/gcp allows that out of the box; on any other host, a Compute Engine VM you built yourself included, set ALLOW_ADC_SERVERS=true in .env first, or the Console refuses a new server with no key).

4. Register the project in DBSheriff

Admin Console → Monitored Servers → Add: engine BigQuery, Project ID in the database field, Location (for example US) in the schema field, and the keys from §3. The Poll mode box defaults to engine default, which polls at the BigQuery value under Poll Intervals in Config Parameters (2 seconds by default). balanced polls every 60 seconds while quiet and every 10 seconds after activity, economy every 5 minutes, realtime every 2 seconds; custom lets you set the quiet and active intervals. The job and audit-log reads are free API calls, so the mode only protects API quota. Switches: collect audit logs, jobs, IAM and keys (on); write sensitivity policy tag back on approval (off by default). DBSheriff excludes its own four accounts from monitoring automatically.

6. Verify

gcloud projects get-iam-policy $P --format="table(bindings.role,bindings.members)"
gcloud iam service-accounts keys list --iam-account=dbsheriff-bq-monitor@$P.iam.gserviceaccount.com
gcloud logging read 'logName:"data_access" AND protoPayload.serviceName="bigquery.googleapis.com"' \
  --project=$P --limit=3 --freshness=1d      # data-access entries exist (they always do on BigQuery)

The Admin Console has no Test button for a server. DBSheriff connects with the monitor key as soon as it starts monitoring the server (within the registry reload interval, five minutes by default); a key BigQuery refuses shows in the producer's log (Error fetching running jobs). The other keys are used when their job runs: the response key for a cancel or lock, the posture key in the hourly import and the posture scans, the classification key in the classification scans. The first hourly import shows identities, authentication events and coverage within the hour.