DBSheriff for MySQL / Aurora MySQL — prerequisites and roles

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.

The three scripts

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.

The 6 accounts

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

Connecting to the target

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_]*.

Creating the accounts

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 ....

Prerequisites, by platform

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.

Onprem / self-managed

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.

AWS (RDS / Aurora MySQL)

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).

GCP (Cloud SQL for 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.

Azure (Database for MySQL Flexible Server)

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.

Register the server

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.

Verify

-- 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.

Optional: the Control account (native controls)

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.

What each account can do if its password leaks

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.