Description
crontab and at read an allow list and a deny list:
/etc/cron.allowand/etc/cron.denyforcrontab;/etc/at.allowand/etc/at.denyforat.
When the allow file exists, only the users it lists may schedule jobs, and the deny file is
ignored. The allow file must exist, belong to root, with group root or the group the tool
runs as (crontab for cron, daemon for at), and mode 0640 or stricter. A deny file, if
present, must have the same ownership and mode.
The checks run only for the tools that are installed (facts cron and at); Debian cloud
images install neither. crontab and at read these files through their setgid group, so a
group other than root must be crontab or daemon for them to work.
Rationale
An allow list is safer than a deny list: a new account cannot schedule jobs until someone decides it should. Scheduled jobs are a common persistence technique, and restricting who can create them reduces that option for a compromised account.
Impact
Users who are not listed in the allow file can no longer create or change crontabs or at jobs.
Add the accounts that need them (service accounts included) before applying. The fix creates
missing allow files with root as the only entry.
Audit
- Run
stat -Lc '%n %U:%G %a' /etc/cron.allow /etc/at.allow. Each allow file must exist, belong toroot, with grouprootorcrontab(cron) /daemon(at), and mode640or stricter. /etc/cron.denyand/etc/at.deny, if they exist, must meet the same rules.
Automated: every check must pass. Scanners report each check by its id.
cron-allow
/etc/cron.allow exists with safe ownership and mode.
script: |
f=/etc/cron.allow
[ -f "$f" ] || { echo "$f missing"; exit 1; }
set -- $(stat -Lc '%U %G %a' "$f")
echo "$f $1:$2 $3"
[ "$1" = root ] && { [ "$2" = root ] || [ "$2" = crontab ]; } && [ $((0$3 & 0137)) -eq 0 ]
cron-deny
/etc/cron.deny is absent or has safe ownership and mode.
script: |
f=/etc/cron.deny
[ -e "$f" ] || { echo "$f absent"; exit 0; }
set -- $(stat -Lc '%U %G %a' "$f")
echo "$f $1:$2 $3"
[ "$1" = root ] && { [ "$2" = root ] || [ "$2" = crontab ]; } && [ $((0$3 & 0137)) -eq 0 ]
at-allow
/etc/at.allow exists with safe ownership and mode.
script: |
f=/etc/at.allow
[ -f "$f" ] || { echo "$f missing"; exit 1; }
set -- $(stat -Lc '%U %G %a' "$f")
echo "$f $1:$2 $3"
[ "$1" = root ] && { [ "$2" = root ] || [ "$2" = daemon ]; } && [ $((0$3 & 0137)) -eq 0 ]
at-deny
/etc/at.deny is absent or has safe ownership and mode.
script: |
f=/etc/at.deny
[ -e "$f" ] || { echo "$f absent"; exit 0; }
set -- $(stat -Lc '%U %G %a' "$f")
echo "$f $1:$2 $3"
[ "$1" = root ] && { [ "$2" = root ] || [ "$2" = daemon ]; } && [ $((0$3 & 0137)) -eq 0 ]
Remediation
- cron:
printf 'root\n' > /etc/cron.allow, add the other allowed users one per line, thenchown root:crontab /etc/cron.allowandchmod 640 /etc/cron.allow. Apply the same owner and mode to/etc/cron.denyif it exists. - at: same steps for
/etc/at.allowand/etc/at.denywith groupdaemon.
| Restart | none |
| Disruption | low |
Rollback
Delete /etc/cron.allow and /etc/at.allow (the deny files then apply again).
cron
f=/etc/cron.allow
if [ ! -e "$f" ]; then
cb_backup "$f"
printf 'root\n' >"$f"
chown root:crontab "$f"
chmod 0640 "$f"
fi
for f in /etc/cron.allow /etc/cron.deny; do
[ -e "$f" ] || continue
set -- $(stat -Lc '%U %G %a' "$f")
[ "$1" = root ] || chown root "$f"
[ "$2" = root ] || [ "$2" = crontab ] || chgrp crontab "$f"
[ $((0$3 & 0137)) -eq 0 ] || chmod u-x,g-wx,o-rwx "$f"
done
at
f=/etc/at.allow
if [ ! -e "$f" ]; then
cb_backup "$f"
printf 'root\n' >"$f"
chown root:daemon "$f"
chmod 0640 "$f"
fi
for f in /etc/at.allow /etc/at.deny; do
[ -e "$f" ] || continue
set -- $(stat -Lc '%U %G %a' "$f")
[ "$1" = root ] || chown root "$f"
[ "$2" = root ] || [ "$2" = daemon ] || chgrp daemon "$f"
[ $((0$3 & 0137)) -eq 0 ] || chmod u-x,g-wx,o-rwx "$f"
done
Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.
References
- crontab(1), Debian manual pages Debian, vendor-doc, accessed 2026-10-18
- at(1), Debian manual pages Debian, vendor-doc, accessed 2026-10-18
Mappings
| Framework | ID | Relation |
|---|---|---|
| NIST SP 800-53 Rev. 55.1.1 | AC-6 | related |
| NIST SP 800-53 Rev. 55.1.1 | CM-7 | related |
How this control relates to the other framework's: equivalent, stricter, looser, partial or related.
Threats mitigated
People and lifecycle
| Authors |
|
| Verified | 2026-10-18 by Sn0wAlice on 12, Proxmox VM, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64. Not applicable on fresh image; with cron and at installed the allow files are missing, fix creates them root:crontab and root:daemon 0640, audit passes after reboot; a non-listed user is refused by crontab. |
| Verified | 2026-10-18 by Sn0wAlice on 13, Proxmox VM, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64. Not applicable on fresh image; with cron and at installed the allow files are missing, fix creates them root:crontab and root:daemon 0640, audit passes after reboot; a non-listed user is refused by crontab. |
| Introduced in | v0.1.0 |
| Created, updated | 2026-10-18, 2026-10-18 |
| Translations | fr |