Handbook sections▾
Foundations
Understanding the threats to a Linux hostWhy we hardenThe defense principles behind every ruleEffective configuration: the truth no file holdsThe standards Pavois maps toSOCLE control ID modelEvidence & exportsHow the A-E grade is computedWhat a PASS proves: the qualified verdictTrust model for the evidence bundleIs SOCLE just your own norm? (governance & the circularity question)What Pavois covers, and what it doesn'tDomains
Hardening SSHHardening PAMMandatory Access Control (SELinux / AppArmor)Hardening the host firewallHardening sudoFile Permissions & OwnershipMount & filesystem hardeningHardening kernel modulesHardening the kernel & network with sysctlAudit logging with auditdHardening logging with journald and rsyslogHardening systemd servicesPackage hygieneHardening the bootloader (GRUB)Synchronizing timeLogin banners and MOTDControlling cron and at accessHardening the GNOME desktop (dconf)Tooling
Undo a hardening run: restore pointsOperating Pavois: privileges, air-gap, timing, exceptionsRun Pavois in CI (GitHub Actions)Feature status: delivered, partial, roadmapGovernance: licence, versioning, provenance, securityControlling cron and at access
Last reviewed
Scheduled jobs are an attacker's favourite way to stay. Lock down who may use cron and at with a default-deny allow-list so a foothold can't quietly become persistence.
The threat: scheduled jobs are turnkey persistence
Getting code onto a machine is only step one for an attacker; staying is the real prize. Scheduled jobs are the classic persistence mechanism: they are built into every Linux box, they survive reboots, and they run on their own with nobody logged in. The MITRE ATT&CK technique T1053.003 (Scheduled Task/Job: Cron) describes exactly this: drop a crontab entry or an at job and your payload re-launches every minute, every reboot, forever.
The danger is amplified by how permissive the defaults are. On many systems any user can write a crontab unless an allow-list says otherwise. If cron and at access is wide open, a compromised low-privilege account (a web service, a CI runner, a stale developer login) can schedule a beacon or a re-infection job nobody watches. A cron.deny-based model makes this worse: it blocks only named users, so any account you forgot to list is implicitly allowed.
Why harden it
Job scheduling is privilege that should be granted, not assumed. Flip the default from everyone may schedule to only an explicit, auditable set of accounts may schedule. That removes a turnkey persistence path from every compromised service account, shrinks the set of identities that can run code unattended, and makes scheduling an explicit, reviewable decision rather than a side effect of having a shell. Most ordinary users never need it, so the allow-list is usually short, and an empty-but-present cron.allow is a perfectly valid, very strong posture.
How does the allow/deny logic work?
The decision is allow-first, and the presence of cron.allow changes everything:
| State of the files | Who may use cron |
|---|---|
cron.allow exists |
only users listed in it (cron.deny is ignored) |
only cron.deny exists |
everyone except users listed in it (fail-open) |
| neither exists | distro-dependent: root only (Debian) or everyone (others) |
The hardened posture is therefore: create cron.allow (listing root and the few accounts that truly need it) and remove cron.deny. at uses the identical logic with at.allow / at.deny. Note that an allow-list gates user crontabs (crontab -e); the system crontab (/etc/crontab, /etc/cron.d, /etc/cron.daily...) runs regardless, which is why their file permissions matter just as much.
How do you set it up?
# cron: explicit allow-list, remove the fail-open deny-list
echo root > /etc/cron.allow
chown root:root /etc/cron.allow && chmod 600 /etc/cron.allow
rm -f /etc/cron.deny
# at: same model
echo root > /etc/at.allow
chown root:root /etc/at.allow && chmod 600 /etc/at.allow
rm -f /etc/at.deny
# lock the system cron surface (root-only)
chown root:root /etc/crontab /etc/cron.d /etc/cron.{hourly,daily,weekly,monthly}
chmod 600 /etc/crontab && chmod 700 /etc/cron.d /etc/cron.{hourly,daily,weekly,monthly}What Pavois audits: the live filesystem state
Pavois's Cron/at access control domain enforces the default-deny model on the live system:
file-cron-allow-exists:/etc/cron.allowmust exist (cron runs in allow-list mode, not the permissive default).file-at-allow-exists:/etc/at.allowmust exist, same model forat/batch.file-cron-deny-absent:/etc/cron.denymust not exist, removing the fail-open deny-list.file-at-deny-absent:/etc/at.denymust not exist, same reason on theatside.
Each control inspects the real filesystem with InSpec's file(...) { should exist / should_not exist }, so the verdict reflects what cron and at actually read at runtime. The companion ownership and permission checks on /etc/cron.allow, the cron.{d,daily,hourly} directories and crontab files live in Pavois's permissions and ownership domains, ensuring the access policy can't be tampered with by the users it restricts.
Verify
ls -l /etc/cron.allow /etc/at.allow # present, root:root, 0600
ls /etc/cron.deny /etc/at.deny 2>&1 # should report 'No such file'
stat -c '%a %U' /etc/crontab /etc/cron.d # 600/700, root
# as a non-listed user, this must be refused:
sudo -u nobody crontab -ePitfalls
- An empty
cron.allowdenies everyone, including admins, from user crontabs. That is intended; putroot(and any real scheduler account) in it. System cron (/etc/cron.d,/etc/crontab) still runs. cron.allowwins overcron.deny: listing a user in both allows them. Don't rely ondenyonceallowexists.- Removing
cron.denywithout creatingcron.allowcan, on some distros, fall back to everyone allowed. Create the allow-list first. - The allow-list does not inspect job content: it controls who may schedule, not what. Pair it with monitoring of the cron directories.
FAQ
cron.allow or cron.deny? cron.allow. It is default-deny (only listed users), whereas cron.deny is fail-open (anyone not named is allowed), so a forgotten account slips through.
Will an empty cron.allow stop system cron jobs? No. /etc/crontab and /etc/cron.d run regardless; the allow-list only gates per-user crontab. Lock the system files with permissions instead.
Why does Pavois check file existence, not a config value? Because cron/at decide access purely from whether these files exist and who they list; the live filesystem is the policy.