Scheduled-task hardening
cron & timer privilege boundaries and PATH hijack.
Scheduled tasks are a favourite of attackers on two fronts: as a privilege-escalation path (a root-run job with a weak file permission somewhere in its path) and as persistence (a job that quietly re-launches a payload). The essentials course showed you how to write a cron entry; hardening is about making sure the jobs on your host cannot be turned against you, and knowing exactly which ones exist so a new one stands out.
The writable-script escalation
The classic escalation: a root cron job runs /opt/app/backup.sh, and that script — or the directory containing it — is writable by a lower-privileged user. The attacker edits the script, waits for cron to run it as root, and now has root. The defense is ownership and permissions: every file a privileged job executes, and every directory in its path, must be owned by root and not writable by anyone else. Audit this for each root-scheduled task.
# audit every root-run scheduled task, then check that what it runs is not writable$ cat /etc/crontab; ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/$ sudo crontab -l # root’s own crontab# every script a root job runs should be root-owned and NOT group/world-writable:$ ls -l /opt/app/backup.sh-rwxr-xr-x 1 root root ... /opt/app/backup.sh # good: only root can edit it$ find /etc/cron* -perm -0002 -o -perm -0020 # any group/world-writable cron content = finding
PATH hijack in scheduled jobs
A subtler version: a root job runs a script that calls a command by bare name (backup instead of /usr/local/bin/backup), and PATH includes a directory an attacker can write to earlier than the real binary. The attacker plants a malicious backup in that directory and cron runs it as root. Two defenses: set a minimal, absolute PATH at the top of the crontab, and require scripts to call commands by full path. This is the same PATH-hijack idea from the privilege-escalation material, applied to automation.
# set a safe, minimal PATH so a job cannot resolve a command from a writable dirPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binSHELL=/bin/bash# and inside scripts, call binaries by absolute path: /usr/bin/python3, not python3
Least privilege and systemd timers
Run scheduled work as the least-privileged user that can do it, not root by reflex — a backup that only needs to read /var/www and write to a backup volume does not need root. And prefer systemd timers over cron for anything you are hardening: a timer runs a service unit, so it inherits all the per-service confinement from the systemd lesson (User=, NoNewPrivileges, ProtectSystem, private tmp), the runs are logged in the journal, and the schedule and the job are separate, reviewable units. cron is fine for simple jobs; timers are the hardened choice.
# backup.service — the job, confined like any hardened service[Service]Type=oneshotUser=backup # least privilege, not rootNoNewPrivileges=trueProtectSystem=strictExecStart=/usr/local/bin/backup.sh# backup.timer — the schedule (systemctl enable --now backup.timer)[Timer]OnCalendar=*-*-* 03:00:00 # 3am daily; journalctl -u backup.service shows every run