Cron jobs, those trusty schedulers on Linux, can become security holes faster than you can say sudo. The real risk isn’t just that a script runs at a certain time, but as whom it runs, and what that user can do.

Let’s watch a cron job in action, not by describing it, but by seeing it.

Imagine we have a simple script that backs up a configuration file.

backup_config.sh:

#!/bin/bash
cp /etc/myapp/config.conf /var/backups/myapp/config.conf.$(date +%Y%m%d_%H%M%S)
echo "Backup created: /var/backups/myapp/config.conf.$(date +%Y%m%d_%H%M%S)" >> /var/log/backup.log

Now, we want this to run daily at 2 AM. A common, but potentially risky, way is to add it to the root user’s crontab:

sudo crontab -e

And add the line:

0 2 * * * /usr/local/bin/backup_config.sh

When 2 AM rolls around, this script executes with full root privileges. If backup_config.sh had a vulnerability, or if someone modified it, they could do anything on the system. That’s the core problem: cron jobs often run with more power than they need.

The principle of least privilege is your best friend here. Instead of running as root, let’s create a dedicated user for this task.

sudo useradd -r -s /sbin/nologin backupuser
sudo mkdir -p /var/backups/myapp
sudo chown backupuser:backupuser /var/backups/myapp
sudo chmod 700 /var/backups/myapp

We’ve created a system user (-r) that can’t log in (-s /sbin/nologin), ensuring it can’t be used for interactive sessions. We then set up the backup directory and make sure only backupuser can write to it.

Next, we need to ensure the script itself is owned by backupuser and is executable.

sudo chown backupuser:backupuser /usr/local/bin/backup_config.sh
sudo chmod 700 /usr/local/bin/backup_config.sh

Now, the crucial part: adding the job to backupuser’s crontab.

sudo crontab -u backupuser -e

And add the line:

0 2 * * * /usr/local/bin/backup_config.sh

This script now runs only as backupuser. It has permissions to write to /var/backups/myapp and /var/log/backup.log (we’d need to ensure backupuser also has write access to the log file, typically by chown backupuser:backupuser /var/log/backup.log and chmod 600 /var/log/backup.log). It cannot modify system files, install software, or do anything else that requires root privileges. If the script is compromised, the damage is limited to what backupuser can do.

The MAILTO variable in crontabs is often overlooked. By default, cron sends output (stdout and stderr) to the user’s mailbox. If you don’t want this, or want to direct it elsewhere, set MAILTO="" at the top of the crontab.

MAILTO=""
0 2 * * * /usr/local/bin/backup_config.sh

This prevents email notifications for every script execution, which can quickly flood a mailbox.

You might also want to ensure that your cron job doesn’t run if the system is under heavy load, or if it’s already running. For simple cases, you can use a lock file.

Modify backup_config.sh:

#!/bin/bash
LOCKFILE="/tmp/backup_config.lock"

if [ -f "$LOCKFILE" ]; then
    echo "Lock file $LOCKFILE exists. Exiting." >> /var/log/backup.log
    exit 1
fi

touch "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT

cp /etc/myapp/config.conf /var/backups/myapp/config.conf.$(date +%Y%m%d_%H%M%S)
echo "Backup created: /var/backups/myapp/config.conf.$(date +%Y%m%d_%H%M%S)" >> /var/log/backup.log

exit 0

The trap "rm -f $LOCKFILE" EXIT ensures the lock file is removed when the script exits, whether normally or due to an error.

The most surprising thing about cron job security is how often the simplest, most direct approach is also the riskiest. Running a script as root because it’s the easiest way to give it access to everything it might need is a classic security anti-pattern. It’s like giving a janitor a master key to the entire building when they only need access to the supply closet.

You can also use cron.d directories for managing cron jobs, which can be cleaner than individual user crontabs, especially for system-wide scheduled tasks. Files in /etc/cron.d/ are also associated with a specific user, defined on the line itself, which can add another layer of explicit control. For example, a file /etc/cron.d/myapp_backup could contain:

0 2 * * * backupuser /usr/local/bin/backup_config.sh

This makes it very clear which user is intended to run the job, without needing to edit backupuser’s crontab directly.

The next hurdle you’ll encounter is managing secrets within your cron jobs. If your script needs passwords or API keys, storing them directly in the script or even in a readable file owned by backupuser is still a risk.

Want structured learning?

Take the full Cdk course →