Query icon
Log icon
If icon
SlackIncomingWebhook icon
Loop icon
Fail icon
Schedule icon

MySQL Account Hygiene Guard

Find anonymous MySQL accounts, empty passwords, remote root and global admin privileges, alert Slack, drop, lock or revoke and re-audit.

Categories
DataInfrastructure

MySQL servers that have been running for a few years have accounts nobody remembers:

  • an anonymous ''@'localhost' left from an old install;
  • a reporting user created with an empty password;
  • root@'%' added to debug from a laptop;
  • a legacy ETL account with ALL PRIVILEGES ON *.* WITH GRANT OPTION, which can read every file the server can and create new admins.

This blueprint reads mysql.user every day, flags those accounts and alerts Slack. It can generate and run the fixes one by one, then audit again to verify. The account the flow itself connects with is reported, but never changed, so a fix cannot lock the flow out halfway through.

This blueprint was created by zkasuran.

Findings

Finding Severity Rule Fix
ANONYMOUS_ACCOUNT High Account with an empty user name DROP USER
EMPTY_PASSWORD High Unlocked account with no password ALTER USER ... ACCOUNT LOCK
REMOTE_ROOT High root allowed from a host other than localhost ALTER USER ... ACCOUNT LOCK
GLOBAL_ADMIN_PRIVILEGES High Global SUPER, FILE, SHUTDOWN, CREATE USER or GRANT OPTION outside privileged_accounts REVOKE IF EXISTS of exactly those privileges, the others are kept
WILDCARD_HOST Medium Non-root account allowed from % Report only

Account names are quoted with QUOTE() inside MySQL. A finding on CURRENT_USER() is marked as the connected account and skipped.

How it works

  1. audit (io.kestra.plugin.jdbc.mysql.Query, read-only) runs one query on mysql.user that returns every finding with its fix statement. MySQL's own system accounts (mysql.sys, mysql.session, mysql.infoschema) are excluded.
  2. summary logs every finding.
  3. route (io.kestra.plugin.core.flow.If):
    • clean: logs it.
    • findings: alert posts to Slack.
      • remediate_gate runs only when auto_remediate is true and dry_run is false.
      • remediate (io.kestra.plugin.core.flow.Loop) runs each fix in its own Query, so one failure does not stop the others.
      • reaudit runs the same audit again, and verify_result fails if a fixed finding is still there.
  4. errors alerts Slack when the audit cannot run.

What you get

  • A daily list of the risky accounts on each server, with the reason each one matters.
  • Fixes that are as narrow as possible: locking instead of dropping a named account, and revoking only the admin privileges.
  • Protection against locking out the account that runs the audit, and a second audit as proof.

Who it's for

  • DBAs and platform teams responsible for MySQL servers that predate their current access policy.
  • Security and compliance teams that need evidence that anonymous, passwordless and over-privileged accounts are gone.
  • Teams evaluating the MySQL plugin who want an example of a catalog query driving account changes.

Why orchestrate this with Kestra

The query is easy to run once. Kestra runs it every day on every server, sends findings to Slack, gates the fixes behind auto_remediate and dry_run, isolates each statement, verifies the result, and keeps an execution history as evidence for audits.

Tested end to end

On Kestra 2.0.5 OSS against MySQL 8.4, with a local HTTP sink standing in for Slack. The flow connected as root@'%'. Seeded accounts:

  • an anonymous ''@'localhost', and reporting@'%' with an empty password;
  • legacy_etl@'%' with ALL ON *.* WITH GRANT OPTION, and dba_tom@'%' with SUPER, FILE;
  • a restricted app@'10.0.%', and a backup@localhost with RELOAD, PROCESS.
Run Inputs Result
1 defaults 8 findings. root@% reported as the connected account. app and backup not flagged. Alert sent
2 auto_remediate: true, dry_run: true Same report. The anonymous account is still in mysql.user
3 auto_remediate: true, dry_run: false 4 statements run and verified: anonymous dropped, reporting locked, admin privileges revoked from dba_tom and legacy_etl. Then in MySQL: reporting gets Account is locked, legacy_etl still reads but SHUTDOWN is denied. root@% untouched
4 same again Only root@% and the wildcard hosts left, nothing run
5 unreachable server errors alert sent

Prerequisites

  • A MySQL 8.0.30 or later server (for REVOKE IF EXISTS), reachable from the Kestra worker.
  • For the audit, SELECT on mysql.user. For remediation, CREATE USER and the privileges to revoke, with GRANT OPTION.
  • Local testing: docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pw mysql:8.4, then create a few accounts.

Secrets

  • MYSQL_USERNAME / MYSQL_PASSWORD: credentials used by every query.
  • SLACK_WEBHOOK_URL: used by alert and the errors block.
  • In Kestra OSS, provide each one as a base64-encoded environment variable prefixed SECRET_.

Inputs

Input Default Purpose
jdbc_url jdbc:mysql://localhost:3306/mysql Server
privileged_accounts [root@localhost] Accounts allowed admin privileges
auto_remediate false Allow fixes
dry_run true Must be false for a statement to run

Quick start

  1. Create a dedicated audit account, so the flow never relies on root. Add MYSQL_USERNAME, MYSQL_PASSWORD and SLACK_WEBHOOK_URL as secrets.
  2. Import the flow and set jdbc_url and privileged_accounts.
  3. Run it once and read the findings.
  4. Check which applications use a flagged account, then run with auto_remediate: true and dry_run: false.
  5. Enable the daily trigger, which always runs in safe mode.

How to extend

  • Loop over a list of JDBC URLs to audit every server in one execution.
  • Add dynamic privileges (SYSTEM_VARIABLES_ADMIN, BINLOG_ADMIN...) from mysql.global_grants to the admin rule.
  • Flag accounts with no password expiry, or that have not changed their password in a year (password_last_changed).
  • Ask for approval with a Pause before locking accounts on production.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.