Best practices

Cron security best practices.

Cron jobs run with their user's full privileges, often unattended and at odd hours. A compromised cron job can be a quiet, persistent foothold for an attacker. This guide covers the security considerations specific to scheduled jobs.

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 ps output (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
Related

Continue reading.