Skip to main content
DevOpsintermediate

ERROR 1045 (28000): Access Denied for User

Fix ERROR 1045 (28000): Access denied for user in MySQL. Read the 'using password' flag, find which account row matched, fix host and plugin issues.

9 min readUpdated August 2026

ERROR 1045 (28000): Access denied for user is MySQL telling you that no account row matched the user name, the client host, and the credentials you supplied — all three at once. The message looks like this:

ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
ERROR 1045 (28000): Access denied for user 'appuser'@'10.0.3.14' (using password: YES)

Read it as three facts, not one: who you tried to be, where the server thinks you came from, and whether a password arrived. Most of the time the password is fine and one of the other two is wrong.

Why This Happens

MySQL does not simply look up a user name. It picks one row from its account list by matching both the user and the client host, with more specific host values winning over wildcards. Once a row is chosen, that row's credentials are the only ones checked — MySQL does not fall through and try another account. That single rule explains most 1045 errors.

Clue in the messageWhat it usually means
using password: NONo password reached the server at all
using password: YESA password arrived and did not match the matched row
Host shows localhostYou connected over a Unix socket
Host shows an IPYou connected over TCP, which is a different account
User shows '' (empty)An anonymous account row matched

Fix 1: Confirm Whether a Password Was Actually Sent

If the message says using password: NO, stop looking at passwords and look at how the client was invoked. Common causes: -p omitted, a shell variable that expanded to nothing, or an option file the client never read.

mysql -u appuser -p            # prompts — the password never appears in shell history
mysql --print-defaults         # shows which option files and values the client actually loaded

Never put the password on the command line as -pSecret: it lands in your shell history and in the process list where any local user can read it.

Fix 2: Check Which Account Row Actually Matched

This is the step that resolves the confusing cases, and almost nobody runs it. From any working connection:

SELECT USER(), CURRENT_USER();

USER() is who you asked to be. CURRENT_USER() is the row MySQL matched. If they differ, your grants are being applied to a different account than you think. The classic result is:

+------------------+----------------+
| USER()           | CURRENT_USER() |
+------------------+----------------+
| appuser@localhost| @localhost     |
+------------------+----------------+

An empty user in CURRENT_USER() means a leftover anonymous account matched. MySQL's documentation calls this out directly: a default row with Host='localhost' and User='' is more specific than the Host='%' row you created, so it wins for local connections and then rejects your password. Remove it:

SELECT user, host FROM mysql.user WHERE user = '';
DROP USER ''@'localhost';

Fix 3: Match the Host You Are Really Connecting From

localhost and 127.0.0.1 are not interchangeable. On Unix, localhost makes the client use a Unix socket, and MySQL matches the literal host value localhost. Using 127.0.0.1 forces TCP and matches 127.0.0.1 or a wildcard. A grant to one does nothing for the other.

SELECT user, host, plugin FROM mysql.user WHERE user = 'appuser';

Compare the host column against how the client actually connects. If your app connects over TCP from another host, grant to that host specifically rather than opening the account to everything:

CREATE USER 'appuser'@'10.0.3.14' IDENTIFIED BY '<strong-password>';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'10.0.3.14';

Grant only the privileges and the schema the application needs. GRANT ALL ON *.* turns a routine connection problem into a standing security exposure, and it will not fix a host mismatch anyway.

To force a TCP connection while testing so you can tell the two paths apart:

mysql -h 127.0.0.1 -P 3306 -u appuser -p        # TCP
mysql -h localhost -u appuser -p                # socket
Advertisement

Fix 4: Check the Authentication Plugin

MySQL 8 creates accounts with caching_sha2_password by default. Older clients, drivers, and language connectors that only implement mysql_native_password fail against those accounts, sometimes surfacing as 1045 and sometimes as "client does not support authentication protocol requested by server".

SELECT user, host, plugin FROM mysql.user WHERE user = 'appuser';

Upgrade the client or driver rather than moving the account back to the older plugin. Downgrading authentication to keep an outdated connector working weakens every future connection that account makes.

Fix 5: Confirm the Account Exists At All

SELECT user, host FROM mysql.user ORDER BY user, host;
SHOW GRANTS FOR 'appuser'@'10.0.3.14';

