Control Who Can Submit at Jobs with at.allow and at.deny

at.allow and at.deny decide who may schedule a job at all, and a short deny-list is easy to misread as a lockdown. This guide checks a policy for at and batch, then confirms whether it is actually being used. The examples follow the installed at 3.2.5 package on this machine. Allow about ten minutes for an inspection, or fifteen to twenty minutes if you need to change the policy and verify it.

You need a shell and an account with sudo access if a file must be changed. Reading the policy may also need elevated privileges, since these files commonly carry administrator-controlled access rules. Nothing below submits a job unless a step is explicitly labelled as a test, and this guide does not submit one.

1. Check which policy files exist

The two files are /etc/at.allow and /etc/at.deny. Start with a read-only check that does not expose their contents:

$ for file in /etc/at.allow /etc/at.deny; do
    if [ -e "$file" ]; then
        stat -c '%A %U:%G %n' "$file"
    else
        printf 'missing %s\n' "$file"
    fi
done

Checkpoint: write down which files exist before changing anything. Whether at.allow exists is the first decision point; if it does, at.deny does not broaden access for anyone.

2. Apply the precedence rule

Use this decision table when reviewing the result:

Files presentWho may use at and batch
/etc/at.allowOnly usernames listed in at.allow
No at.allow, but at.denyEvery username not listed in at.deny
No at.allow and an empty at.denyEvery user
Neither fileOnly the superuser

This is a default-allow rule whenever only at.deny exists. A common review mistake is seeing a short deny list and assuming everyone else is blocked; they are allowed instead. A second common mistake is editing at.deny while an old at.allow still exists: the allow file always wins.

The rule covers submission through both at and batch. It does not cancel jobs already submitted, and it does not control ordinary cron entries.

3. Inspect the usernames exactly

Once you know which file controls access, read it with privilege if needed:

$ sudo sed -n '1,120p' /etc/at.allow
$ sudo sed -n '1,120p' /etc/at.deny

A policy file is a list of usernames, one per line. Whitespace is not permitted: no indentation, trailing spaces or explanatory comments unless your installed implementation documents them as valid. Compare names against the account database rather than guessing:

$ getent passwd | cut -d: -f1 | sort
$ id -un
operator

Checkpoint: every entry should be an intentional account name, with no accidental blank-looking characters. If the file names an account from an old server build or one that no longer exists, treat that as a review finding rather than silently rewriting the policy.

4. Choose the policy that matches the requirement

scheduler
operator
contractor
untrusted-service

Do not create both files casually. With both present, only the names in at.allow matter, which can lock out an account somebody expected at.deny to exempt. If neither file exists, the manpage's fallback is superuser-only access. An empty at.deny, in contrast, permits every user, so an empty file is not a locked-down configuration.

5. Edit the controlling file carefully

Changing either file is a security-sensitive action. Before editing, save a recoverable copy and confirm the target path. This command reads the file as root and writes a backup beside it:

$ sudo cp -p -- /etc/at.allow /etc/at.allow.before-change

If at.allow is missing, back up at.deny instead: do not run a copy command for a path the previous inspection reported as missing. Then edit only the file that controls the intended policy:

$ sudoedit /etc/at.allow
# or, when at.allow is absent and deny-list behaviour is intentional:
$ sudoedit /etc/at.deny

Keep the existing owner, group and restrictive mode unless your local package policy says otherwise. Avoid a broad chmod or a replacement file created in a temporary directory with the wrong ownership. To undo the example, restore the matching backup and repeat the inspection:

$ sudo cp -p -- /etc/at.allow.before-change /etc/at.allow
$ stat -c '%A %U:%G %n' /etc/at.allow

Warning: restoring a backup is itself a policy change. Do it only after checking that the backup is the intended version and that no administrator has made a later change.

6. Verify the file and the effective result

Check for whitespace and display the numbered entries:

$ sudo awk '/[[:space:]]/ { print "whitespace on line", NR; bad=1 } END { exit bad }' /etc/at.allow
$ sudo nl -ba /etc/at.allow

Substitute the at.deny path for that policy instead. The first command prints nothing and returns status 0 when it finds no whitespace, but a clean syntax check is not proof the intended account is allowed: the effective answer still depends on whether at.allow exists.

A real access test requires submitting a job, which changes state and can run a command later. If you are authorised to do that, use a harmless command, record the job number, and remove the job immediately:

$ printf '%s\n' 'printf at-policy-test' | at now + 2 minutes
job 12 at Tue Sep 22 14:02:00 2026
$ atrm 12
$ atq

The job number and displayed time are examples and will vary. Only run this test in a maintenance window where a transient queued job is acceptable. If the account is denied, at should reject submission instead. Do not infer success from the file contents alone when an access decision matters.

Done means