DBSheriff connects to your workspace with four service principals, each with only the privileges its job needs. They authenticate with OAuth client secrets (no personal access tokens). You need one running SQL warehouse; Community Edition has no SQL warehouses and cannot be monitored.
Run the commands as a workspace admin with the Databricks CLI
(databricks configure first). Secrets are shown once — paste them into
the DBSheriff Admin Console right away.
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 |
|---|---|---|---|---|
| Unity Catalog metastore attached to the workspace | required | catalog-qualified classification, tags, grants, system tables | no catalog-qualified table names, no tags, no system tables — DBSheriff cannot monitor the workspace | account console |
System tables schemas enabled: system.access, system.query, system.billing (system.compute recommended) |
required | audit events, Query History back-fill, cost (the "system.* grant" is impossible on the trial tier — see §5) | no audit events, no query back-fill, no cost: only the live Query History poll works, failed logins and permission changes are invisible | account admin: enable the schemas, GRANT USE SCHEMA / SELECT to the posture principal |
| A SQL warehouse the product principals may use | required | Query History polling, evidence queries, classification sampling | no polling, no evidence queries — nothing works | §3 |
| OAuth M2M secrets for the service principals | required | token-less authentication | personal access tokens instead; they expire and are tied to a person (PS02) | §2 |
| Query History REST API with statement text (workspace privacy setting) | required | scoring reads statement text; a redacted history is reported as PS27 | statements arrive redacted. If the posture principal can read system.query.history, DBSheriff fills the text in from it a few minutes later (PS27, informational). If it cannot, the text rules are off for the server: no table extraction, no classification of what was read, no honeytoken rule; the metadata rules score, and a privileged ALTER / CREATE / DROP / GRANT is still recognised by the statement class Query History reports (not by what it touched). Posture finding PS83 (a WARNING, shown above the Account Inventory in the console) says so. See section 9 |
workspace admin setting |
| IP access lists (Premium tier) | optional | PS21/PS22 network coverage and the trusted-network mirror | PS21 is raised as a finding (one per workspace, CRITICAL when an admin or service principal other than DBSheriff's own exists, WARNING otherwise) because no network control binds any identity, PS22 does not apply, and the trusted-network mirror stays empty | workspace setting |
| SCIM / account-level user management for the response principal | optional (the lock needs it; the identity list and posture checks PS01 and PS05 also read SCIM) | finding the identity to lock or unlock, and the SCIM-off rung of the lock ladder | every lock and unlock starts by looking the identity up through SCIM; if that fails the lock ends as LOCK_ERROR ("no SCIM User or ServicePrincipal found") and no rung runs, so nothing is removed or deactivated | §4 |
| Response principal's OAuth-secret-deletion step (lock ladder) | known limitation, not yet resolvable from this side | the lock-ladder step that actually denies a service principal — SCIM deactivation and warehouse/catalog permission removal alone do not reliably stop one from minting a fresh OAuth token | the step is off by default (DATABRICKS_LOCK_DELETE_SECRETS=false; deleting the secrets cannot be undone) and is not attempted, so a Databricks lock on a service principal that has no direct warehouse or catalog grant is recorded as LOCK_INEFFECTIVE, with the reason in the lock's detail, and should not be treated as reliably effective. LOCK_INEFFECTIVE also covers a SCIM deactivation that was refused or not applied and access the identity still holds through a group (the detail names the group); an unlock whose SCIM re-activation fails ends as UNLOCK_PARTIAL, with what was restored and what was not, and the lock stays open to retry. Once the step is switched on it can report a permission error in the lock's detail rather than succeeding. The likely fix is an account-owner-granted, account-level permission on the response principal — not yet confirmed to actually close the gap, since granting it and re-testing the lock hasn't been done |
no action to take today unless you choose to switch the step on; see databricks-features.md §4 |
Unity Catalog APPLY TAG on the catalog, plus USE CATALOG and USE SCHEMA, for the response principal |
optional | dbsheriff_sensitivity tag write-back on approval (a table approved, a column approved, or a column approved automatically after a scan) |
approvals are recorded in DBSheriff only; the dbsheriff_sensitivity tag never reaches the catalog, and the column's row in the classification tab shows error (its tooltip is the workspace's refusal, for example does not have USE SCHEMA) instead of tagged |
§5 |
| Principal | What it does | Workspace privileges |
|---|---|---|
dbsheriff_monitor |
reads Query History and warehouse metadata | CAN_MANAGE on every monitored SQL warehouse (with CAN_USE alone Query History shows only its own statements); USE CATALOG + BROWSE on your catalogs, USE SCHEMA on their schemas — metadata only, no SELECT |
dbsheriff_response |
cancels statements, revokes, runs the lock ladder, writes tags | member of the workspace admins group; USE CATALOG, USE SCHEMA and APPLY TAG on the catalogs and schemas where tag write-back is wanted; CAN_USE on a SQL warehouse (the admins group has it on every warehouse) |
dbsheriff_posture |
reads the system tables for posture and evidence | USE SCHEMA + SELECT on system.access, system.query, system.billing (granted by an account admin); CAN_USE on one warehouse to run the hourly import |
dbsheriff_classify |
samples column values for classification (optional) | CAN_USE on a warehouse to run the sampling query; USE CATALOG, USE SCHEMA + SELECT on the schemas to sample |
for sp in dbsheriff_monitor dbsheriff_response dbsheriff_posture dbsheriff_classify; do
databricks service-principals create --display-name $sp --json '{"active": true}'
done
databricks service-principals list --output json | jq -r '.[] | select(.displayName|startswith("dbsheriff")) | "\(.displayName) id=\(.id) client_id=\(.applicationId)"'
# one secret per principal (two years); the secret is printed once
databricks service-principal-secrets-proxy create <numeric id> --json '{"lifetime":"63072000s"}'
The client id (application id) goes in the console's User field and the secret in the Password field of the matching tier.
# monitor needs CAN_MANAGE on every warehouse users run statements on
databricks api patch /api/2.0/permissions/sql/warehouses/<warehouse id> --json \
'{"access_control_list":[{"service_principal_name":"<monitor client id>","permission_level":"CAN_MANAGE"}]}'
# posture needs CAN_USE on one warehouse for the hourly import
databricks api patch /api/2.0/permissions/sql/warehouses/<warehouse id> --json \
'{"access_control_list":[{"service_principal_name":"<posture client id>","permission_level":"CAN_USE"}]}'
# classify needs CAN_USE on a warehouse too -- without it, the Unity
# Catalog grants in §5 give it read access with no way to actually run a
# sampling query against it:
databricks api patch /api/2.0/permissions/sql/warehouses/<warehouse id> --json \
'{"access_control_list":[{"service_principal_name":"<classify client id>","permission_level":"CAN_USE"}]}'
Repeat the monitor grant for every warehouse; DBSheriff's posture checks report any warehouse it cannot fully see.
databricks groups list --output json | jq -r '.[] | select(.displayName=="admins") | .id'
databricks api patch /api/2.0/preview/scim/v2/Groups/<admins group id> --json \
'{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"add","value":{"members":[{"value":"<response numeric id>"}]}}]}'
Admin membership is what lets it cancel any statement, manage tokens,
warehouse permissions and identities for the lock ladder. It is also what
lets DBSheriff see who the admins are: Databricks shows a credential that is
not an admin the admins group with no members and every identity with no
groups. DBSheriff checks the response principal's own group membership at
every import; when admins is missing it logs a warning, records each
identity's admin standing and groups as unknown (a value an earlier run
recorded is kept, not overwritten with "not an admin"; an identity that no
admin run has seen yet is stored with no value at all, and the Account
Inventory lists it as "admin standing unknown" without a privileged mark; the
on-sight fit test refuses a lock of such an account, because it is not known
whether it can be locked (Disable on sight is not offered on Databricks today,
so this changes nothing yet), and the findings PS17 and PS21 do not count it as
an admin and say so)
and reports the stats line identity_admin_view: unknown. The next hourly
import with an admin credential rewrites the values. The posture scan does not read an empty
group as a clean result either: PS05 (admins outside the expected set) is
reported as not checked when the admins group comes back with no members,
which a workspace never has, and PS01 and PS02 are not checked when SCIM or the
token list is refused; the reason is stored with each result (the note column)
and shown in the compliance report and the evidence pack.
-- monitor: metadata only (with HONEYTOKENS=on the read login, the posture principal or else this one, is also granted SELECT on each decoy table
-- DBSheriff plants, and on nothing else)
GRANT USE CATALOG, BROWSE ON CATALOG <catalog> TO `<monitor client id>`;
GRANT USE SCHEMA ON CATALOG <catalog> TO `<monitor client id>`;
-- posture: system tables (account admin)
GRANT USE SCHEMA, SELECT ON SCHEMA system.access TO `<posture client id>`;
GRANT USE SCHEMA, SELECT ON SCHEMA system.query TO `<posture client id>`;
GRANT USE SCHEMA, SELECT ON SCHEMA system.billing TO `<posture client id>`;
-- decoy tables (only with HONEYTOKENS=on, and only when a posture principal is registered): to read a decoy it also needs USE CATALOG and USE SCHEMA,
-- which DBSheriff never grants and none of the lines above give it:
-- GRANT USE CATALOG ON CATALOG <catalog> TO `<posture client id>`;
-- GRANT USE SCHEMA ON SCHEMA <catalog>.<decoy_schema> TO `<posture client id>`;
-- classification (optional): the schemas you want sampled
GRANT USE CATALOG ON CATALOG <catalog> TO `<classify client id>`;
GRANT USE SCHEMA, SELECT ON SCHEMA <catalog>.<schema> TO `<classify client id>`;
-- response: tag write-back (optional, per catalog). Setting a tag needs APPLY TAG on the object AND USE CATALOG / USE SCHEMA on the
-- catalog and schema above it; with APPLY TAG alone the write fails with "does not have USE SCHEMA on Schema ..."
GRANT USE CATALOG ON CATALOG <catalog> TO `<response client id>`;
GRANT USE SCHEMA ON CATALOG <catalog> TO `<response client id>`;
GRANT APPLY TAG ON CATALOG <catalog> TO `<response client id>`;
If your workspace tier does not allow granting the system schemas (the system tables are owned by System user on free and trial tiers), register an account admin's token as the posture credential instead; DBSheriff reports the situation as a posture finding otherwise.
Admin Console → Monitored Servers → Add: engine Databricks, host
dbc-xxxxxxxx-xxxx.cloud.databricks.com, warehouse id (from the
warehouse's Connection Details, the last part of the HTTP path; Query
History is polled for this warehouse only, so statements on other
warehouses are not scored and posture check PS18 reports each of them as
outside the registered filter — a blank field polls every warehouse the
monitor principal can see, but
the hourly import and the Query History text back-fill need a warehouse
id), poll mode
balanced, then the four tiers: monitor (user = client id, password =
secret), kill and lock (the response principal), posture, classification.
Switches: collect system tables, identities and IP access lists (on),
write sensitivity tag back on approval (on by default), then save.
Within the registry reload interval (five minutes) DBSheriff starts
monitoring the server and polls with the monitor credential; a monitor
credential the workspace refuses shows in the producer's log as an error
and raises the A09 MONITOR_TOKEN_REJECTED finding on the first poll. The
refusal is the workspace's own (HTTP 401 invalid_client from the token
endpoint): a wrong or mistyped secret is not hidden by an access token that
another server, or the old secret, had obtained, because a token is kept per
client id and secret. The collector of the hourly import looks for newly
saved servers every minute after the registry reload, so a server saved
between two hourly cycles is imported within a few minutes, not at the next
hour. The hourly import uses
the posture credential (system tables) and the lock credential (identities,
tokens, warehouse permissions, IP access lists); the posture credential is
also used by the text back-fill as soon as the workspace returns redacted
statement text. The kill and classification credentials are first used when
a kill or a classification scan needs them.
Add the four client ids to the server's excluded-users list so DBSheriff's own statements are never scored.
enableTokensConfig) is off or the maximum token lifetime is unset or
above its ceiling (90 days by default, DBX_TOKEN_MAX_LIFETIME_DAYS);
the finding reports the lifetime of the longest-lived existing token.system.query.history meanwhile, provided the posture
principal can read it (section 9).databricks api get "/api/2.0/sql/history/queries?max_results=5" # as the monitor principal: other users' rows appear
databricks api get /api/2.0/preview/scim/v2/Me # as the response principal: 200
A saved server is picked up within the registry reload interval (five minutes), and a monitor credential the workspace refuses shows in the producer's log and as the A09 MONITOR_TOKEN_REJECTED finding. The first hourly import, which uses the posture and lock credentials, shows identities, authentication events and coverage within the hour.
Seen on a Databricks Free Edition workspace (October 2026). Other tiers
differ: where an account admin can grant the system schemas, the second
limit below does not apply. DBSheriff works on the free tier, with the
following holes, and says so in the console instead of staying quiet.
| The tier | What it does to DBSheriff | What you see |
|---|---|---|
Query History returns <REDACTED> as the statement text of every statement, for every caller, the statement's own owner included |
the text of a statement is not available from the REST API | posture finding PS27 (the text is recovered from system.query.history a few minutes late, if it can be read) |
The system.* schemas cannot be granted (User is not an account admin), so dbsheriff_posture cannot read system.query.history |
the text cannot be filled in from the system table either: every text rule is off (what a statement reads or changes: the classified-table and honeytoken rules, R06 R19 R30 R63 R64 R73 and the others that read the statement). Metadata rules still score and cancel: who ran it, when, how long, how many statements in a burst, and the statement class Query History reports, by which a privileged ALTER / CREATE / DROP / GRANT is still recognised (without the table or column it touched) |
PS83 "statement text unavailable" (the text rules are off), PS26 for each system schema, a WARNING in the producer's log once an hour while statements are published without text |
| The workspace may refuse to start compute (HTTP 400 "Cannot create the resource, please try again later") for minutes or hours | every statement DBSheriff runs on the warehouse fails: the text back-fill, the hourly evidence import, classification sampling, tag write-back. Query History polling and statement cancelling are REST calls and keep working | availability finding A07, "monitor compute quota exhausted" (written once per interval while it lasts, resolved by the first statement that runs); the hourly import's stats line says compute: refused |
| No IP access lists | no network control binds any identity | PS21 |
If you register an account admin's credential as the posture credential (section 5), the text comes back and PS83 clears. Until then, treat the Databricks server as monitored on behavior and statement class, not on statement content.