Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read

Bash & Python Error Guide: 'Permission denied' Running a Script

Fix 'Permission denied' when executing a script: missing execute bit, noexec-mounted filesystem, directory permissions, ACLs, and SELinux/AppArmor denials.

  • #automation
  • #troubleshooting
  • #errors
  • #permissions

Overview

Permission denied when running a script is the kernel refusing to execute the file with your current credentials. Unlike “No such file or directory” (a missing interpreter), this means the file is found but exec is not allowed. The permission check spans several layers: the file’s execute bit, the permissions of every directory in the path, the mount options of the filesystem it lives on, POSIX ACLs, and mandatory access control (SELinux/AppArmor). Any one of them can veto execution.

The plain form:

bash: ./deploy.sh: Permission denied

Or via the interpreter when only the script (not the dir or mount) is the issue:

./deploy.sh: line 1: /etc/secrets/key: Permission denied

It occurs at exec time. The distinction that matters: running ./script.sh requires the execute bit, while running bash script.sh only requires the read bit — so if bash script.sh works but ./script.sh does not, you are missing +x (or the filesystem is mounted noexec).

Symptoms

  • ./script.sh fails with Permission denied but bash script.sh runs fine.
  • The execute bit is set, yet exec still fails (points to mount options or MAC).
  • A script that worked yesterday fails after the file was copied or the volume remounted.
  • A command inside the script hits Permission denied on a file it reads/writes.
ls -l ./deploy.sh
-rw-r--r-- 1 ubuntu ubuntu 412 Jun 23 10:02 ./deploy.sh
./deploy.sh
bash: ./deploy.sh: Permission denied

Common Root Causes

1. The execute bit is not set

The most common cause. The file is readable but not executable, so ./script.sh is denied (though bash script.sh would work).

ls -l ./deploy.sh
-rw-r--r-- 1 ubuntu ubuntu 412 Jun 23 10:02 ./deploy.sh

No x in the mode. Add it:

chmod +x ./deploy.sh
ls -l ./deploy.sh
-rwxr-xr-x 1 ubuntu ubuntu 412 Jun 23 10:02 ./deploy.sh

2. The filesystem is mounted noexec

/tmp, /dev/shm, removable media, and some container volumes are often mounted with noexec, which blocks execution regardless of the file’s mode.

df ./deploy.sh
mount | grep "$(df --output=target ./deploy.sh | tail -1)"
Filesystem  1K-blocks  Used Available Use% Mounted on
tmpfs         1048576  1024   1047552   1% /tmp
/dev/sda1 on /tmp type ext4 (rw,nosuid,nodev,noexec,relatime)

noexec is set on /tmp. Run it via the interpreter (bash ./deploy.sh) or move it to an exec-allowed location.

3. A directory in the path lacks execute (traverse) permission

To reach a file, you need x on every parent directory. A 750 directory you are not a member of blocks access to everything inside.

namei -l ./deploy.sh
f: ./deploy.sh
 drwxr-xr-x root  root   /
 drwxr-x--- alice deploy /srv
 drwxr-xr-x alice deploy /srv/app
 -rwxr-xr-x alice deploy /srv/app/deploy.sh

/srv is drwxr-x--- owned by alice:deploy; if you are not alice or in deploy, you cannot traverse it. Add yourself to the group or adjust the directory mode.

4. The script itself is owned/restricted so you cannot read it

./script.sh ultimately needs read access (the interpreter reads the file). A rwx------ script owned by another user denies you.

ls -l ./secret.sh
id
-rwx------ 1 root root 200 Jun 23 ./secret.sh
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)

You are ubuntu; only root can read/exec it. Use sudo or relax the mode if appropriate.

5. A POSIX ACL overrides the visible mode

The ls mode can look permissive while an ACL mask or a per-user deny entry restricts you. The + after the mode bits signals an ACL.

ls -l ./run.sh
getfacl ./run.sh
-rwxr-xr-x+ 1 ubuntu ubuntu 300 Jun 23 ./run.sh
# file: run.sh
# owner: ubuntu
user::rwx
user:deploy:r--          #effective:r--
mask::r--

The mask::r-- caps everyone (except owner) to read-only, so exec is denied despite the visible x. Fix the mask/ACL:

setfacl -m mask::rwx ./run.sh

6. SELinux or AppArmor blocks execution

