MySQL ERROR 1045 (28000): Access denied for user looks like a wrong password, and sometimes it is. More often the password is fine and MySQL is checking it against a different account than you think. In MySQL an account is a user name and a host, and the server picks one matching row before it looks at any password. Find out which row it picked, and the error usually explains itself.
The message has three parts worth reading:
ERROR 1045 (28000): Access denied for user 'app'@'10.20.0.15' (using password: YES)
- 'app': the user name the client sent. If you didn't pass
-u, the MySQL docs on connecting say the default is your Unix login name, so runningmysqlas root on Linux tries the MySQL userroot. - '10.20.0.15': the client host as the server sees it, either an IP address, a resolved host name or
localhost. Behind a proxy, NAT or a container network this is often not the machine you think you are on. - using password: YES / NO: whether the client sent a password at all. "NO" means the client didn't send a password, which usually means a missing
-p, an empty variable in an application config or a client reading the wrong option file.
The troubleshooting chapter puts it the same way: the message tells you who you tried to log in as, from which host, and whether you used a password. Normally there should be one row in mysql.user that matches that exact user and host.
How MySQL picks an account
The connection verification docs describe Stage 1 of access control. The server accepts a connection only if the client host and user name match the Host and User columns of some row in mysql.user, the client supplies the credentials that row requires (checked by the plugin in the plugin column), and the account is not locked.
When several rows could match, MySQL doesn't try them one by one until a password works. It sorts the rows when it loads the grant tables, takes the first match, and checks the password against that row only. The sort puts the most specific hosts first:
- literal IP addresses and host names;
- then IP ranges in CIDR notation, then ranges with a netmask;
'%'(any host) near the end, and an empty host after that.
Within the same host, a named user sorts before the anonymous user ''. Rows with the same host and user specificity are ordered nondeterministically.
That sorting is behind the most confusing version of this error. You create 'app'@'%', test from the database server itself, and get "Access denied" with a password you just set. The docs walk through this exact case: if an anonymous account ''@'localhost' exists, it is more specific than '%' for a local connection, so it wins. The server then checks your password against the anonymous account, which fails. MySQL's own manual calls the belief that rows naming your user are always tried first "a common misconception".
A clean MySQL 8.4 initialization creates 'root'@'localhost' and a few reserved accounts, not anonymous ones. Older installations and servers built by hand can still have them, so look:
SELECT user, host, plugin, account_locked, password_expired
FROM mysql.user
WHERE user IN ('app', '')
ORDER BY host, user;
If you can log in at all, even as another user, ask the server what it thinks you are:
SELECT USER(), CURRENT_USER();
USER() is what the client asked for. CURRENT_USER() is the account row that was actually used. If the second one shows @localhost with an empty user name, you are being treated as the anonymous user.
localhost is not 127.0.0.1
On Unix, localhost is special for MySQL clients: they connect through the Unix socket file, not TCP/IP. The docs treat socket, named pipe and shared memory connections as local connections. They match a host part of localhost if such accounts exist, otherwise wildcard hosts that match it. A connection to 127.0.0.1 is TCP/IP and is matched by address. So one user can have two different accounts, 'app'@'localhost' and 'app'@'127.0.0.1', with different passwords and privileges. Then "it works from the shell but not from the application" is exactly what you'd expect.
mysql -u app -p -h localhost # Unix socket, matches 'app'@'localhost'
mysql -u app -p -h 127.0.0.1 # TCP/IP, matched by address
mysql -u app -p --protocol=TCP # TCP/IP even for localhost
mysql --no-defaults -u app -p -h 127.0.0.1 # ignore option files
The last line matters more than people expect. Clients read option files, and the troubleshooting docs warn that an old password in ~/.my.cnf or another option file gives "Access denied" even when you didn't type a password. If "using password: YES" appears while you typed none, an option file is sending one. --no-defaults rules that out in seconds.
Name resolution is the other host-side trap. If you created accounts with host names but the server runs with skip_name_resolve, the host cache docs say only accounts with IP addresses (and localhost) can be used. If DNS lookups fail, the error can show the user as ''@'unknown' or with an empty host.
Fix it with the right account, not a wider one
Once you know the host the server sees and the row it matched, the fix is usually one of these:
- No row exists for this user from this host. Create one for the real client network and grant only what it needs.
- The wrong row wins. Remove the anonymous account, or add a more specific account for the user. If you changed grant tables with direct
INSERT/UPDATE/DELETEinstead ofCREATE USERorALTER USER, runFLUSH PRIVILEGES. The docs note that direct edits have no effect until you flush. - The password differs between twin accounts. Reset it on the exact account that matched, the one
CURRENT_USER()showed.
-- a dedicated account for the real client network
CREATE USER 'app'@'10.20.0.%' IDENTIFIED BY 'long-random-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'10.20.0.%';
-- or reset the password of the account that actually matched
ALTER USER 'app'@'10.20.0.%' IDENTIFIED BY 'long-random-password';
Quote the user and host separately. The account name docs point out that 'app@10.20.0.%' in a single pair of quotes is a user literally named app@10.20.0.% at host '%', and that this behavior is deprecated.
'app'@'%' with ALL PRIVILEGES and move on. That makes the account reachable from any host that can get to port 3306. It also may not even fix the problem if a more specific row still wins. The docs themselves say to replace a temporary '%' with the real host once you know it.Similar errors that are not 1045
The server error reference has several neighbours that are easy to mix up, and each points somewhere else:
- 1698 "Access denied for user 'root'@'localhost'" with no "using password" part. In the 8.4 authentication source this variant is used when the plugin reports that a password was never part of the exchange, as with auth_socket. That plugin authenticates socket clients by the operating system user running the client. Some packaged installations may set up local admin accounts this way. If
pluginforroot@localhostsaysauth_socket, run the client as the matching OS user. A password won't help. - 1130 "Host '...' is not allowed to connect to this MySQL server": no account exists for that host at all.
- 1044 "Access denied for user ... to database ...": login worked, but the account has no privileges on that database.
- 3118 "Account is locked" and 3955 "Account is blocked ... due to ... consecutive failed login attempts": the account is locked with
ACCOUNT LOCKor temporarily blocked byFAILED_LOGIN_ATTEMPTSwithPASSWORD_LOCK_TIME.
Authentication plugins also deserve a check after upgrades. MySQL 8.4 no longer enables mysql_native_password by default. Accounts still on that plugin stop working until you migrate them or turn the plugin back on, as our MySQL 8.0 to 8.4 upgrade guide explains.
See failures in the server log
The client only gets the short message. The server can log every denied login too, but only at the highest verbosity. In the 8.4 source the access-denied log entries are written at INFORMATION priority. The system variable docs show log_error_verbosity defaults to 2 (errors and warnings), while 3 adds information messages. The variable is dynamic, so you can raise it briefly while you debug:
SET GLOBAL log_error_verbosity = 3;
-- reproduce the failed login, then read the error log
SET GLOBAL log_error_verbosity = 2;
A sudden burst of these from one address is worth investigating. It can be a deployment with a stale secret retrying in a loop. While you fix that, watch connection counts too: aggressive retries add load, and together with other trouble they can push a busy server toward "Too many connections".
Lost root password: reset it safely
If nobody has a working admin login, the official reset procedure for Unix is the safer route. Stop the server cleanly and put a single ALTER USER 'root'@'localhost' IDENTIFIED BY '...'; line in a file only the mysql OS user can read. Then start mysqld once with --init-file pointing at it. Once the server has started, delete the file, then stop the server and restart it normally.
The generic alternative, --skip-grant-tables, lets anyone connect without a password and with all privileges. The docs note that MySQL enables skip_networking automatically in that mode to block remote connections. You also have to run FLUSH PRIVILEGES before account statements like ALTER USER work. Use it only if the init-file method is not possible, keep the window short, and restart normally right after.
After a fresh install, a 1045 for root@localhost with "using password: NO" usually means initialization generated a random root password. The docs say to look for it in the error log.
Key takeaways
- MySQL ERROR 1045 is about the account, user plus host, that matched first. Very often the password is not the problem.
- Rows are sorted by host specificity, and an anonymous
''@'localhost'can beat'app'@'%'. localhostmeans the Unix socket;127.0.0.1means TCP. They can be different accounts.- Compare
USER()andCURRENT_USER(), and use--no-defaultsto rule out option files. - Errors 1698, 1130, 1044, 3118 and 3955 look similar but have different causes.
- Reset a lost root password with
--init-file, and keep any--skip-grant-tableswindow short.