# yaml-language-server: $schema=../../../../schema/control.schema.json id: CB-UBU-SVC-019 title: Allow only listed users to schedule jobs with crontab and at finding: title: Any local user can schedule jobs with crontab or at description: Scheduled jobs let an intruder persist and run code unattended, even after logging out. status: active profiles: [core] risk: severity: medium assessment: automated tags: [cron, at, access-control, persistence] description: | `crontab` and `at` read an allow list and a deny list: - `/etc/cron.allow` and `/etc/cron.deny` for `crontab`; - `/etc/at.allow` and `/etc/at.deny` for `at`. 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. 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. notes: | The checks run only for the tools that are installed (facts `cron` and `at`). Ubuntu installs cron everywhere, and `at` only on 20.04 (pulled in by the `ubuntu-server` metapackage). `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. audit: manual: | 1. Run `stat -Lc '%n %U:%G %a' /etc/cron.allow /etc/at.allow`. Each allow file must exist, belong to `root`, with group `root` or `crontab` (cron) / `daemon` (at), and mode `640` or stricter. 2. `/etc/cron.deny` and `/etc/at.deny`, if they exist, must meet the same rules. logic: all checks: - id: cron-allow engine: shell description: /etc/cron.allow exists with safe ownership and mode. facts: [cron] spec: 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 ] - id: cron-deny engine: shell description: /etc/cron.deny is absent or has safe ownership and mode. facts: [cron] spec: 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 ] - id: at-allow engine: shell description: /etc/at.allow exists with safe ownership and mode. facts: [at] spec: 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 ] - id: at-deny engine: shell description: /etc/at.deny is absent or has safe ownership and mode. facts: [at] spec: 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: manual: | 1. cron: `printf 'root\n' > /etc/cron.allow`, add the other allowed users one per line, then `chown root:crontab /etc/cron.allow` and `chmod 640 /etc/cron.allow`. Apply the same owner and mode to `/etc/cron.deny` if it exists. 2. at: same steps for `/etc/at.allow` and `/etc/at.deny` with group `daemon`. effects: restart: none disruption: low lockout_risk: false rollback: | Delete `/etc/cron.allow` and `/etc/at.allow` (the deny files then apply again). scripts: - id: cron engine: shell facts: [cron] content: | 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 - id: at engine: shell facts: [at] content: | 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 references: - title: crontab(1), Ubuntu manual pages url: https://manpages.ubuntu.com/manpages/noble/man1/crontab.1.html publisher: Canonical kind: vendor-doc accessed: 2026-10-19 - title: at(1), Ubuntu manual pages url: https://manpages.ubuntu.com/manpages/noble/man1/at.1.html publisher: Canonical kind: vendor-doc accessed: 2026-10-19 mappings: - framework: nist-800-53r5 framework_version: "5.1.1" id: AC-6 relation: related - framework: nist-800-53r5 framework_version: "5.1.1" id: CM-7 relation: related - framework: cis-ubuntu-2004 framework_version: "3.0.0" id: "2.4.1.9" relation: equivalent - framework: cis-ubuntu-2004 framework_version: "3.0.0" id: "2.4.2.1" relation: equivalent - framework: cis-ubuntu-2204 framework_version: "3.0.0" id: "2.4.1.9" relation: equivalent - framework: cis-ubuntu-2204 framework_version: "3.0.0" id: "2.4.2.1" relation: equivalent - framework: cis-ubuntu-2404 framework_version: "2.0.0" id: "2.4.1.9" relation: equivalent - framework: cis-ubuntu-2404 framework_version: "2.0.0" id: "2.4.2.1" relation: equivalent - framework: cis-ubuntu-2604 framework_version: "1.0.0" id: "2.4.1.9" relation: equivalent - framework: cis-ubuntu-2604 framework_version: "1.0.0" id: "2.4.2.1" relation: equivalent threats: - framework: mitre-attack id: T1053.003 - framework: mitre-attack id: T1053.002 authors: - name: Alice github: Sn0wAlice verified: - date: 2026-10-19 by: Sn0wAlice versions: ["20.04"] environment: Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic notes: "Stock image fails (no cron.allow, no at.allow; at installed by ubuntu-server); fix creates both with root only, audit passes after reboot." - date: 2026-10-19 by: Sn0wAlice versions: ["22.04"] environment: Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic notes: "Stock image fails (no cron.allow); fix creates it; with at installed at.allow is created too; audit passes after reboot." - date: 2026-10-19 by: Sn0wAlice versions: ["24.04"] environment: Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic notes: "Stock image fails (no cron.allow); fix creates it; with at installed at.allow is created too; audit passes after reboot." - date: 2026-10-19 by: Sn0wAlice versions: ["26.04"] environment: Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic notes: "Stock image fails (no cron.allow); fix creates it; with at installed at.allow is created too; audit passes after reboot." introduced_in: 0.1.0 created: 2026-10-19 updated: 2026-10-19