Incident Response

MySQL ERROR 1045 (28000): Access Denied for User - How to Fix It

The password is right, the user exists, and MySQL still says "Access denied". Usually it is not the password. It is the host part of the account, a socket instead of TCP, or a forgotten option file. Here is how to find which, and fix it.

· 16 min read · AG Data Team

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)

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:

  1. literal IP addresses and host names;
  2. then IP ranges in CIDR notation, then ranges with a netmask;
  3. '%' (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:

-- 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.

Don't make it '%' with all privileges. Under incident pressure it is tempting to create '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:

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 Security Authentication Troubleshooting
Work with us

Locked out of MySQL in production?

Access errors after a migration, a failover or a password rotation are incidents we handle every week. Send us the exact error and we will help you get back in without opening the server to everyone.

Get in touch More from the blog