Database audit fix

How to Restrict MySQL Wildcard User Host Privileges

MySQL authorises on user plus host, so an account created with the % wildcard host accepts connections from any address and leaves the password as the only control. Pin each one to the app server IP with RENAME USER.

Hiren KalariyaLast reviewed: Sep 25, 2026Check mysql-wildcard-hosts
Medium
severity
Yes
needs root or sudo
3
results it can return
1 of 6
checks in this audit

Every threshold on this page is transcribed from the database-security-least-privilege audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 6 Least-Privilege Permissions checks, or how the audit runs.

Medium severityNeeds sudo

What this check reads

The check queries the grant table directly with SELECT user,host FROM mysql.user WHERE host='%' AND user<>'', which lists every named account that may connect from any address. It matches the host column literally against the single character %, and the user<>'' clause drops rows with a blank user name so that anonymous accounts are counted once, by the Anonymous Users check. One row is the threshold: any non-empty result is a WARN naming each user@host pair, and only an empty result passes.

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 audits the grant table, not the listener - a user@'%' account is reported even when bind-address=127.0.0.1, skip_networking is on, or a firewall blocks 3306, because the over-broad grant is the finding. Only the bare % host is matched: partial wildcards such as 10.0.% or %.internal are not reported, so PASS does not mean every host value is specific. Accounts with an empty user name are excluded here (they are the Anonymous Users check's job).

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPMySQL not installed / not accessibleNothing was checked. No MySQL was found, or it could not be queried as root, so the grant table was never read.
WARNOne or more `user@'%'` accountsAt least one named account can authenticate from any address, and the message lists which ones.
PASSNoneNo named account carries the bare % host, so every grant this check can see names a specific address.

Why it matters

MySQL authorisation is on user@host. % makes the password the only control; combined with bind-address=0.0.0.0 and an open port it exposes the account to the internet. OWASP: restrict accounts to allowed hosts. CIS MySQL "Ensure no users have wildcard hostnames".

Why it fails, and when it is wrong

  • Docker/Compose MYSQL_USER creates user@'%' by design; inside a private Compose network that is acceptable only if 3306 is not published.
  • Applications that connect over TCP to 127.0.0.1 need user@'127.0.0.1' or user@'localhost' (the latter means Unix socket in MySQL). Many admins pick % to avoid learning this distinction.
  • Partial wildcards (10.0.%, %.internal) are not reported; only the bare %.
  • If skip_networking is on or bind-address=127.0.0.1, a % account is unreachable remotely; the check still warns because it audits the grant, not the listener.

How to fix it

List the wildcard accounts

The WARN message already names every offender. If you are working on the server instead, the check's own query gives you the same list.

sudo mysql -N -e "SELECT CONCAT(user,'@',host) FROM mysql.user WHERE host='%' AND user<>''"

Rename each account to a specific host

RENAME USER rewrites the host half of the pair in place. The password hash and every grant travel with it, so there is no window in which the account does not exist and nothing has to be re-granted afterwards.

RENAME USER 'app'@'%' TO 'app'@'10.0.0.5';      -- keeps password and grants
-- or for local TCP:
RENAME USER 'app'@'%' TO 'app'@'127.0.0.1';

In MySQL localhost means the Unix socket, so an application that dials 127.0.0.1:3306 needs the 127.0.0.1 form rather than localhost.

Verify the application connects

Restart or reconnect the application and confirm it still authenticates from its new host before you move on. When it does not, the cause is almost always the socket-versus-TCP distinction above rather than a lost grant.

Pair the grant with a bound listener

Pair with bind-address and a firewall rule (see the Network Isolation checks). The grant decides who may authenticate; the listener and the firewall decide who can reach port 3306 at all, and this check deliberately audits only the first of the three.

Verify the fix

sudo mysql -N -e "SELECT CONCAT(user,'@',host) FROM mysql.user WHERE host='%' AND user<>''"
# expected: no rows

# See every host value, including the partial wildcards this check ignores:
sudo mysql -N -e "SELECT user, host FROM mysql.user ORDER BY user"

# Grants survive RENAME USER - confirm the account still works from its new host:
mysql -u app -h 10.0.0.5 -p -e 'SELECT 1'

Debugging

HAS_MY=0 or MYOK=0. Test with sudo mysql -N -e 'SELECT 1'; see the Authentication checks' debugging notes for socket-path and sudo causes.

The MYSQL_USER entrypoint always creates user@'%'. Inside a private Compose network with 3306 unpublished this is low risk, but it is still a real wildcard grant. Confirm the port is not published (docker ps --format '{{.Ports}}') and record it, or pin the account to the app container's subnet.

Deliberate. The check audits the grant; exposure is covered by MySQL Network Binding and Exposed Database Ports. Fixing the grant is still the right move - defence in depth.

In MySQL localhost means the Unix socket, not TCP to 127.0.0.1. An app connecting to 127.0.0.1:3306 needs app@'127.0.0.1'. This trips people up constantly and is the usual reason % was chosen in the first place.

Partial wildcards are invisible to this check. Review the full user, host listing above.

Sources

How the script reads this

Next

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

All 6 Least-Privilege Permissions 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.

Windows

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