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 message | What it usually means |
|---|---|
using password: NO | No password reached the server at all |
using password: YES | A password arrived and did not match the matched row |
Host shows localhost | You connected over a Unix socket |
| Host shows an IP | You 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
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_passwordaccounts 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
- Where are MySQL logs stored — the server side of every connection failure
- Fix "MySQL server has gone away" if connections succeed and then drop mid-query
- Fix "Lock wait timeout exceeded" if you are connected but queries stall