Database audit fix

How to Remove MySQL Anonymous Users and Accounts

An anonymous MySQL account is a row in mysql.user with a blank username, so a client can connect with any name that does not otherwise exist. Count them with SELECT COUNT(*) FROM mysql.user WHERE user='' and drop every host variant.

Hiren KalariyaLast reviewed: Aug 30, 2026Check mysql-anonymous-users
High
severity
Yes
needs root or sudo
3
results it can return
2 of 8
checks in this audit

Every threshold on this page is transcribed from the database-security-authentication audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 8 Authentication checks, or how the audit runs.

High severityNeeds sudo

What this check reads

Runs SELECT COUNT(*) FROM mysql.user WHERE user='' against the live server over the privileged socket connection. Any row with an empty user name is an anonymous account: clients can connect with any username that does not otherwise exist and get that account's privileges. The threshold is zero: a count of 0 is the PASS, any count above 0 is the FAIL, and the number itself is quoted in the result message. It reads the account table rather than a config file, so it always reflects the accounts that exist right now.

When it applies

Runs only when MySQL/MariaDB is detected (HAS_MY=1) and the script can query it as root over the socket (MYOK=1). Without that socket access the check SKIPs rather than guessing. It reads the server's own mysql.user table, so it reflects the live account list, not a config file. Containerised MySQL is not covered.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
FAILCount > 0Anyone can connect without credentials. The count is reported; run mysql_secure_installation or drop each blank-username row by hand.
PASSCount = 0Every connection has to name a real account.
SKIP`MYOK` is 0 (no root socket access, or server down)Nothing was measured. The account list was never read, so no claim is made either way.

Why it matters

MySQL's access-control stage 1 matches the most specific user@host row; an anonymous row ''@'localhost' shadows real accounts and grants access with no credentials. mysql_secure_installation removes them. CIS MySQL: "Ensure anonymous accounts are removed".

Why it fails, and when it is wrong

  • Older source/tarball installs and some cloud images ship ''@'localhost' and ''@'<hostname>'. Modern packages do not.
  • The test database grants were historically writable by anonymous users (see the Insecure Database Defaults check).
  • No known false positives: an empty user name is never legitimate.

How to fix it

Count the anonymous accounts

Any count above zero means connections are accepted with no username. List the rows too, because an installer usually leaves one per hostname.

SELECT COUNT(*) FROM mysql.user WHERE user='';
SELECT user, host FROM mysql.user WHERE user='';

List the passwordless named accounts

The same pass is worth spending on named accounts with an empty password. Socket-authenticated accounts and the packaged system accounts are excluded, because neither is a finding.

SELECT CONCAT(user,'@',host) FROM mysql.user
WHERE authentication_string=''
  AND plugin NOT IN ('auth_socket','unix_socket')
  AND user NOT IN ('','mysql.sys','mysql.session','mysql.infoschema','mariadb.sys','debian-sys-maint');

Run the hardening script

Run mysql_secure_installation and answer yes to "Remove anonymous users". It removes anonymous accounts, drops the test database and disables remote root in one pass.

Fix what is left by hand

Give a named account a password if something still uses it, drop it if nothing does, and repeat the DROP USER for every host returned in step 1.

ALTER USER 'appuser'@'localhost' IDENTIFIED BY 'long-random-password';
DROP USER ''@'localhost';   -- repeat for each host returned
FLUSH PRIVILEGES;

Verify the fix

sudo mysql -N -e "SELECT COUNT(*) FROM mysql.user WHERE user=''"
# expected: 0

# Any client using a made-up username must now be refused:
mysql -u nosuchuser -h 127.0.0.1 -e 'SELECT 1'
# expected: ERROR 1045

Debugging

MYOK=0. Run sudo mysql -N -e 'SELECT 1'; if that fails the check cannot run. Causes: server stopped, wrong socket path, or no usable sudo (see Script context).

You dropped ''@'localhost' but not ''@'<hostname>'. List them all with SELECT user,host FROM mysql.user WHERE user=''; and drop each host variant, then FLUSH PRIVILEGES.

sudo mysql -N -e "SELECT COUNT(*) FROM mysql.user WHERE user=''" is the entire test. Any non-zero count is the FAIL.

There are no known false positives here: an empty user name is never a legitimate account. If the count is above zero, the rows exist; list them with SELECT user,host FROM mysql.user WHERE user=''; and drop each one.

Sources

How the script reads this

Next

Re-run the Authentication audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 8 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 8 Authentication fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals