Cron, Anacron, at and systemd Timers: Linux Scheduling
Linux scheduled task artifacts (crontabs, anacron stamps, at spool jobs, systemd timers) that reveal scheduled persistence and prove when jobs ran.
- Location
- /var/spool/cron/
- Proves
- Which commands were scheduled to run, by which account, and when cron, anacron or a timer last fired them
- Timestamps
- File mtime/ctime; syslog or journal time for runs; anacron stamps as YYYYMMDD local date
- Access
- root for all spool directories; any user for their own crontab via crontab -l
- Retention
- Until deleted; execution evidence follows syslog/journal rotation
- Collection
- UAC, Velociraptor, cp -a, journalctl
Tools
Compare all tools- VelociraptorPlatform · open source
- UACCLI · open source
- grep / zgrepCLI · built into Linux
- systemctlCLI · built into Linux
What it is
Linux has four overlapping schedulers. cron (Vixie cron on Debian and Ubuntu, cronie on the RHEL family) runs recurring jobs from system and per-user tables. anacron runs daily, weekly and monthly jobs on machines that are not always on, and remembers the last run date. at (atd) runs one-shot jobs. systemd timers are .timer units that activate a service on a calendar or monotonic schedule, and they now drive much of the housekeeping that cron used to do.
All four are plain files on disk, which makes them easy to collect and easy for an intruder to abuse (MITRE ATT&CK T1053.003 cron, T1053.002 at, T1053.006 systemd timers).
Where it lives
| Artifact | Debian / Ubuntu | RHEL / Fedora / Rocky / Alma |
|---|---|---|
| System table (has a user field) | /etc/crontab | /etc/crontab |
| Drop-in system tables | /etc/cron.d/* | /etc/cron.d/* (includes 0hourly) |
| run-parts script directories | /etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly, /etc/cron.monthly | same |
| User crontabs (no user field) | /var/spool/cron/crontabs/<user> | /var/spool/cron/<user> |
| Anacron config | /etc/anacrontab | /etc/anacrontab |
| Anacron last-run stamps | /var/spool/anacron/<job-id> | /var/spool/anacron/<job-id> |
| at jobs | /var/spool/cron/atjobs/ (plus atspool/) | /var/spool/at/ (plus .SEQ) |
| Access control | /etc/cron.allow, /etc/cron.deny, /etc/at.allow, /etc/at.deny | same |
| Timer units | *.timer in the systemd unit paths (see systemd units) | same |
| Persistent timer stamps | /var/lib/systemd/timers/stamp-<name>.timer | same |
Transient timers (systemd-run --on-*) | /run/systemd/transient/, /run/user/<uid>/systemd/transient/ | same |
| Execution log | CRON[pid] lines in /var/log/syslog or the journal | /var/log/cron or the journal |
On RHEL, /etc/cron.d/0hourly runs /etc/cron.hourly, and a script there starts anacron, which then handles the daily, weekly and monthly directories. On Debian, /etc/crontab calls run-parts for these directories unless anacron is installed.
What it proves
- Scheduled persistence: which command is set to run, how often, and (for system tables) as which user. User crontabs run as the file owner.
- Execution: a
CMDlog line proves cron started the command at that minute. It does not prove the command succeeded. - Last periodic run: an anacron stamp records the last date a job ran; a timer stamp's mtime records the last trigger of a
Persistent=truetimer. - Who edited a crontab:
crontab[pid]: (user) BEGIN EDIT / REPLACE / END EDITlines in the log. - Not proven: a job file's presence alone does not show it ever ran (the daemon may be disabled, or the file ignored; Debian cron skips
/etc/cron.dnames containing a dot).
Key fields
| Item | Meaning |
|---|---|
m h dom mon dow | Five schedule fields in every crontab line |
| user field | Sixth field, only in /etc/crontab and /etc/cron.d/* |
@reboot, @hourly, @daily ... | Shorthand schedules; @reboot is a favourite for persistence |
MAILTO, SHELL, PATH, CRON_TZ | Environment lines inside a crontab; CRON_TZ (cronie) changes the schedule time zone |
anacrontab period delay job-identifier command | Days between runs, delay in minutes, stamp file name, command |
START_HOURS_RANGE, RANDOM_DELAY | Anacron run window and jitter |
| at job file | Shell script: captured environment, cd to the original working directory, command at the end |
.timer OnCalendar=, OnBootSec=, OnActiveSec=, OnUnitActiveSec= | Calendar or monotonic trigger |
.timer Unit=, Persistent= | Activated unit (default: same base name .service); catch-up after downtime |
Timestamps
- Crontab and job files carry only file system times. Because
crontabrewrites the spool file, its mtime is the last install time. On Debian, a file installed bycrontabstarts with aDO NOT EDIT THIS FILEheader that names the temporary file and the install date; a spool file without that header was likely written directly. - Anacron stamps contain the last run date as
YYYYMMDD(no time), in local time. - Timer stamp files are empty; the information is the file mtime.
- Log lines follow the syslog format (see syslog and auth.log); journal entries store microseconds since the Unix epoch in UTC.
stat -c '%y %z %n' /mnt/evidence/var/lib/systemd/timers/stamp-*.timer
cat /mnt/evidence/var/spool/anacron/cron.daily # e.g. 20260927
Retention
Crontabs, pending at jobs, timers and anacron stamps persist until deleted. Anacron never deletes its own stamps, so stale stamps can reveal jobs that were later removed from /etc/anacrontab. Transient timers under /run are lost at reboot. Execution evidence (CMD lines, unit start messages) lasts only as long as syslog rotation or journal size limits allow.
Collection
UAC collects /etc in full and /var/spool/cron, /var/spool/anacron, /var/spool/at through its job scheduler artifact; its systemd artifact adds unit directories and transient timers. On a live host it also records systemctl list-timers --all.
# dead box, read-only mount
tar -C /mnt/evidence -czf sched.tgz etc/crontab etc/cron.* etc/anacrontab \
var/spool/cron var/spool/anacron var/spool/at var/lib/systemd/timers 2>/dev/null
# live
systemctl list-timers --all # NEXT, LEFT, LAST, PASSED, UNIT, ACTIVATES
journalctl -D /mnt/evidence/var/log/journal SYSLOG_IDENTIFIER=CRON -o short-iso # CROND on RHEL
Velociraptor's Linux.Sys.Crontab parses crontabs and lists the run-parts scripts.
Parsing
Crontabs are text: strip comments and read every line.
grep -HvE '^\s*(#|$)' /mnt/evidence/etc/crontab /mnt/evidence/etc/cron.d/* \
/mnt/evidence/var/spool/cron/crontabs/* /mnt/evidence/var/spool/cron/* 2>/dev/null
tail -n 5 /mnt/evidence/var/spool/cron/atjobs/* /mnt/evidence/var/spool/at/a* 2>/dev/null
grep -hE 'CRON\[|CROND\[|crontab\[' /mnt/evidence/var/log/syslog* /mnt/evidence/var/log/cron*
For timers, read each .timer and the unit it activates, then look for the matching start messages in the journal.
Investigator tips
- Sort every scheduler file by ctime, not mtime;
touchcan backdate mtime but not ctime. - A new script dropped into
/etc/cron.dailyneeds no crontab edit and produces only the genericrun-partsCMDline, so review the directory contents, not just tables. - Frequent schedules (
* * * * *),@reboot,curl/wgetpiped to a shell, base64 blobs and paths in/tmp,/dev/shmor dot-directories are classic indicators. See /tmp and /dev/shm. - A crontab edit with no matching
crontab[pid]log line suggests the spool file was written directly. - Service accounts (
www-data,apache,postgres) rarely have crontabs; one appearing after a web compromise is a strong lead. Correlate with web server logs. - User timers under
~/.config/systemd/useronly run while the user is logged in, unless lingering is enabled (/var/lib/systemd/linger/<user>). - Tie each job to execution with journal or syslog entries and, where available, auditd EXECVE records.