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
- Check which operation fails. Run
strace -f -o /tmp/strace.log your_script.shfrom cron (or manually with the same user). Look for the syscall that returnsEPERM. The syscall name tells you what capability you need. For example,socket(AF_INET, SOCK_RAW, ...)failing means you needCAP_NET_RAW. - Add capabilities to the script. You have two options:
Option A: Usesetcapon the binary the script calls:
This works if the binary is a compiled executable. For shell scripts, you need tosudo setcap 'cap_net_raw+ep' /usr/bin/your-binarysetcapon 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 usecapsh.
Option B: Usecapshinside the cron job:
This spawns the script with the capability. Test first: run*/5 * * * * /usr/sbin/capsh --caps='cap_net_raw+ep' --keep=1 -- -c '/path/to/your/script.sh'capsh --printto see current caps. - Check SELinux. If you're on RHEL/CentOS/Fedora, run
ausearch -m avc -ts recentorgrep denied /var/log/audit/audit.log. An AVC denial tells you exactly what rule is blocking the operation. You can generate a policy module withaudit2allow -M mycronpolicy, then install it withsemodule -i mycronpolicy.pp. Don't usesetenforce 0in production — that disables SELinux entirely. - Check AppArmor. On Ubuntu/Debian, run
aa-statusto see which profiles are loaded. Thengrep DENIED /var/log/syslogfor AppArmor denials. You can add a rule to /etc/apparmor.d/local/ or useaa-complainto test in permissive mode. - 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. - Use systemd timers instead. Systemd gives you finer control over capabilities and security contexts. Create a service unit with
CapabilityBoundingSet=andAmbientCapabilities=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.