'Permission Denied' Usually Is Not About Permissions

Updated 2026-09-23

A server administration panel showing user limits, host settings and password fields
Photo: Wikimedia Commons (Public domain)

The word in the error is the wrong word

“Permission denied” sounds like a permission problem, so people go and change permissions. On a good day that does nothing. On a bad day it works, for the wrong reason, and quietly removes a protection.

The reliable read is different. In most of these cases the system has decided you are somebody other than who you think you are, or a policy unrelated to file permissions is refusing before permissions are ever consulted.

So the first move is never to grant anything. It is to make the system state, out loud, which identity it matched you to.

MySQL: access denied for user

MySQL has the clearest example of this in all of computing, and almost nobody knows it.

A MySQL user is not a username. It is a username and a host, together, as one identity. 'app'@'localhost' and 'app'@'%' are two different accounts. They can have different passwords and different grants. Knowing “the password for app” is therefore not a complete thought.

The one query that ends the guessing

SELECT USER(), CURRENT_USER();

These return different things, and the difference is the entire diagnosis.

When those disagree, you have found your bug without touching a single permission. You are being authorized as an account you did not intend to use.

Why they disagree: most specific host wins

MySQL sorts its accounts and matches the most specific host first. A literal localhost beats the wildcard %.

The classic trap is the anonymous user. Some installations ship a row with an empty username and host localhost. It matches any username arriving from localhost, and because localhost is more specific than %, it wins over your real 'app'@'%' account. You then get access denied using a password that is completely correct, for an account you are not being matched to.

Look at the actual account list:

SELECT user, host, plugin FROM mysql.user ORDER BY user, host;

An empty user value in that output is the anonymous account, and on a server you control it should generally not exist.

localhost and 127.0.0.1 are not the same thing

This surprises people every time. In MySQL:

They are different transports, and they match different grant rows. So 'app'@'localhost' and 'app'@'127.0.0.1' are separate accounts, and a connection string that works from one machine can fail from another purely because one spelled it as a name and the other as an address.

If you need TCP explicitly:

mysql --protocol=TCP -h 127.0.0.1 -u app -p

Reading the rest of the message

Access denied for user 'app'@'localhost' (using password: NO)

using password: NO means no password was sent at all, which is a different problem from sending the wrong one. Usually the client never picked one up: a missing -p, an empty environment variable, or a config file that was not read.

using password: YES means the password was sent and rejected, which is when you start looking at the account list above.

Authentication plugin mismatch produces its own version of this. Modern MySQL defaults to caching_sha2_password, and older clients and drivers cannot negotiate it. The plugin column in the query above tells you which one an account uses. A client that fails while another client succeeds with identical credentials points here.

PowerShell: the execution policy is not a permission

cannot be loaded because running scripts is disabled on this system

This is the most misdiagnosed message in the Windows world, because the standard advice is to disable the policy machine wide, and that is almost never the right fix.

Execution policy is not a security boundary. Microsoft documents it as such. It exists to stop you running a script by accident, not to stop anyone determined, and it is trivially bypassed by design. Treating it as protection and then turning it off is the worst of both readings.

See every scope at once, because one of them is overriding the others:

Get-ExecutionPolicy -List

Scopes apply in a fixed order of precedence: MachinePolicy, then UserPolicy, then Process, then CurrentUser, then LocalMachine. A Group Policy setting at the top cannot be overridden locally, and if that is what you are hitting, no amount of Set-ExecutionPolicy will help. The output tells you which scope to argue with.

The fix people skip: unblock the file

Very often the policy is fine and the file is marked. Windows attaches a Mark of the Web to anything downloaded from the internet or extracted from a downloaded archive, and PowerShell treats marked files more strictly.

Unblock-File -Path .\script.ps1

That removes the mark on a file you have decided to trust, and leaves the policy intact for everything else. It is the correct fix for “I downloaded a script and it will not run”, and it is far better than loosening the setting globally so that every future download runs too.

If you do need to change the policy, scope it

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

Process scope lasts only for the current window and disappears when you close it. Compare that with the usual internet advice of setting Unrestricted for LocalMachine, which is permanent, affects every user, and outlives the reason you did it by several years.

Linux: when the permissions genuinely do look correct

Here the message is frequently accurate, but only after you have ruled out the cases where it is not.

Check what you are looking at

ls -l /path/to/file
id

id is the equivalent of MySQL’s CURRENT_USER(). Running under a different user or a different group set than you assume, particularly inside sudo, su, a service unit or a container, explains a large share of these.

A recently added group does not apply to your current session. Group membership is read at login. Adding yourself to docker or www-data and then immediately retrying will fail, and the fix is to log out and back in, not to change permissions. Compare id with the group list in /etc/group to confirm.

Execute permission on directories is not what people assume. For a directory, x means “may traverse into”, not “may run”. You need x on every directory along the path. A file readable by everyone, sitting inside a directory with no traversal permission, is unreachable, and the error blames the file.

Permissions look perfect and it still fails. This is normally SELinux or AppArmor refusing after the ordinary check passed. Standard tooling shows nothing wrong because nothing is wrong at that layer.

ls -Z /path/to/file
sudo ausearch -m avc -ts recent

If ausearch produces entries mentioning your process, stop editing permissions. The denial is coming from somewhere else entirely, and the log line names the exact rule.

Immutable files are a rarer but very confusing case. A file can be locked against modification even for root:

lsattr /path/to/file

An i in that output means immutable, and it will refuse writes no matter how generous the mode bits are.

Docker: the two permission errors worth knowing

Permission denied on the Docker socket means your user is not in the docker group, or is but has not logged out since being added, per the group caveat above. Worth knowing before you reach for it: membership in that group is effectively equivalent to root access on the host, because it lets you start a container that mounts the entire filesystem. It is a reasonable choice on your own machine and a considered one on a shared server.

Permission denied on a mounted volume is a user ID mismatch. The container has its own idea of which numeric UID owns what, and a file owned by UID 1000 on the host is owned by whatever UID 1000 means inside the image, which may be a different account or none at all. The file did not change. The interpretation of its owner did.

docker run --user "$(id -u):$(id -g)" ...

And the case that is not a permission error at all: a private image that reports “not found” rather than “access denied”. Registries hide the existence of repositories you cannot see, so an authentication failure arrives dressed as a missing image. Covered in our companion guide on why “not found” errors lie.

Windows: access denied on a file

Before touching an ACL, rule out the common impostor: the file is in use. Windows reports a sharing violation in terms that read like a permission problem. If a program has the file open, no permission change will help, and granting yourself more access teaches you nothing about which program it is.

When it really is permissions, look before you change anything:

icacls "C:\path\to\file"

Ownership and permission are different things. An administrator who cannot read a file can usually take ownership of it and then grant access, which is two operations rather than one. If a single permission change appears not to work, this distinction is often why.

The short version

Do not start by granting. Start by asking the system who it thinks you are.

The permissions are usually correct. The identity is usually not.