Even with correct Unix permissions, a mandatory-access-control policy can deny exec (wrong SELinux type label, or an AppArmor profile).

./svc.sh
sudo ausearch -m avc -ts recent 2>/dev/null | tail -3
bash: ./svc.sh: Permission denied
type=AVC msg=audit(...): avc:  denied  { execute } for  pid=... comm="bash" name="svc.sh" scontext=...:user_home_t tcontext=...:user_home_t

The SELinux denial (avc: denied { execute }) shows the script’s type label is not executable in that context. Restore the correct context:

sudo restorecon -v ./svc.sh

Diagnostic Workflow

Step 1: Compare direct exec vs. interpreter exec

./script.sh        # needs +x and exec-allowed mount
bash script.sh     # only needs read

If bash script.sh works, you are missing +x or hitting noexec.

Step 2: Check the file mode and ownership

ls -l ./script.sh
id

No x, or ownership you do not match, narrows it quickly.

Step 3: Walk the full path for traverse permission

namei -l ./script.sh

Any parent directory without x for you blocks the whole path.

Step 4: Check mount options for noexec

findmnt -no OPTIONS -T ./script.sh

noexec in the output explains denial despite a correct +x.

Step 5: Inspect ACLs and MAC denials

getfacl ./script.sh
ls -Z ./script.sh                       # SELinux label
sudo dmesg | grep -i 'denied\|apparmor' | tail

Example Root Cause Analysis

A deployment job downloads a release script into /tmp, chmod +x’s it, and runs it. It fails every time:

/tmp/install.sh: Permission denied

The execute bit is clearly set:

ls -l /tmp/install.sh
-rwxr-xr-x 1 deploy deploy 1840 Jun 23 11:20 /tmp/install.sh

So mode is not the problem. Checking the mount:

findmnt -no OPTIONS -T /tmp/install.sh
rw,nosuid,nodev,noexec,relatime

The host hardens /tmp with noexec (a common CIS-benchmark setting). The kernel refuses to exec any file there regardless of its mode bits — the chmod +x was irrelevant.

Fix: stage the script in an exec-allowed directory, or invoke it through the interpreter:

install -m 0755 /tmp/install.sh /usr/local/bin/install.sh
/usr/local/bin/install.sh
# or, without moving:
bash /tmp/install.sh

The script runs from the exec-permitted location.

Prevention Best Practices

  • Set the execute bit in version control (git update-index --chmod=+x script.sh) so checkouts arrive runnable and CI does not need an extra chmod step.
  • Never stage executables in /tmp or /dev/shm on hardened hosts; they are routinely mounted noexec. Use /usr/local/bin or a dedicated app directory.
  • Audit directory traversal permissions with namei -l when access fails — a single 750 parent dir blocks everything beneath it.
  • Be explicit about whether you ./script (needs +x) or bash script (needs read); pick one convention per pipeline.
  • On SELinux/AppArmor systems, run restorecon after copying scripts into managed locations, and check ausearch/dmesg before assuming a Unix-permission cause.
  • For diagnosing denials surfacing in automation logs, the free incident assistant can distinguish a missing exec bit from a noexec mount or MAC denial.

Quick Command Reference

# Direct vs interpreter exec (isolates +x / noexec)
./script.sh ; bash script.sh

# Mode and ownership
ls -l ./script.sh ; id

# Full path traverse permissions
namei -l ./script.sh

# Mount options (noexec?)
findmnt -no OPTIONS -T ./script.sh

# ACLs and SELinux label
getfacl ./script.sh ; ls -Z ./script.sh

# Fixes
chmod +x ./script.sh
setfacl -m mask::rwx ./script.sh
sudo restorecon -v ./script.sh

Conclusion

Permission denied running a script is the kernel vetoing exec across one of several layers. The recurring causes:

  1. The execute bit is missing (./script denied, bash script works).
  2. The filesystem is mounted noexec (common on hardened /tmp).
  3. A parent directory lacks traverse (x) permission for you.
  4. The script’s own read/exec bits exclude your user.
  5. A POSIX ACL or mask overrides the visible mode.
  6. SELinux/AppArmor denies execution despite correct Unix permissions.

Start by comparing ./script against bash script, then walk outward — mode, namei -l path, findmnt for noexec, then ACLs and MAC. The fix is usually a chmod +x, a move off a noexec mount, or a restorecon. More patterns in the Bash & Python automation guides.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.