Scheduling tasks with cron
Run things automatically on a schedule.
Every server has chores that have to happen whether or not a person is logged in: rotate the logs at midnight, back up the database at 3am, delete yesterday's temp files, check that a service is still breathing. Doing all of that by hand would mean sitting at the keyboard forever. cron is the fix. The name comes from chronos, the Greek word for time, and the program itself is a daemon (a piece of software that runs quietly in the background with no window and no prompt, waiting for its moment). It wakes up once every minute, looks at a list of jobs you have handed it, and runs anything whose scheduled time has arrived. It works like a reliable assistant who checks the clock every sixty seconds and does whatever your sticky notes told them to do.
The five-field schedule
You write those sticky notes in a crontab (short for cron table), and every line has the same shape: five time boxes, then the command to run. The five boxes work like the repeat settings on a phone alarm, where you pick the minute, the hour, and which days it should fire. Left to right they are minute, hour, day of the month, month, and day of the week. A star (*) in a box means every value that fits there, so a star in the hour box means every hour.
# ┌───────────── minute (0-59)# │ ┌─────────── hour (0-23)# │ │ ┌───────── day of month (1-31)# │ │ │ ┌─────── month (1-12)# │ │ │ │ ┌───── day of week (0-6, Sunday = 0 or 7)# │ │ │ │ │# * * * * * command to run0 3 * * * /opt/app/backup.sh # 3:00 AM every day*/15 * * * * /usr/local/bin/health-check.sh # every 15 minutes0 0 1 * * /opt/app/monthly-report.sh # midnight on the 1st
Once you can read the boxes, you can read any schedule. 0 3 * * * is minute 0 of hour 3, every day, so 3am nightly. */15 * * * * uses a step: the /15 means every fifteenth minute, so it runs at :00, :15, :30, and :45 forever. You can also give a range like 1-5 (Monday to Friday in the day-of-week box) or a list like 1,15 (the 1st and the 15th). Put those pieces together and 30 8 * * 1-5 reads as 8:30am, Monday through Friday.
One combination trips up almost everyone. The day-of-month box and the day-of-week box look like they should team up, so you might read 0 0 13 * 5 as midnight on Friday the 13th. cron does the opposite. When both day boxes are restricted (neither is a star), the job fires when EITHER one matches. So 0 0 13 * 5 runs at midnight on the 13th of every month, and also at midnight every Friday. If you truly need Friday-the-13th-only, check the date inside your script and leave the schedule looser.
Editing your crontab
You do not open the crontab file directly. You use the crontab command, which is like handing your notes to the assistant instead of rummaging through their drawer. crontab -e opens your personal table in a text editor (your default one, set by $EDITOR or $VISUAL, often nano on Ubuntu); save the file and the new schedule is live that same minute, no reboot needed. crontab -l prints what you currently have. crontab -r wipes all of it, with no confirmation and no undo, which is uncomfortably close on the keyboard to -e.
cron also understands a few shorthand names for common schedules, and it lets you set variables at the top of the table. @daily (the same as 0 0 * * *) runs at midnight, @hourly at the top of every hour, and @reboot runs once each time the machine boots, which is handy for starting a helper that has no service file of its own. MAILTO says where a job's output should be emailed; set it to an address to get failure notices, or leave it out and know that output goes nowhere by default.
[email protected] # email this address with any outputPATH=/usr/local/bin:/usr/bin:/bin@reboot /usr/local/bin/start-tunnel.sh # once, at every boot@daily /usr/bin/find /tmp -mtime +7 -delete # same as 0 0 * * *
Where cron actually keeps things
Your own jobs live in a per-user file that you should never open by hand: on Debian and Ubuntu it sits in /var/spool/cron/crontabs/, owned by you and readable only by you (mode 600, so no other ordinary account can see what you scheduled). The file's group is set to crontab for one narrow reason: the crontab command runs with that group's rights so it can update the spool safely on your behalf. Alongside your personal table, the system has its own. /etc/crontab and the drop-in files in /etc/cron.d/ are edited directly by administrators and packages, and they carry one extra box: a username between the schedule and the command, saying which account the job runs as. That username field is the whole reason these files matter for security, and we will come back to it.
# /etc/crontab: system-wide crontab# These files have a username field that per-user crontabs do not.SHELL=/bin/shPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin# m h dom mon dow user command17 * * * * root cd / && run-parts --report /etc/cron.hourly25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
Why it works in your shell but dies under cron
Here is the failure that catches everyone. A script runs perfectly when you type its name, then does nothing under cron. The reason is that cron does not start in your comfortable shell. When you log in, your shell loads .bashrc and friends, builds a long PATH (the list of folders it searches for commands), and knows who you are. cron gives a job almost none of that. It runs /bin/sh with a bare PATH of roughly /usr/bin:/bin, no .bashrc, and little in the environment. So python3 might not be found, a variable your script expects is empty, and the job quietly fails.
Two habits fix most of it. Use full paths for everything, both the script and the programs inside it: write /usr/bin/python3 /opt/app/backup.py, not python3 backup.py. And capture the output, because by default cron tries to email it, and on a server with no mail sender that output is silently dropped. Redirect both streams, normal output (stdout) and error output (stderr), into a log file so a failure leaves a trace you can read.
# full paths + capture output, so a failure leaves a trace0 3 * * * /usr/bin/python3 /opt/app/backup.py >> /var/log/backup.log 2>&1# └ full path └ full path └ append stdout AND stderr
When a job misbehaves, the log you wrote is the first place to look, and the system log is the second. cron records every job it starts, so before you go hunting inside the script you can confirm whether the schedule even fired. journalctl is the command that reads the systemd journal (the central log where the system records what its services are doing).
The security angle
Two things make cron interesting to attackers and defenders alike. The first is persistence. When someone breaks into a machine, they want to survive a reboot and keep a foothold, and a cron job is a quiet way to do it: one line that re-downloads and runs their code every ten minutes. It shows up in the running-process list only for the second it runs, so during an incident review, every cron source is a place to check, not only the victim user's table but /etc/cron.d/, /etc/crontab, and the cron.daily and cron.hourly directories.
The second is privilege. Remember that username field on system jobs. A job that runs as root is trusted completely, so if the script it runs, or any folder on the path to that script, can be written by an ordinary user, that user can replace the script's contents and have their own code executed as root the next time cron fires. That is one of the most common real privilege-escalation paths on a badly configured box. The defenses are plain: run each job as the least-powerful account that can do the work, keep root's scripts owned by root and writable by nobody else, and use full paths so an attacker cannot slip a fake command earlier in the search order.
/opt/app/backup.sh is mode 777 (writable by everyone) and a root cron job runs it, any local user can append two lines to it: cp /bin/bash /tmp/rootbash; chmod u+s /tmp/rootbash. The next time root runs the script, those lines copy the shell to /tmp/rootbash and set its set-user-ID bit (the u+s flag, which makes a program run with its owner's identity, here root, no matter who launches it). After that any user can run /tmp/rootbash -p and get a root shell. Run find /etc/cron* /var/spool/cron -type f -perm -0002 to list the world-writable files under cron. There should be none.So when you land on an unfamiliar server, or you are cleaning up after an incident, enumerate every schedule in one pass. Read each user's crontab, list the drop-in directories, and eyeball the commands for anything that reaches out to the network. A web account like www-data (the low-privilege user the web server runs as) should almost never have a crontab at all, and a line that pipes a download straight into a shell is a bright red flag.
0 0 13 * 5. When does cron actually run it?One habit will save you real time: before you trust a new schedule, do not wait a whole day to find out you got the boxes wrong. Set it to */2 * * * * for a few minutes, watch the log fill, confirm it does the right thing, then change it back to the real time. cron runs exactly what you wrote, on time, forever, including your mistakes.
Try this
Work through “The security angle” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: a world-writable script in a root cron job is a root shell for anyone. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.