Run as the right user
The most common cron mistake is running everything as root. Cron jobs inherit the full privileges of the user that owns the crontab. A bug in a script that runs as root can wipe out the system; the same bug as a low-privilege user is contained.
Principle of least privilege
- Each cron job should run as a dedicated user with only the permissions it needs.
- Database backup → user with read-only DB access
- Log shipper → user with read access to /var/log and write to the shipper output
- Application maintenance → the application's own user, not root
How to use a non-root user
# Create a user for backup jobs sudo useradd -r -s /bin/bash -d /var/lib/backup backup # Edit that user's crontab sudo crontab -u backup -e # OR put the job in /etc/cron.d/backup with explicit user: # /etc/cron.d/backup: 0 2 * * * backup /usr/local/bin/backup.sh
The 6th field in /etc/cron.d/* and /etc/crontab is the user. Personal crontabs (crontab -e) implicitly use the editing user.
File and directory permissions
The script itself
Cron scripts should be readable and executable by their owner only, never world-writable:
# Good -rwx------ 1 backup backup 1234 May 14 10:00 /usr/local/bin/backup.sh # Bad — anyone can rewrite this and pwn the system -rwxrwxrwx 1 root root 1234 May 14 10:00 /usr/local/bin/backup.sh
chmod 700 /usr/local/bin/backup.sh chown backup:backup /usr/local/bin/backup.sh
The crontab files themselves
ls -la /etc/cron.d/ ls -la /var/spool/cron/crontabs/
These should be readable only by root. If a user can edit another user's crontab file, they can run arbitrary code as that user.
The PATH
If a cron job uses commands without absolute paths and PATH includes a writable directory (like . or /tmp), an attacker can shim binaries. Always:
- Set an explicit, restrictive PATH at the top of your crontab
- Or use absolute paths to every binary in your scripts
Handling secrets and credentials
Never put secrets in the crontab
Bad:
0 * * * * curl -u admin:SuperSecret123 https://api.example.com/...
The crontab is world-readable on many systems; even when it's not, anyone with shell access can run ps and see the full command line during execution.
Use environment files outside the crontab
Store secrets in a separate file readable only by the cron user:
# /etc/cron-secrets/backup.env (chmod 600) DATABASE_PASSWORD=... S3_SECRET_KEY=...
# In your script source /etc/cron-secrets/backup.env pg_dump -h db -U app | gzip | aws s3 cp - s3://bucket/dump.sql.gz
Better: use a secret manager
HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, or systemd's LoadCredential. The script fetches credentials at runtime, so they're never on disk:
export AWS_PROFILE=backup export DB_PASS=$(aws secretsmanager get-secret-value --secret-id prod/db/password --query SecretString --output text) pg_dump ...
Avoiding command injection
Cron jobs that incorporate user-controllable data (filenames, URLs, anything from a database or HTTP request) need the same care as web servers:
Bad: unquoted variables
# If $filename contains "; rm -rf /", this evaluates as a separate command mv $filename /processed/
Good: quote everything
mv "$filename" /processed/
Better: use safer interfaces
# Don't: result=$(curl http://api.example.com/items/$user_id) # Do (use printf %q to escape, or pass via stdin/array): curl --data-urlencode "user=$user_id" http://api.example.com/items
For Python/Ruby/Node cron jobs
Never os.system() with concatenated strings. Use subprocess.run([...]) with a list (Python), or the equivalent in your language. Never shell=True with user input.
Auditing what cron jobs exist
Cron jobs accumulate. Old jobs that no one remembers but still run are a common source of "wait, what is using up our quota?" surprise.
Find every cron job on a system
# All user crontabs
for u in $(cut -d: -f1 /etc/passwd); do
crontab -u "$u" -l 2>/dev/null | grep -v '^#' | grep -v '^$' \
| sed "s/^/[$u] /"
done
# System-wide
cat /etc/crontab
ls /etc/cron.d/
ls /etc/cron.{hourly,daily,weekly,monthly}/
# systemd timers
systemctl list-timers --all
Catalog and document
For every cron job, document somewhere central (a wiki, a runbook, a YAML file in your infra repo):
- What does it do?
- Who owns it?
- What's the schedule?
- How do you know it's working?
- How do you disable it safely?
If a job's owner has left the company and no one knows what it does, that's a security risk in itself.
Security checklist
- ☐ Runs as a dedicated low-privilege user (not root)
- ☐ Script is owned and writable only by that user (chmod 700)
- ☐ Secrets are in a separate, mode 600 file or a secret manager
- ☐ No secrets appear in
psoutput (use stdin, env, or runtime fetch) - ☐ PATH is set explicitly and doesn't include writable dirs
- ☐ User input is quoted/escaped throughout
- ☐ Output is logged (so a compromise leaves traces)
- ☐ The job is documented and the owner is known