Operation not permitted

Fix 'Operation not permitted' on Linux when running cron jobs

Linux & Unix Intermediate 👁 0 views 📅 May 26, 2026

Cron jobs hitting permission denied? Here's why and how to fix it without guessing.

You've set up a cron job that runs a script every hour. The script works fine when you run it manually from the terminal. But when cron fires it, you check the logs (/var/log/syslog or journalctl -u cron) and see 'Operation not permitted' on a file operation or system call. No other error. Just that cold wall.

What's actually happening here is that cron runs your job in a restricted environment. The key restriction: cron inherits a limited set of capabilities. If your script needs CAP_NET_RAW (for raw sockets), CAP_SYS_ADMIN (for certain kernel operations), or tries to use chroot, mount, or setuid without the right capability, the kernel slaps back with Operation not permitted. It's not a password issue or a file ownership problem — it's the kernel saying "you don't have the right to do that from this context".

Another common trigger: AppArmor or SELinux policies are stricter when a process is spawned by cron. Cron processes often run under a different security context than your interactive shell. So a script that works fine from your SSH session gets blocked when cron fires it.

Root cause

Cron doesn't grant all capabilities by default. The pam_cap.so module (if used) can inject capabilities, but most distros don't do that for cron. And SELinux/AppArmor policies may prevent certain operations for cron-spawned processes. The fix usually involves either granting the missing capability, changing the security context, or rethinking the approach if the operation is inherently privileged.

Steps to fix

  1. Check which operation fails. Run strace -f -o /tmp/strace.log your_script.sh from cron (or manually with the same user). Look for the syscall that returns EPERM. The syscall name tells you what capability you need. For example, socket(AF_INET, SOCK_RAW, ...) failing means you need CAP_NET_RAW.
  2. Add capabilities to the script. You have two options:
    Option A: Use setcap on the binary the script calls:
    sudo setcap 'cap_net_raw+ep' /usr/bin/your-binary
    This works if the binary is a compiled executable. For shell scripts, you need to setcap on the interpreter (/bin/bash) — but that's insecure because it gives capabilities to all bash scripts. Better: wrap the script in a small C program or use capsh.
    Option B: Use capsh inside the cron job:
    */5 * * * * /usr/sbin/capsh --caps='cap_net_raw+ep' --keep=1 -- -c '/path/to/your/script.sh'
    This spawns the script with the capability. Test first: run capsh --print to see current caps.
  3. Check SELinux. If you're on RHEL/CentOS/Fedora, run ausearch -m avc -ts recent or grep denied /var/log/audit/audit.log. An AVC denial tells you exactly what rule is blocking the operation. You can generate a policy module with audit2allow -M mycronpolicy, then install it with semodule -i mycronpolicy.pp. Don't use setenforce 0 in production — that disables SELinux entirely.
  4. Check AppArmor. On Ubuntu/Debian, run aa-status to see which profiles are loaded. Then grep DENIED /var/log/syslog for AppArmor denials. You can add a rule to /etc/apparmor.d/local/ or use aa-complain to test in permissive mode.
  5. Try running as root (temporary). If the script needs root privileges, add the cron job to root's crontab (sudo crontab -e). This bypasses capability restrictions but increases risk. Only do this if you understand the security implications.
  6. Use systemd timers instead. Systemd gives you finer control over capabilities and security contexts. Create a service unit with CapabilityBoundingSet= and AmbientCapabilities= directives. Then a timer unit triggers it. This is the modern way and more predictable than cron.

If it still fails

Check for no_new_privs flag. Some distros set this for cron-spawned processes. Run cat /proc/$(pgrep -f your_script)/status | grep NoNewPrivs. If it's 1, setcap won't work. You'll need to run the job outside cron (systemd timer or a daemon). Also verify the script user has read+execute on all binaries and directories in the chain. And double-check that the cron environment doesn't override PATH — it often does, so your script might be calling the wrong binary.

Final thought: if you're fighting cron for more than 30 minutes, switch to systemd timers. They're less opaque, easier to debug, and give you proper access to capabilities and security policies.

Was this solution helpful?