If SHOW GRANTS errors with "There is no such grant defined", the account genuinely does not exist for that host — create it as in Fix 3.

You do not need FLUSH PRIVILEGES after CREATE USER, GRANT, ALTER USER, or DROP USER; those update the privilege cache immediately. It is only required if you modified the mysql grant tables directly with INSERT or UPDATE, which is not the supported way to manage accounts.

Verify the Fix

mysql -h 127.0.0.1 -u appuser -p -e "SELECT USER(), CURRENT_USER(), DATABASE();"

USER() and CURRENT_USER() should now agree, and the host in CURRENT_USER() should be the row you intended. Then confirm the privileges are what you expect and no more:

SHOW GRANTS FOR CURRENT_USER();

What the Server Log Shows

The client message is deliberately vague about which of the three facts failed. The server's error log is not. With log_error_verbosity = 3, MySQL logs aborted connections with the user and host as the server resolved them — and that resolved host is the value your grants must match, which is frequently not the one you assumed. See where MySQL logs are stored for the path on your platform.

About skip-grant-tables

Search results will suggest starting MySQL with --skip-grant-tables. Understand what that does before you consider it: it starts the server with authentication switched off entirely, so for as long as it runs, anyone who can reach the server has unrestricted access to every database on it. It is a last-resort recovery procedure for a genuinely lost root password on an isolated host with networking disabled — not a fix for an application that cannot log in, and never something to run on a production server. Every cause above is diagnosable and fixable with the server running normally.

Prevent It Coming Back

  • Create accounts per host and per application, with only the privileges that application needs. Broad wildcard grants hide host problems until they become security problems.
  • Remove anonymous accounts on any server you inherit: SELECT user, host FROM mysql.user WHERE user = '';
  • Keep drivers current so caching_sha2_password accounts work without weakening authentication.
  • Store credentials in an option file with restrictive permissions (chmod 600) or a secret manager, not in shell commands or source control.
  • Check CURRENT_USER() after any grant change — it is the fastest confirmation that the row you edited is the row being used.

Next Steps

Frequently Asked Questions

Find answers to common questions

MySQL found no account row that both matches the user name and client host you connected from and accepts the credentials you sent. It is an authorisation match failure, not necessarily a wrong password — the account may exist for a different host, or a different account row may have matched first.

It reports whether the client sent a password at all. 'using password: NO' means no password reached the server — usually a missing -p flag, an empty variable in a connection string, or a config file the client did not read. 'using password: YES' means a password was sent and rejected.

They are different accounts. On Unix, connecting to 'localhost' uses a Unix socket and matches the 'localhost' host value, while 127.0.0.1 forces TCP and matches the '127.0.0.1' or '%' host value. Grants made to one do not apply to the other.

A default anonymous account with Host='localhost' and User='' is more specific than your '%' row, so MySQL matches it first and then rejects your password. Run SELECT USER(), CURRENT_USER() — if they differ, an anonymous row matched. Remove the anonymous account with DROP USER ''@'localhost'.

Run SELECT USER(), CURRENT_USER(). USER() shows what you asked to connect as, and CURRENT_USER() shows the account row MySQL actually matched. When those two disagree, your grants are being applied to a different account than you think.

No, not on any server holding real data. It starts MySQL with authentication switched off entirely, so anyone who can reach the server has full access while it runs. Reserve it for a genuinely lost root password on an isolated host, with networking disabled, and treat it as an incident rather than a fix.

Almost always a different identity or host. The application may connect over TCP while you use a socket, run as a different OS user reading a different option file, or run in a container whose source address is not the one you granted. Compare CURRENT_USER() from both.

Yes. MySQL 8 creates accounts with caching_sha2_password by default, and older clients or drivers that only speak mysql_native_password fail against it. Check the plugin column in mysql.user and upgrade the client or driver rather than downgrading the account's authentication.

No. CREATE USER, GRANT, ALTER USER, and DROP USER update the in-memory privilege cache immediately. FLUSH PRIVILEGES is only needed if you modified the mysql grant tables directly with INSERT or UPDATE, which is not the supported way to manage accounts.

The MySQL error log records connection failures, and with log_error_verbosity set to 3 it logs aborted connections with the user and host as the server saw them. That host value is the one your grants must match. See where MySQL logs are stored for the path on your platform.