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.
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.
Use this decision table when reviewing the result:
| Files present | Who may use at and batch |
|---|---|
/etc/at.allow | Only usernames listed in at.allow |
No at.allow, but at.deny | Every username not listed in at.deny |
No at.allow and an empty at.deny | Every user |
| Neither file | Only 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.
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.
at.allow when the requirement is "only these named accounts may submit jobs". Put one approved username on each line, for example scheduler and operator.at.deny when the requirement is "most local users may submit jobs, except these named accounts", for example contractor and untrusted-service.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.
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.
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.
at.allow and at.deny exist.at.allow controls the decision.at.deny permits every user.atq.