What this check reads
Queries mysql.user for rows with authentication_string='' (and, on old servers, password=''), excluding socket-auth plugins (auth_socket, unix_socket) and the built-in locked system accounts (mariadb.sys, mysql.sys, mysql.session, mysql.infoschema, debian-sys-maint). The first query references the pre-5.7.6 password column; if that errors the script silently retries without it. Output is user@host pairs, joined by GROUP_CONCAT into a single string. Empty output, or the literal string NULL, is the PASS; one surviving pair is enough to FAIL, and every pair found is listed in the message.
When it applies
Runs only when MySQL/MariaDB is detected (HAS_MY=1) and reachable as root over the socket (MYOK=1); otherwise it SKIPs. It queries mysql.user live. Accounts using auth_socket/unix_socket are excluded by design, as are the built-in system accounts mariadb.sys, mysql.sys, mysql.session, mysql.infoschema and debian-sys-maint. Anything else with an empty authentication string is reported, including PAM/LDAP accounts that legitimately store no hash.
What each result means
| Result | When | What it means |
|---|---|---|
| FAIL | One or more non-system, non-socket accounts have an empty authentication string | The accounts are listed as user@host and can be used with no password at all. Set a password, lock them, or drop them. |
| PASS | None | Every named account authenticates with a password or through socket auth. |
| SKIP | No root socket access | Nothing was checked. The account table was never queried, so passwordless accounts are neither confirmed nor ruled out. |
Why it matters
CIS MySQL "Ensure passwords are not stored in the global configuration / Ensure no users have an empty password". An empty authentication_string with a password plugin (mysql_native_password, caching_sha2_password) means any client that can reach the socket or port logs in with no secret.
Why it fails, and when it is wrong
- Accounts created with
CREATE USER 'app'@'%';and never given a password. - Accounts migrated with
IDENTIFIED WITH mysql_native_passwordbut an empty hash after a botched dump/restore. - Accounts using PAM/LDAP plugins (
authentication_pam,authentication_ldap_*) legitimately have an emptyauthentication_stringand will be reported. Add the plugin name to the exclusion list or accept the finding. account_locked='Y'accounts (e.g. definer-only accounts) also have empty strings. The script excludes the standard ones by name but not custom locked accounts. Check withSELECT user,host,plugin,account_locked FROM mysql.user.
How to fix it
ALTER USER 'app'@'%' IDENTIFIED BY 'long-random-password';
-- or, for accounts that should never log in:
ALTER USER 'definer'@'localhost' ACCOUNT LOCK;
-- or remove them:
DROP USER 'stale'@'%';Verify the fix
sudo mysql -N -e "SELECT CONCAT(user,'@',host) FROM mysql.user
WHERE authentication_string=''
AND plugin NOT IN ('auth_socket','unix_socket')
AND user NOT IN ('','mariadb.sys','mysql.sys','mysql.session','mysql.infoschema','debian-sys-maint')"
# expected: no rows
# Confirm a fixed account really needs its password now:
mysql -u app -h 127.0.0.1 -e 'SELECT 1' # no -p
# expected: ERROR 1045Debugging
MYOK=0; see the same note under MySQL Anonymous Users.
Check its plugin: SELECT user,host,plugin,account_locked FROM mysql.user WHERE user='<name>';. PAM/LDAP plugins (authentication_pam, authentication_ldap_*) and custom locked accounts have an empty authentication_string by design. The script's exclusion list is fixed, so these must be accepted as known findings.
The message is built from GROUP_CONCAT; a literal string NULL is treated as "none". If you see odd output, run the query above directly to see the real rows.
The script first runs a query that also references the legacy password column and silently retries without it if that errors. If both queries fail you get PASS with no accounts listed; verify by hand with the query above adapted to your column layout.
Sources
- MySQL 8.4 Reference: Pluggable Authentication
- MySQL 8.4 Reference: ALTER USER (passwords, locking)
- MySQL 8.4 Reference: Reserved Accounts (
mysql.sys,mysql.session,mysql.infoschema) - MariaDB KB: mysql.user table
- MariaDB KB: Account Locking
- Debian wiki:
debian-sys-maintaccount background
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