A likelihood-ordered checklist for tracing "Permission denied" on Linux through mode bits, ownership, ACLs, SELinux, and mount options.
"Permission denied" is one error string covering at least seven different failures. The trick to fixing it fast is to check them in order of likelihood instead of guessing. Below is the order I actually work through on a live box, from the boring-and-common down to the exotic.
Resist the urge to chmod 777 your way out. It usually does fix the symptom, which is exactly why it is dangerous: you never learn what was wrong, and you leave a world-writable file behind for the next person to find. Diagnose first.
Start with the file itself. Who owns it, and who are you?
$ ls -l deploy.conf
-rw-r----- 1 root deploy 812 Jul 20 14:02 deploy.conf
$ id
uid=1001(alice) gid=1001(alice) groups=1001(alice),27(sudo)
Read the mode left to right: -rw-r-----. Owner (root) gets read/write, group (deploy) gets read, everyone else gets nothing. You are alice, not root, and you are not in the deploy group. So you land in the "other" bucket with zero permissions. That is your denial.
The fix is to grant the right access to the right principal, not to open the file to the world. Add yourself to the group, or adjust group ownership:
$ sudo usermod -aG deploy alice # then log out/in for it to take
$ sudo chgrp deploy deploy.conf # or fix group ownership instead
The three fields that matter every time: the mode string, the owner (third column), and the group (fourth column). Compare them against id output. Ninety percent of denials end here.
On a directory, the execute bit means "may traverse," not "may run." Without x on a directory you cannot enter it, cannot cd into it, and cannot reach anything inside it even if the target file is 0777.
The part people miss: every parent in the path needs x for you. A permission problem three levels up denies the leaf.
$ cat /srv/app/secrets/token
cat: /srv/app/secrets/token: Permission denied
$ namei -l /srv/app/secrets/token
f: /srv/app/secrets/token
drwxr-xr-x root root /
drwxr-xr-x root root srv
drwxr-x--- root app app
drwxr-xr-x root app secrets
-rw-r--r-- root app token
namei -l walks the full path and prints the mode of each component. The token file is world-readable, but app is drwxr-x--- and you are not root or in app. You cannot traverse app, so you never reach token. This is the single most useful command for "but the file permissions look fine" cases.
If you are trying to run something and get denied, check the execute bit on the file itself.
$ ./backup.sh
bash: ./backup.sh: Permission denied
$ ls -l backup.sh
-rw-r--r-- 1 alice alice 240 Jul 21 09:10 backup.sh
$ chmod +x backup.sh
Note the distinct signal: the shell prefixes the error with bash: and the path. That is the kernel refusing to exec a file without an execute bit. A related lookalike is bash: ./backup.sh: cannot execute: required file not found, which is usually a bad shebang line or CRLF line endings, not a permission issue at all. Read the exact wording before reaching for chmod.
If ls -l shows a trailing + on the mode string, standard bits are not the whole story. There are POSIX ACLs layered on top.
$ ls -l report.csv
-rw-rw----+ 1 finance finance 1841 Jul 19 22:31 report.csv
$ getfacl report.csv
# file: report.csv
# owner: finance
# group: finance
user::rw-
user:alice:--- # explicit deny for alice
group::rw-
mask::rw-
other::---
The base group bits say rw-, so you would expect group members to read it. But there is an explicit user:alice:--- entry stripping your access. ACLs let you deny a single user regardless of group membership, and ls -l only hints at it with that +. Grant access with setfacl:
$ sudo setfacl -m u:alice:rw report.csv
Also watch the mask line. The effective permission for named users and the group is the ACL entry ANDed with the mask, so a restrictive mask can silently clamp everything below it.
Here is the classic tell: the mode bits are correct, ownership is correct, no ACLs, and it still fails. Then someone runs setenforce 0 and it works. That is SELinux, not permissions.
$ sudo cat /var/www/html/index.html
cat: /var/www/html/index.html: Permission denied
$ ls -Z /var/www/html/index.html
-rw-r--r--. root root unconfined_u:object_r:user_home_t:s0 index.html
The . after the mode means an SELinux context is present. That context is user_home_t, which is wrong for something the web server should serve (it wants httpd_sys_content_t). The file was probably mv'd from a home directory, carrying its old label. Confirm with the audit log:
$ sudo ausearch -m avc -ts recent
type=AVC msg=audit(...): avc: denied { read } for pid=1123 comm="httpd"
name="index.html" dev="sda1" ino=54217
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0
$ sudo sealert -a /var/log/audit/audit.log # human-readable suggestions
$ sudo restorecon -v /var/www/html/index.html # reset to policy default
ausearch -m avc shows the denial; sealert explains it and suggests a fix; restorecon relabels to the policy default, which fixes most of these. Reach for semanage fcontext only when the default is genuinely wrong for your layout. Do not leave SELinux disabled as a "fix."
On Debian and Ubuntu the equivalent is AppArmor. Check sudo aa-status and look in dmesg or the journal for apparmor="DENIED" lines naming the profile and the path it blocked.
If a whole filesystem behaves oddly, the mount itself may be the constraint. noexec blocks running any binary from that mount; ro blocks all writes no matter what the bits say.
$ ./tool
bash: ./tool: Permission denied
$ findmnt -no OPTIONS -T ./tool
rw,nosuid,nodev,noexec,relatime
There it is: noexec on this mount, common on /tmp, /home, and removable media hardened by policy. The file has its x bit, but the mount forbids execution. Similarly a write to a read-only mount fails with Read-only file system rather than the generic message. findmnt -T <path> tells you which mount a path lives on and its options.
A subtle one: you can cat a file but >> fails. Read and write are separate bits, and you may match a class that grants one but not the other.
$ ls -l app.log
-rw-r--r-- 1 root adm 9210 Jul 21 11:44 app.log
$ echo x >> app.log
bash: app.log: Permission denied
You are neither root (owner, rw) nor in adm (group, r), so you fall to "other," which is read-only. Reading works, appending does not. The mental model: the kernel picks exactly one class for you (owner, else group, else other) and applies only that class's bits. It does not fall through to a more permissive one.
Work top to bottom and stop at the first hit. Nine times in ten it is a mode-bit or ownership mismatch you can read straight out of ls -l and id; namei -l catches the parent-directory traversal cases. Only after those come out clean should you suspect the + of an ACL or the . of an SELinux label. Fix the specific principal that is being denied. Do not chmod 777, do not leave setenforce 0 running, and relabel with restorecon rather than disabling policy.
For the broader methodology, see the Linux troubleshooting guide, and for a deeper reference on the bits themselves read Linux file permissions explained.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Explore more articles in this category
A field-tested workflow for diagnosing why a systemd unit refuses to start, from status output to exit codes to the usual root causes.
When a Linux box misbehaves, the same dozen problems come up again and again. This is the map: what each symptom means and the fast path to the fix.
A field-tested workflow for finding which process burns your CPU and whether the time is user, system, or iowait.
Evergreen posts worth revisiting.