Skip to content

Permission Denied Errors

"Permission denied" is one of the most common — and most misdiagnosed — errors on Linux. The trap is that the file's ls -l permissions can look perfect and the operation still fails, because a second, invisible access-control layer (SELinux) is denying it. This playbook walks the whole stack, from the obvious bits to the AVC in the audit log.

Tested on

AlmaLinux 9 / RHEL 9 (and Rocky/CentOS Stream 9) with the default targeted SELinux policy. Standard-permission, ACL, and mount commands are identical on Debian/Ubuntu; the SELinux sections do not apply there (Debian/Ubuntu use AppArmor instead).

Symptom

You hit one of these:

  • Opening or writing a file fails: cat: report.txt: Permission denied or bash: file.txt: Permission denied.
  • Running a script fails even though it exists: ./deploy.sh: Permission denied.
  • A service cannot read or write its own data — for example a web server returns 403, or a daemon logs Permission denied and refuses to start.
  • cd into a directory fails: bash: cd: /srv/app: Permission denied.

The error text is the same regardless of which layer denied you, so the whole job is figuring out which layer.

Likely causes

Cause Tell-tale sign
Wrong file permissions (rwx bits) ls -l shows you lack the needed bit
Wrong ownership File owned by another user/group; you are neither
Missing execute bit on a script/binary ls -l shows no x; bash script.sh works but ./script.sh does not
Missing execute (search) bit on a parent directory A directory in the path lacks x — you cannot traverse it
Restrictive umask Newly created files come out without the bits you expected
ACLs overriding the visible mode ls -l shows a trailing +; the standard bits lie
Mount options (noexec, ro) The filesystem itself forbids execution or writing
SELinux context mismatch ls -l perms are correct, but access is still denied — the denial appears as an AVC in the audit log, not in normal perms

The key distinction: permissions vs. labels

Standard permissions (Discretionary Access Control) are what ls -l shows — owner, group, and rwx bits. SELinux labels (Mandatory Access Control) are a separate layer shown by ls -Z. A request must pass both. So a file can have a flawless -rw-r--r-- root root mode and still be denied because its SELinux type is wrong. When DAC looks fine but you are still blocked, suspect SELinux.

Diagnose

Work from the most common cause to the least, and don't stop at the first thing that looks plausible.

1. Standard permissions and ownership

ls -l /path/to/file
id                     # who am I, and which groups am I in?

Read the mode (-rwxr-x---), the owner, and the group, then compare against your id output. If you are not the owner and not in the group, you only get the "other" bits.

2. Walk the entire path

To open /srv/app/data/x.db you need the execute (search) bit on every directory along the way — /, /srv, /srv/app, /srv/app/data. A single directory missing x for you breaks the whole chain, even if the file itself is world-readable. namei -l makes this obvious:

namei -l /srv/app/data/x.db
f: /srv/app/data/x.db
 drwxr-xr-x root root /
 drwxr-xr-x root root srv
 drwxr-x--- root deploy app          <-- you are not 'deploy' -> no traverse
 drwxr-xr-x deploy deploy data
 -rw-r--r-- deploy deploy x.db

3. Execute bit on scripts

ls -l ./deploy.sh

No x for you? That is why ./deploy.sh fails while bash deploy.sh works (the latter does not need the file to be executable).

4. ACLs

A + at the end of the mode (-rw-r--r--+) means ACLs are in play and the displayed bits are not the whole story:

getfacl /path/to/file

5. Privilege escalation (sudo)

If the action needs root, confirm what you are actually allowed to run:

sudo -l

6. Mount options

A filesystem mounted noexec blocks execution regardless of the x bit; ro blocks all writes:

mount | grep /srv          # or:  findmnt /srv

Look for noexec, ro, or nosuid in the options.

7. SELinux — the non-obvious one

If everything above checks out and you are still denied, look at SELinux. Crucially, an SELinux denial does not appear in ls -l — it is logged as an AVC (Access Vector Cache) denial in the audit log:

getenforce                                   # Enforcing means SELinux can deny
ls -Z /path/to/file                          # show the SELinux label/type
sudo ausearch -m avc -ts recent              # recent SELinux denials
sudo sealert -a /var/log/audit/audit.log     # human-readable analysis + suggested fix

A typical AVC line names the source type (e.g. httpd_t), the target file, and the target type (e.g. default_t instead of the expected httpd_sys_content_t). That mismatch is the problem. See SELinux for the full model.

Fix

Apply the fix that matches what you found — do not blindly chmod 777.

Standard permissions and ownership

sudo chown deploy:deploy /srv/app/data/x.db      # fix ownership
sudo chmod u+rw /srv/app/data/x.db               # grant the bit you need

Execute bit

chmod +x ./deploy.sh

Directory traversal bits

Give traverse (x) on the directories in the path — without making their contents readable if you don't want a directory listing:

sudo chmod o+x /srv/app                          # 'x' without 'r' = traverse, no listing

ACLs

sudo setfacl -m u:deploy:rX /srv/app/data        # grant deploy read+traverse
sudo setfacl -R -m u:deploy:rwX /srv/app/data    # recursive; X = dir-x only

See User & Permission Management for ownership, groups, and ACL patterns in depth.

Mount options

Remount without the blocking option (and fix /etc/fstab so it survives reboot):

sudo mount -o remount,exec /srv                  # drop noexec
sudo mount -o remount,rw /srv                    # make writable

SELinux

If the label is wrong, the correct fix is almost never to disable SELinux. Relabel to the policy default:

sudo restorecon -Rv /srv/app                     # reset to policy-defined labels

If the path lives somewhere non-standard, teach the policy the correct type first, then relabel:

sudo semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?"
sudo restorecon -Rv /srv/app

chcon sets a label too, but it is temporary — a relabel or restorecon will revert it, so use it only for quick testing:

sudo chcon -t httpd_sys_content_t /srv/app/index.html   # temporary

When the denial is about a capability the policy gates behind a boolean (e.g. letting Apache make network connections), flip the boolean persistently:

sudo setsebool -P httpd_can_network_connect on

Don't reach for chmod 777 or setenforce 0

chmod 777 makes files world-writable — a real security hole — and rarely fixes the actual cause. setenforce 0 masks SELinux problems instead of solving them and leaves you exposed. Diagnose the layer, then fix that layer.

Prevent

  • Least privilege. Grant the narrowest mode that works. Use group ownership and ACLs to share access instead of widening "other" bits.
  • Set a sane umask. 022 (files 644, dirs 755) is the default; tighten to 027 for service accounts so new files aren't world-readable.
  • Understand contexts. When deploying to a non-standard path, set the semanage fcontext rule up front so restorecon always knows the right label — and so a future relabel doesn't break you.
  • Read the audit log first. When DAC looks correct, ausearch -m avc / sealert will usually hand you the exact fix. See Logs & journald for log navigation.
  • Test traversal end to end. Use namei -l after deploys to confirm the service account can reach its data through every parent directory.

Test yourself