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 | When | What it means |
|---|---|---|
| FAIL | Count > 0 | Anyone can connect without credentials. The count is reported; run mysql_secure_installation or drop each blank-username row by hand. |
| PASS | Count = 0 | Every 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
testdatabase 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 1045Debugging
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
- MySQL 8.4 Reference: Connection Access (how anonymous rows match)
- MySQL 8.4 Reference: Securing the Initial MySQL Account
- MariaDB KB: mariadb-secure-installation
- MySQL DROP USER
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