Skip to content

PersistenceExecution

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

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

ArtifactDebian / UbuntuRHEL / 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.monthlysame
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.denysame
Timer units*.timer in the systemd unit paths (see systemd units)same
Persistent timer stamps/var/lib/systemd/timers/stamp-<name>.timersame
Transient timers (systemd-run --on-*)/run/systemd/transient/, /run/user/<uid>/systemd/transient/same
Execution logCRON[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 CMD log 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=true timer.
  • Who edited a crontab: crontab[pid]: (user) BEGIN EDIT / REPLACE / END EDIT lines 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.d names containing a dot).

Key fields

ItemMeaning
m h dom mon dowFive schedule fields in every crontab line
user fieldSixth field, only in /etc/crontab and /etc/cron.d/*
@reboot, @hourly, @daily ...Shorthand schedules; @reboot is a favourite for persistence
MAILTO, SHELL, PATH, CRON_TZEnvironment lines inside a crontab; CRON_TZ (cronie) changes the schedule time zone
anacrontab period delay job-identifier commandDays between runs, delay in minutes, stamp file name, command
START_HOURS_RANGE, RANDOM_DELAYAnacron run window and jitter
at job fileShell 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 crontab rewrites the spool file, its mtime is the last install time. On Debian, a file installed by crontab starts with a DO NOT EDIT THIS FILE header 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; touch can backdate mtime but not ctime.
  • A new script dropped into /etc/cron.daily needs no crontab edit and produces only the generic run-parts CMD line, so review the directory contents, not just tables.
  • Frequent schedules (* * * * *), @reboot, curl/wget piped to a shell, base64 blobs and paths in /tmp, /dev/shm or 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/user only 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.

See also