DBSheriff connects with its own dedicated accounts — never your
application's credentials — each limited to what its job needs (the
last section says exactly what each one can do if its password leaks). Setup has two parts: server prerequisites
(performance_schema and its statement-history consumer, optionally an
audit trail) and the accounts themselves. Three scripts automate all
of it; MySQL 8.0.22+ is required (8.4 / 9.x recommended), as is Aurora
MySQL 3.
On an installed host there is no scripts/ folder, so read every scripts/<name>.py
below as sudo /opt/dbsheriff/current/deploy/onprem/stack.sh tool <name> ..., for example
sudo /opt/dbsheriff/current/deploy/onprem/stack.sh tool setup_dbsheriff mysql db1.acme.internal. The scripts/ form
is for a source checkout. The details are under "How to start the scripts" in
Before you install: creating DBSheriff's database users.
| Script | What it does | Example |
|---|---|---|
setup_dbsheriff.py |
Recommended. One command: checks/enables prerequisites, then creates the accounts. | scripts/setup_dbsheriff.py mysql db1.acme.internal |
install_prereqs.py |
Prerequisites only — run this alone if you want to review each step separately before creating any accounts. | scripts/install_prereqs.py mysql db1.acme.internal |
create_reset_dbuser.py |
Accounts only — creates or resets them once prerequisites are already in place. | scripts/create_reset_dbuser.py mysql db1.acme.internal |
All three are interactive (they ask which platform, which accounts, and
for database credentials), connect only as a database user — never a
cloud credential — and never restart the database server. If a
change needs a restart to take effect, they tell you the exact command
for your platform and stop there; you restart on your own schedule and
re-run to finish. Add --dry-run to any of them to preview without
changing anything. To create fewer than all six: pass
--roles=monitor,kill to setup_dbsheriff.py, or answer
create_reset_dbuser.py's own account-selection prompt with the
numbers/names you want (or all).
Add --check to any of them for a read-only status report instead: what's
already there, what's missing, and the exact command to fix each gap —
nothing is ever written, not even if you say yes to something (there's no
prompt to say yes to). Exits with a non-zero status if anything's
missing, so it's usable in a script.
The account's host part ('user'@'host') is auto-discovered from the
connection the script itself makes — run it from the machine (or one
representative machine) DBSheriff monitors from, never 'user'@'%'. If
DBSheriff connects from more than one address, widen the host to a subnet
by hand afterward.
| Account | Required? | Unlocks |
|---|---|---|
dbsheriff_monitor |
Required | Live and completed statement monitoring, process list, table/session counters |
dbsheriff_kill |
Optional | Terminating a malicious statement or session |
dbsheriff_lock |
Optional | Locking a compromised account |
dbsheriff_posture |
Optional | A dedicated credential for posture checks + hourly governance collection (otherwise reuses dbsheriff_monitor) |
dbsheriff_classify |
Optional | Sampling column values to classify sensitive data |
dbsheriff_plan |
Optional, recommended | The login DBSheriff's EXPLAIN of a captured statement can run under: SELECT on the schemas and nothing else — no PROCESS, FILE, write or DDL right. Created by setup_dbsheriff.py / create_reset_dbuser.py for a new server and recorded in the registry (plan_db_user); nothing uses it until the EXPLAIN path is switched to it |
setup_dbsheriff.py, create_reset_dbuser.py, install_prereqs.py and onboard_target.py connect as your database administrator, and the administrator password and every new account's password cross that connection. They therefore verify the server's certificate by default (--ssl-mode verify-full). Give the CA that signed it with --ca-file <bundle> (Amazon RDS / Aurora: the RDS CA bundle for your region; a private or self-signed server: its CA or its own certificate); without --ca-file the system trust store is used. A refused certificate stops the run and the message names the flag. --ssl-mode require encrypts without verifying (self-signed targets, an explicit opt-down that prints a warning); --ssl-mode disable --allow-plaintext sends everything in clear text and is for a trusted private link only.
The accounts' host part is the address DBSheriff connects from, read from the administrator connection. If that cannot be read (a proxy or NAT in between) the scripts stop and ask for --client-host <address or subnet pattern such as 10.0.%>; they never create an account for '%'. A generated password is never printed: it is stored in the registry (encrypted) when the server is registered, and otherwise written to a new file with mode 600 (--credentials-file, default ./dbsheriff-credentials-<server>-<time>.txt) whose path is shown; move it into your password manager and delete the file. Account names must match [A-Za-z][A-Za-z0-9_]*.
The account statements and the monitor/posture/classify grants are
identical everywhere (the classify account's SELECT on
performance_schema.threads below too: one statement on every platform). The kill and lock accounts' session-kill
grant is the one place platform matters — MySQL's GRANT takes exactly
one object per statement, so grant each mysql.* table separately
(a comma-separated list, valid in PostgreSQL, is a MySQL syntax error).
CREATE USER 'dbsheriff_monitor'@'10.0.0.5' IDENTIFIED BY '<password>'; -- the DBSheriff host's address, never '%'
GRANT PROCESS ON *.* TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON performance_schema.* TO 'dbsheriff_monitor'@'10.0.0.5';
-- mysql.user holds the password hashes (authentication_string), so it is granted BY COLUMN, never as a table:
-- exactly the columns the account inventory and the posture checks read (the list is MYSQL_USER_COLUMNS in
-- the provisioning tool, checked against both queries by a test)
GRANT SELECT (User, Host, plugin, account_locked, password_expired, ssl_type, max_questions, max_user_connections,
password_last_changed, password_lifetime, User_attributes,
Select_priv, Insert_priv, Update_priv, Delete_priv, Create_priv, Drop_priv, Reload_priv, Shutdown_priv,
Process_priv, File_priv, Grant_priv, References_priv, Index_priv, Alter_priv, Show_db_priv, Super_priv,
Create_tmp_table_priv, Lock_tables_priv, Execute_priv, Repl_slave_priv, Repl_client_priv, Create_view_priv,
Show_view_priv, Create_routine_priv, Alter_routine_priv, Create_user_priv, Event_priv, Trigger_priv,
Create_tablespace_priv, Create_role_priv, Drop_role_priv)
ON mysql.user TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON mysql.global_grants TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON mysql.role_edges TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON mysql.func TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON mysql.component TO 'dbsheriff_monitor'@'10.0.0.5';
-- optional, each as its own statement (which database roles hold access to the classified tables; without them
-- DBSheriff logs one warning and leaves that part empty). mysql.columns_priv is read only when the entitlement
-- collection (ENTITLEMENTS=on) is switched on:
GRANT SELECT ON mysql.tables_priv TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON mysql.db TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON mysql.columns_priv TO 'dbsheriff_monitor'@'10.0.0.5';
-- dbsheriff_posture: the same account statement + the same grants above.
-- (User_attributes carries the failed-login-lockout setting for check PS14, and holds a password hash only for an
-- account given ALTER USER ... RETAIN CURRENT PASSWORD. It is read in the same query as the other mysql.user columns,
-- so if any column of that query is not granted, PS01, PS02, PS05, PS14, PS36, PS37, PS38, PS40 and PS41 all
-- report NOT_CHECKED.)
-- If your platform refuses a column-level grant on mysql.user, the scripts skip it and say so; account inventory and
-- the account posture checks report NOT_CHECKED until it is granted. The table-level grant is not the fallback.
CREATE USER 'dbsheriff_plan'@'10.0.0.5' IDENTIFIED BY '<password>'; -- the EXPLAIN login: SELECT only
-- plan needs no server-level grant — only the per-schema SELECT below.
CREATE USER 'dbsheriff_classify'@'10.0.0.5' IDENTIFIED BY '<password>';
-- classify's one server-level grant (LIVE-283): the single performance_schema table it reads to recognise its own session's thread, so
-- that DBSheriff's classification sessions are not scored as a customer's `unknown`. No other performance_schema table, no PROCESS, no mysql.*.
-- It shows this login the live process list of the server (account, host and the statement each session is running), see the table of what a
-- leaked password can do. Beyond it, only the per-schema SELECT below.
GRANT SELECT ON performance_schema.threads TO 'dbsheriff_classify'@'10.0.0.5';
An existing server needs this one statement (it was not part of the
classify account before): run it as the administrator, or run
onboard_target.py / create_reset_dbuser.py / setup_dbsheriff.py again
and answer yes to the reset of dbsheriff_classify, which repeats exactly this
grant (a reset repeats no other server-level privilege). Until then the
classification scan works as before, and its sessions are recognised only
where the server has PS_CURRENT_THREAD_ID() (MySQL 8.0.16 and later). MariaDB
is left as it was: the provisioning does not depend on the grant there, a
server that refuses the statement gets the account without it (the scripts skip
it with a note), and, having no PS_CURRENT_THREAD_ID(), its classification
sessions stay unmarked as before.
Kill and lock's session-kill grant — three platform variants, all
fully working, none a compromise. CONNECTION_ADMIN is DBSheriff's
default; RDS/Aurora and Azure Flexible Server can never grant it (or
SUPER) to another account, so each provides its own substitute pair of
EXECUTE-grantable stored procedures instead — same idea, different
names.
Onprem / GCP Cloud SQL:
CREATE USER 'dbsheriff_kill'@'10.0.0.5' IDENTIFIED BY '<password>';
GRANT CONNECTION_ADMIN, PROCESS ON *.* TO 'dbsheriff_kill'@'10.0.0.5';
GRANT SELECT ON performance_schema.* TO 'dbsheriff_kill'@'10.0.0.5';
CREATE USER 'dbsheriff_lock'@'10.0.0.5' IDENTIFIED BY '<password>';
GRANT CREATE USER ON *.* TO 'dbsheriff_lock'@'10.0.0.5';
GRANT CONNECTION_ADMIN ON *.* TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON mysql.user TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON mysql.global_grants TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON performance_schema.* TO 'dbsheriff_lock'@'10.0.0.5';
Amazon RDS for MySQL / Aurora MySQL — mysql.rds_kill() /
rds_kill_query():
CREATE USER 'dbsheriff_kill'@'10.0.0.5' IDENTIFIED BY '<password>';
GRANT PROCESS ON *.* TO 'dbsheriff_kill'@'10.0.0.5';
GRANT SELECT ON performance_schema.* TO 'dbsheriff_kill'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.rds_kill TO 'dbsheriff_kill'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.rds_kill_query TO 'dbsheriff_kill'@'10.0.0.5';
CREATE USER 'dbsheriff_lock'@'10.0.0.5' IDENTIFIED BY '<password>';
GRANT CREATE USER ON *.* TO 'dbsheriff_lock'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.rds_kill TO 'dbsheriff_lock'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.rds_kill_query TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON mysql.user TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON mysql.global_grants TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON performance_schema.* TO 'dbsheriff_lock'@'10.0.0.5';
Azure Database for MySQL Flexible Server — its own equivalent,
mysql.az_kill() / az_kill_query() (undocumented by Microsoft, found
live in information_schema.ROUTINES; confirmed live: CALL
mysql.az_kill(<id>) terminated another account's connection):
CREATE USER 'dbsheriff_kill'@'10.0.0.5' IDENTIFIED BY '<password>';
GRANT PROCESS ON *.* TO 'dbsheriff_kill'@'10.0.0.5';
GRANT SELECT ON performance_schema.* TO 'dbsheriff_kill'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.az_kill TO 'dbsheriff_kill'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.az_kill_query TO 'dbsheriff_kill'@'10.0.0.5';
CREATE USER 'dbsheriff_lock'@'10.0.0.5' IDENTIFIED BY '<password>';
GRANT CREATE USER ON *.* TO 'dbsheriff_lock'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.az_kill TO 'dbsheriff_lock'@'10.0.0.5';
GRANT EXECUTE ON PROCEDURE mysql.az_kill_query TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON mysql.user TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON mysql.global_grants TO 'dbsheriff_lock'@'10.0.0.5';
GRANT SELECT ON performance_schema.* TO 'dbsheriff_lock'@'10.0.0.5';
Then, per schema to monitor:
-- A schema name is a PATTERN in a database-level GRANT: escape \ % and _ (`my\_db`), or `my_db` also grants on `myxdb`
-- and a schema named `%` grants on every schema. The scripts do this for every schema they discover.
-- EXCEPT where SELECT @@partial_revokes is 1 (Cloud SQL turns it on): the name is then taken literally, `my\_db` names
-- a schema that does not exist and the account sees no table of `my_db`. Write `my_db` as it is on such a server;
-- the scripts read the variable first and do the same.
GRANT USAGE ON `finance`.* TO 'dbsheriff_monitor'@'10.0.0.5'; -- metadata only, never a row of your data
GRANT USAGE ON `finance`.* TO 'dbsheriff_posture'@'10.0.0.5';
GRANT SELECT ON `finance`.* TO 'dbsheriff_classify'@'10.0.0.5'; -- the scan samples values
GRANT SELECT ON `finance`.* TO 'dbsheriff_plan'@'10.0.0.5'; -- the EXPLAIN login: SELECT only
ANALYZE TABLE finance.customers UPDATE HISTOGRAM ON email, ssn, card_number WITH 64 BUCKETS; -- optional evidence
-- optional write-back target (mysql_tag_writeback): one row per approved column
CREATE DATABASE IF NOT EXISTS dbsheriff;
GRANT SELECT, INSERT, UPDATE, CREATE ON dbsheriff.* TO 'dbsheriff_lock'@'10.0.0.5';
USAGE for monitor/posture really is correct and sufficient — they never
read a row of your data, only performance_schema / information_schema
/ SHOW ....
DBSheriff needs performance_schema on (required) with its
events_statements_history_long consumer enabled — without either, the
statement history DBSheriff scores from doesn't exist. How you enable
these, and whether an audit trail can be added on top, is different on
every platform — jump to yours.
scripts/setup_dbsheriff.py mysql <hostname> # choose "onprem" when asked
Stages performance_schema with a normal SQL statement — no file access
needed at all:
SET PERSIST_ONLY performance_schema = ON; -- MySQL 8.0+; writes to mysqld-auto.cnf, not my.cnf itself
Restart to apply (performance_schema only takes effect at startup):
sudo systemctl restart mysql # Debian/Ubuntu
sudo systemctl restart mysqld # RHEL/Rocky/CentOS
brew services restart mysql # Homebrew
net stop MySQL80 && net start MySQL80 # Windows, elevated
Then enable the consumer (fully dynamic, no restart, resets to its startup value on every restart so this is worth automating rather than remembering):
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME = 'events_statements_history_long';
Recommended, not required — my.cnf (paths below) plus a restart unless
noted:
| Install | Path |
|---|---|
| Debian/Ubuntu | /etc/mysql/mysql.conf.d/mysqld.cnf |
| RHEL/Rocky/CentOS | /etc/my.cnf.d/mysql-server.cnf (or /etc/my.cnf) |
| Homebrew, Apple Silicon / Intel | /opt/homebrew/etc/my.cnf / /usr/local/etc/my.cnf |
| Windows | C:\ProgramData\MySQL\MySQL Server 8.0\my.ini |
performance_schema_events_statements_history_long_size = 20000 # ring size, default is smaller
performance_schema_max_sql_text_length = 4096
max_digest_length = 4096
log_error_verbosity = 3 # SET PERSIST log_error_verbosity = 3; — dynamic, no restart
log_bin = ON
require_secure_transport = ON # when every client supports TLS
local_infile = OFF
secure_file_priv = /var/lib/mysql-files/
Audit trail — the general query log as a table, the source that needs no log export, cloud permission or file mount:
SET PERSIST log_output = 'TABLE'; -- dynamic — sets it live AND persists across restarts, one statement
SET PERSIST general_log = 'ON'; -- every statement, including sub-250ms sessions monitor never gets an identity for otherwise
Size it and rotate it (TRUNCATE mysql.general_log from a job), and grant
read access to the monitor account (the producer uses the table to resolve who a
session is) and to the posture account (the governance collector reads it as the
audit source; skip the second statement if you created no posture account):
GRANT SELECT ON mysql.general_log TO 'dbsheriff_monitor'@'10.0.0.5';
GRANT SELECT ON mysql.general_log TO 'dbsheriff_posture'@'10.0.0.5';
RDS/Aurora's MARIADB_AUDIT_PLUGIN / Advanced Auditing and MySQL
Enterprise Audit are richer alternatives, and DBSheriff reads them too:
from the CloudWatch export (logs:FilterLogEvents on the log group), from
the RDS log API (rds:DescribeDBLogFiles and
rds:DownloadDBLogFilePortion on the instance), or from an Enterprise /
Percona JSON or MariaDB CSV audit file mounted into the governance
collector. The source is the server's mysql_audit_source (default
auto: the general_log table when log_output is TABLE and
general_log is ON, else the log group, else the RDS instance id, else
the file path), with mysql_audit_log_group and mysql_audit_file_path
alongside it; the Monitored Servers form has a field for each (MySQL audit
source, MySQL audit log group, MySQL audit file; also API fields of
the same names), shown for MySQL and Aurora MySQL. The log group and the file are read with the collector's own rights (the host's cloud identity, the collector container's file system), so only an unrestricted approver can set them (the file as an absolute path inside the collector container); the AWS region and RDS instance id name the instance the rdsapi read and the capacity refresh go to, with the same cloud identity, so they are an unrestricted approver's to set or change as well (an approver whose scope is limited to some servers or engines is refused with a 403; such an approver can still save a server with the values already stored, or with both left blank). general_log is the one
that works everywhere without any of that.
scripts/setup_dbsheriff.py mysql <hostname> # choose "aws"; asks for the RDS instance/cluster id
Stages the change through your own authenticated aws CLI session — no
cloud credential is asked for. From a source checkout, with the aws CLI
installed and signed in, the script stages the change itself. Through
stack.sh tool on an installed host the container has no cloud CLI, so the
script says it could not stage the change and sends you here: run the
commands under "By hand instead of the script" below yourself. It refuses only if the instance is still on
AWS's uneditable default parameter group; create and attach a custom one
first, then re-run:
aws rds create-db-parameter-group --db-parameter-group-name dbsheriff-mysql80 \
--db-parameter-group-family mysql8.0 --description "DBSheriff settings"
aws rds modify-db-instance --db-instance-identifier <instance> \
--db-parameter-group-name dbsheriff-mysql80 --apply-immediately
By hand instead of the script — performance_schema is static, so it
needs a reboot:
aws rds modify-db-parameter-group --db-parameter-group-name dbsheriff-mysql80 \
--parameters "ParameterName=performance_schema,ParameterValue=1,ApplyMethod=pending-reboot"
aws rds reboot-db-instance --db-instance-identifier <instance>
Aurora MySQL: set the consumer as a parameter too, not only with the
UPDATE below. The UPDATE is a runtime setting and every restart
resets it (a stop/start, the automatic restart of an instance AWS started
again after seven days stopped, maintenance): the statement history then
stays empty, completed statements are no longer captured (the live process
list still is), and the producer log says events_statements_history_long
is empty. Aurora's cluster parameter group has the persistent setting,
performance_schema_consumer_events_statements_history_long, a static
parameter like performance_schema itself, so the script stages both in one
change and they take effect together at the next reboot:
aws rds modify-db-cluster-parameter-group --db-cluster-parameter-group-name <cluster-parameter-group> \
--parameters "ParameterName=performance_schema,ParameterValue=1,ApplyMethod=pending-reboot" \
"ParameterName=performance_schema_consumer_events_statements_history_long,ParameterValue=1,ApplyMethod=pending-reboot"
aws rds reboot-db-instance --db-instance-identifier <writer-instance>
RDS for MySQL has no such parameter (the mysql8.0 and mysql8.4
parameter groups list none), so the runtime UPDATE is all there is: re-run
scripts/setup_dbsheriff.py mysql <hostname> (it finds the consumer off and
enables it again) after every restart of the instance.
Enable the consumer now, without waiting for a reboot (same statement as onprem — dynamic, no reboot; the script runs it for you):
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME = 'events_statements_history_long';
The RDS/Aurora master user cannot SET GLOBAL general_log/log_output
directly — confirmed against AWS's own dynamic-privileges reference, the
master user is denied SYSTEM_VARIABLES_ADMIN on every RDS for MySQL
version. general_log/log_output are dynamic parameters, though, so the
same parameter group takes effect immediately, no reboot:
aws rds modify-db-parameter-group --db-parameter-group-name dbsheriff-mysql80 \
--parameters "ParameterName=general_log,ParameterValue=1,ApplyMethod=immediate" \
"ParameterName=log_output,ParameterValue=TABLE,ApplyMethod=immediate"
Kill: CONNECTION_ADMIN cannot be granted on RDS or Aurora MySQL and
SUPER never. DBSheriff detects a managed instance and calls
mysql.rds_kill() / mysql.rds_kill_query() instead — see "Creating the
accounts" above. Verified live: a session was ended through
CALL mysql.rds_kill(<id>) in under 100 ms.
Failed logins come from performance_schema.error_log, same as
self-managed (log_error_verbosity = 3 in the parameter group, also
ApplyMethod=immediate).
Networking: the instance must be reachable from the DBSheriff host — same VPC, or publicly accessible in a DB subnet group of public subnets with the host's address in the security group.
Aurora Serverless v2 with scale to zero: monitoring keeps the cluster awake, and that costs.
A cluster created with a minimum capacity of 0 ACU pauses after
seconds_until_auto_pause (300 s by default) without a connection. DBSheriff keeps
its sessions to a monitored server open and polls them every few seconds (Admin
Console, Poll Intervals), so a cluster that is monitored never pauses: it stays at
its minimum capacity, 0.5 ACU, all day. That is 12 ACU-hours a day for each
monitored cluster, plus whatever the load adds (about 1 to 2 ACU for four minutes
after a resume). Multiply by your region's ACU-hour price: at 0.12 USD, which is the
us-east-1 price for Aurora Standard at the time of writing (check the current price
list), 0.5 ACU is 0.06 USD an hour, about 1.44 USD a day or 43 USD a month for each
cluster. Lengthening the poll interval does not change this, because the sessions
stay open; real-time detection is the product, so there is no pause-friendly mode.
Where the cost matters more than watching that cluster, pause monitoring for it
(Monitored Servers, the Pause button, which sets monitoring_enabled off): the
sessions are released at the next registry reload and the cluster pauses about 6 to 9
minutes later (measured). A paused cluster takes 12 to 16 seconds to resume on the
first connection (measured 13 s on Aurora MySQL).
scripts/install_prereqs.py mysql <hostname> # choose "gcp" — checks performance_schema and the consumer; changing the flags below is manual
performance_schema needs a restart on Cloud SQL for MySQL — same as
every other platform — and gcloud sql instances patch restarts the
instance as it applies the flag, so the script never runs it: when
performance_schema is off it only prints a pointer to this section. Set
it yourself, via --database-flags (which replaces the whole flag
list, so read the current one first):
gcloud sql instances describe <instance> --format="value(settings.databaseFlags)"
gcloud sql instances patch <instance> --database-flags=performance_schema=on,<...the rest you read above...>
gcloud warns and prompts before this restart (-q to auto-confirm).
Then enable the consumer (same statement as every other platform — dynamic,
no restart):
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME = 'events_statements_history_long';
general_log is one of the flags that does not need a restart on
Cloud SQL for MySQL (confirmed against Google's own flag reference) —
unlike performance_schema, above:
gcloud sql instances patch <instance> --database-flags=general_log=on,<...the rest you're keeping...>
Google's docs don't confirm this routes to a queryable mysql.general_log
table the way self-managed/RDS do — verify with SELECT COUNT(*) FROM
mysql.general_log; after a moment of traffic before relying on it; this
script doesn't automate it for that reason.
Cloud SQL turns partial_revokes on (SELECT @@partial_revokes; answers 1), and
the schema grants of "Creating the accounts" change for it: with the variable on, the
name in GRANT ... ON <schema>.* is read literally, so the escaped my\_db names a
schema that does not exist and dbsheriff_classify and dbsheriff_plan then see no
table of my_db (the classification scan finds nothing). The scripts read the
variable before they grant and write my_db as it is; if you grant by hand on such a
server, do the same.
scripts/setup_dbsheriff.py mysql <hostname> # choose "azure"; asks for resource group + server name
Stages the change through your own authenticated az CLI session. From a
source checkout, with the az CLI installed and signed in, the script stages
the change itself. Through stack.sh tool on an installed host the container
has no cloud CLI, so the script says it could not stage the change and sends
you here: run the az commands below yourself.
Unlike RDS's ApplyMethod flag, Azure applies the new value immediately
but only a separate, explicit restart actually enables a static
parameter like this one — the script stages it and tells you the restart
command, same "never restarts for you" rule as everywhere else:
az mysql flexible-server parameter set --resource-group <rg> --server-name <server> \
--name performance_schema --value ON
az mysql flexible-server restart --resource-group <rg> --name <server>
Then enable the consumer (dynamic, no restart):
UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME = 'events_statements_history_long';
general_log/log_output aren't automated here — same reasoning as
GCP: DBSheriff doesn't read Cloud Logging or Azure Monitor yet, and this
platform's own audit trail isn't a mysql.general_log-table read.
Kill/lock: see "Creating the accounts" above — Flexible Server's admin
login has no CONNECTION_ADMIN/SUPER, but its own mysql.az_kill() /
az_kill_query() procedures do the same job; kill and lock work fully
here, just through those instead of a plain GRANT CONNECTION_ADMIN.
Admin Console → Monitored Servers → Add: engine mysql (or
aurora_mysql), host, port, the schema to monitor, the credentials for
whichever accounts you created (leave the rest blank), ssl_mode (a new
server defaults to verify-full and needs MYSQL_SSL_CA: see the
installation guide (inside the package), section 4.8; or choose require).
Per-server switches on the same row: kill_mode
(terminate | cancel_then_terminate), kill_scope
(statement | session | login), burst_high_volume_threshold (calls
per second), aws_region, rds_instance_id (set by an unrestricted approver only,
see the audit source above), mysql_tag_writeback. Turn on kill enabled / lock-on-burst enabled
only if you created those accounts.
-- as dbsheriff_monitor
SELECT COUNT(*) FROM performance_schema.events_statements_history_long; -- > 0 once traffic flows
SELECT COUNT(*) FROM performance_schema.threads WHERE PROCESSLIST_USER IS NOT NULL;
SELECT user, host, account_locked FROM mysql.user; -- readable (the columns above)
-- SELECT authentication_string FROM mysql.user -- must fail with error 1143: the hashes are not readable
DBSheriff connects with the monitor credential when it starts monitoring the server (within the registry reload interval, five minutes), and a login the target refuses shows as a connection error in the producer's log. The other accounts are first used when their feature runs (the kill and lock accounts when a response needs one), so a wrong password on one of them shows up then. Live data appears within a minute; the first posture/governance snapshot within the hour.
If you will let DBSheriff create standing native controls in this MySQL server (masks, limits, time-boxed grants) through the database's own features, or write the SQL for your DBA to run, read native controls privileges (inside the package) first: it says what each tier can do, what the optional Control account needs here, and how to remove everything DBSheriff made. The account is created only on request, with create_reset_dbuser.py mysql <host> --with-control; setup_dbsheriff.py never creates it, and native controls are off until an approver turns a server to script or managed.
| Account | Can | Cannot |
|---|---|---|
dbsheriff_monitor, dbsheriff_posture |
Read the statement history of every account (performance_schema, which includes the text of other people's statements), the process list, the account inventory columns of mysql.user (names, hosts, plugin, lock and expiry state, privilege flags) and the other mysql.* tables above |
Read a password hash, read or change any application row, kill a session, change an account |
dbsheriff_kill |
Terminate any session (CONNECTION_ADMIN, or mysql.rds_kill / mysql.az_kill on a managed platform) — a denial of service against every application login |
Read application data, change an account |
dbsheriff_lock |
CREATE USER ON *.*: lock, unlock, drop or re-password any account that does not hold SYSTEM_USER, which is an account takeover of the application logins; read authentication_string (it keeps the previous hash for the opt-in PASSWORD_ROTATE rung); kill sessions. Treat it as data-access-equivalent |
Read application rows directly (it can still make itself a login that can) |
dbsheriff_classify |
SELECT on every row of every schema it was granted (GRANT SELECT ON schema.*, which covers the tables created later), and read the server's live process list through performance_schema.threads (every session's account, host and the statement it is running now; this is the one performance_schema table it holds, LIVE-283) |
Write, kill, change an account, read the statement history or any other performance_schema table |
dbsheriff_plan |
SELECT on the schemas it was granted — the same data reach as classify |
Anything else: no PROCESS, FILE, write, DDL or account privilege |
DBSheriff keeps these credentials encrypted in its registry (SERVER_CREDENTIALS_KEY); whoever can read both the registry and that key holds every account above. Create only the accounts you will use (--roles), and prefer a host part that names DBSheriff's address.