What this check reads
Runs SELECT host FROM mysql.user WHERE user='root' AND host NOT IN ('localhost','127.0.0.1','::1') over the privileged socket connection and joins the matches with GROUP_CONCAT. Any other host value (including %, a subnet, or a specific IP) means root can authenticate over the network. Empty output, or the literal string NULL, is the PASS; a single non-local row is enough to FAIL, and every host found is named 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 SKIP. It reads the account list, not the network configuration - a root@'%' row is reported even when a firewall or bind-address makes port 3306 unreachable, because the grant itself is the finding. Network exposure is covered separately by MySQL Network Binding and Exposed Database Ports.
What each result means
| Result | When | What it means |
|---|---|---|
| FAIL | root has one or more non-local host entries | The host values are listed, and root can authenticate from every one of them. Remote root is the first account brute-forcers try. |
| PASS | root is local-only | Root can only log in from the machine itself, whatever its password is. |
| SKIP | No root socket access | Nothing was read. The grant table was never queried, so the host list for root is unknown. |
Why it matters
OWASP: accounts should connect only from allowed hosts; administrative accounts should never be network-reachable. CIS MySQL "Ensure 'root' is restricted to localhost". Credential-stuffing bots try root first on 3306.
Why it fails, and when it is wrong
- Docker
mysqlimages createroot@'%'whenMYSQL_ROOT_HOSTis not restricted. Managed installs (cPanel, Plesk) sometimes addroot@<hostname>. - Even
root@'10.0.0.5'is a finding under this check. If a remote admin host is truly required, create a dedicated admin account with a different name and TLS-required (REQUIRE SSLorREQUIRE X509) rather than exposing root. - Some setups rely on
root@<hostname>for replication or monitoring tools. Replace with least-privilege accounts (REPLICATION SLAVE,PROCESS, etc.).
How to fix it
SELECT user,host FROM mysql.user WHERE user='root';
DROP USER 'root'@'%';
-- if remote admin is unavoidable:
CREATE USER 'dba'@'10.0.0.5' IDENTIFIED BY '...' REQUIRE SSL;
GRANT ALL ON *.* TO 'dba'@'10.0.0.5' WITH GRANT OPTION;Verify the fix
sudo mysql -N -e "SELECT CONCAT(user,'@',host) FROM mysql.user WHERE user='root'"
# expected: only root@localhost, root@127.0.0.1 and/or root@::1
# From another host, remote root must be refused:
mysql -u root -h <server-ip> -e 'SELECT 1'
# expected: ERROR 1130 (host not allowed) or a connection failureDebugging
MYOK=0; see MySQL Anonymous Users.
There is more than one non-local row. List every one with SELECT user,host FROM mysql.user WHERE user='root'; and drop each, then FLUSH PRIVILEGES.
This check has no allow-list, so a legitimate root@'10.0.0.5' still FAILs. The intended resolution is a differently named admin account with REQUIRE SSL, after which the finding clears; otherwise record it as an accepted exception.
sudo mysql -N -e "SELECT GROUP_CONCAT(host SEPARATOR ' ') FROM mysql.user WHERE user='root' AND host NOT IN ('localhost','127.0.0.1','::1')". Empty or NULL output is the PASS condition.
Sources
- MySQL 8.4 Reference: Specifying Account Names (host part semantics)
- MySQL 8.4 Reference: Security Guidelines
- MySQL 8.4 Reference: CREATE USER ... REQUIRE (TLS options)
- Docker Hub mysql image:
MYSQL_ROOT_HOST - MariaDB KB: Configuring MariaDB for Remote Client Access
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