{
  "id": "CB-UBU-SVC-019",
  "standard": "ubuntu",
  "version": "0.1.1",
  "url": "https://baselines.cirsec.org/b/ubuntu/v0.1.1/CB-UBU-SVC-019",
  "json": "https://baselines.cirsec.org/b/ubuntu/v0.1.1/CB-UBU-SVC-019.json",
  "yaml": "https://baselines.cirsec.org/b/ubuntu/v0.1.1/CB-UBU-SVC-019.yml",
  "profiles_effective": [
    "core",
    "server",
    "server-hardened",
    "desktop",
    "desktop-hardened"
  ],
  "license": "Text CC BY 4.0, code Apache-2.0",
  "attribution": "CIRSEC Baselines, https://baselines.cirsec.org",
  "control": {
    "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:\n\n- `/etc/cron.allow` and `/etc/cron.deny` for `crontab`;\n- `/etc/at.allow` and `/etc/at.deny` for `at`.\n\nWhen the allow file exists, only the users it lists may schedule jobs, and the deny file is\nignored. The allow file must exist, belong to root, with group `root` or the group the tool\nruns as (`crontab` for cron, `daemon` for at), and mode `0640` or stricter. A deny file, if\npresent, must have the same ownership and mode.\n",
    "rationale": "An allow list is safer than a deny list: a new account cannot schedule jobs until someone\ndecides it should. Scheduled jobs are a common persistence technique, and restricting who can\ncreate them reduces that option for a compromised account.\n",
    "impact": "Users who are not listed in the allow file can no longer create or change crontabs or at jobs.\nAdd the accounts that need them (service accounts included) before applying. The fix creates\nmissing allow files with `root` as the only entry.\n",
    "notes": "The checks run only for the tools that are installed (facts `cron` and `at`). Ubuntu installs\ncron 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\ngroup other than `root` must be `crontab` or `daemon` for them to work.\n",
    "audit": {
      "manual": "1. Run `stat -Lc '%n %U:%G %a' /etc/cron.allow /etc/at.allow`. Each allow file must exist,\n   belong to `root`, with group `root` or `crontab` (cron) / `daemon` (at), and mode `640` or\n   stricter.\n2. `/etc/cron.deny` and `/etc/at.deny`, if they exist, must meet the same rules.\n",
      "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\n[ -f \"$f\" ] || { echo \"$f missing\"; exit 1; }\nset -- $(stat -Lc '%U %G %a' \"$f\")\necho \"$f $1:$2 $3\"\n[ \"$1\" = root ] && { [ \"$2\" = root ] || [ \"$2\" = crontab ]; } && [ $((0$3 & 0137)) -eq 0 ]\n"
          }
        },
        {
          "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\n[ -e \"$f\" ] || { echo \"$f absent\"; exit 0; }\nset -- $(stat -Lc '%U %G %a' \"$f\")\necho \"$f $1:$2 $3\"\n[ \"$1\" = root ] && { [ \"$2\" = root ] || [ \"$2\" = crontab ]; } && [ $((0$3 & 0137)) -eq 0 ]\n"
          }
        },
        {
          "id": "at-allow",
          "engine": "shell",
          "description": "/etc/at.allow exists with safe ownership and mode.",
          "facts": [
            "at"
          ],
          "spec": {
            "script": "f=/etc/at.allow\n[ -f \"$f\" ] || { echo \"$f missing\"; exit 1; }\nset -- $(stat -Lc '%U %G %a' \"$f\")\necho \"$f $1:$2 $3\"\n[ \"$1\" = root ] && { [ \"$2\" = root ] || [ \"$2\" = daemon ]; } && [ $((0$3 & 0137)) -eq 0 ]\n"
          }
        },
        {
          "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\n[ -e \"$f\" ] || { echo \"$f absent\"; exit 0; }\nset -- $(stat -Lc '%U %G %a' \"$f\")\necho \"$f $1:$2 $3\"\n[ \"$1\" = root ] && { [ \"$2\" = root ] || [ \"$2\" = daemon ]; } && [ $((0$3 & 0137)) -eq 0 ]\n"
          }
        }
      ]
    },
    "remediation": {
      "manual": "1. cron: `printf 'root\\n' > /etc/cron.allow`, add the other allowed users one per line, then\n   `chown root:crontab /etc/cron.allow` and `chmod 640 /etc/cron.allow`. Apply the same owner\n   and mode to `/etc/cron.deny` if it exists.\n2. at: same steps for `/etc/at.allow` and `/etc/at.deny` with group `daemon`.\n",
      "effects": {
        "restart": "none",
        "disruption": "low",
        "lockout_risk": false
      },
      "rollback": "Delete `/etc/cron.allow` and `/etc/at.allow` (the deny files then apply again).\n",
      "scripts": [
        {
          "id": "cron",
          "engine": "shell",
          "facts": [
            "cron"
          ],
          "content": "f=/etc/cron.allow\nif [ ! -e \"$f\" ]; then\n  cb_backup \"$f\"\n  printf 'root\\n' >\"$f\"\n  chown root:crontab \"$f\"\n  chmod 0640 \"$f\"\nfi\nfor f in /etc/cron.allow /etc/cron.deny; do\n  [ -e \"$f\" ] || continue\n  set -- $(stat -Lc '%U %G %a' \"$f\")\n  [ \"$1\" = root ] || chown root \"$f\"\n  [ \"$2\" = root ] || [ \"$2\" = crontab ] || chgrp crontab \"$f\"\n  [ $((0$3 & 0137)) -eq 0 ] || chmod u-x,g-wx,o-rwx \"$f\"\ndone\n"
        },
        {
          "id": "at",
          "engine": "shell",
          "facts": [
            "at"
          ],
          "content": "f=/etc/at.allow\nif [ ! -e \"$f\" ]; then\n  cb_backup \"$f\"\n  printf 'root\\n' >\"$f\"\n  chown root:daemon \"$f\"\n  chmod 0640 \"$f\"\nfi\nfor f in /etc/at.allow /etc/at.deny; do\n  [ -e \"$f\" ] || continue\n  set -- $(stat -Lc '%U %G %a' \"$f\")\n  [ \"$1\" = root ] || chown root \"$f\"\n  [ \"$2\" = root ] || [ \"$2\" = daemon ] || chgrp daemon \"$f\"\n  [ $((0$3 & 0137)) -eq 0 ] || chmod u-x,g-wx,o-rwx \"$f\"\ndone\n"
        }
      ]
    },
    "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"
      }
    ],
    "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"
  }
}