Control PAM Access with a Carefully Scoped List File
pam_listfile allows or denies a PAM request by checking one item per line in a protected file. That makes it the module of choice for a quick, explicit block or allow list. This guide uses it from libpam-modules version 1.5.3-5ubuntu5.7 on the local system.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about twenty minutes, plus a maintenance window if the target is a live login or remote access service. You need root access to create the list and edit a PAM service file, and a second working session or console for recovery. The module works for auth, account, password and session stacks, but the examples below stick to auth.
Safety warning
PAM changes can deny access immediately. Keep an existing root shell open, test from a separate session, and never give up your last recovery path before the new rule has been checked.
1. Confirm the installed module and contract
Check the package and shared object first. These are read-only and need no elevated privileges:
$ dpkg-query -W -f='${Package} ${Version}\n' libpam-modules:amd64
libpam-modules 1.5.3-5ubuntu5.7
$ ls -l /lib/x86_64-linux-gnu/security/pam_listfile.so
-rw-r--r-- 1 root root ... /lib/x86_64-linux-gnu/security/pam_listfile.so
The module never runs standalone: PAM loads it from a line in a service file such as /etc/pam.d/login. Its required arguments are item, sense, file and onerr, and the manual is explicit that you should not rely on defaults for any of them.
The supported items are user, tty, rhost, ruser, group and shell. The module reads the matching PAM value and looks for it in the named file, one item per line.
2. Create a protected list
Decide the purpose before you decide the sense. On a deny list, a match in the file fails the request. On an allow list, a match succeeds and a miss does the opposite.
This deny-list example blocks one named account from the login service. Replace ACCOUNT_TO_BLOCK with a real account only when that is genuinely what you mean to do:
# install -o root -g root -m 0644 /dev/null /etc/login-deny-users
# printf '%s\n' 'ACCOUNT_TO_BLOCK' > /etc/login-deny-users
# chown root:root /etc/login-deny-users
# chmod 0644 /etc/login-deny-users
# cat /etc/login-deny-users
ACCOUNT_TO_BLOCK
The file must be a plain file and must not be world-writable. A root-owned mode like 0644 lets PAM read it while keeping ordinary users from editing the policy themselves. Use the exact names the service actually supplies to PAM, and skip explanatory comments unless you have confirmed how the installed module treats them: the documented format is strictly one item per line.
Checkpoint
Confirm the file's type, owner and mode before touching PAM:
# stat -c '%F %U:%G %a %n' /etc/login-deny-users
regular file root:root 644 /etc/login-deny-users
3. Add the smallest possible PAM rule
Back up the service file, then add one rule to its existing auth stack:
# cp --preserve=all /etc/pam.d/login /etc/pam.d/login.before-pam-listfile
# editor /etc/pam.d/login
Add this line in the appropriate auth section:
auth required pam_listfile.so onerr=fail item=user sense=deny file=/etc/login-deny-users
sense=deny means a listed username gets refused, while an unlisted one is not refused by this module. onerr=fail makes a missing or broken list fail closed, which is usually the safer choice for access control, though it also means a missing file can stop authentication for everyone who reaches this rule.
Skip quiet here: it suppresses logging for refusals and missing list files, which makes a real operational failure much harder to diagnose later. Keep this line separate from other module arguments so it stays easy to review.
4. Check the rule before testing access
Inspect the resulting service file and confirm the module path is still there. None of this authenticates anyone or changes state:
# grep -n -F 'pam_listfile.so' /etc/pam.d/login
42:auth required pam_listfile.so onerr=fail item=user sense=deny file=/etc/login-deny-users
# test -f /etc/login-deny-users && test ! -L /etc/login-deny-users && echo 'list file is present and not a symlink'
list file is present and not a symlink
Check the whole stack, not just this one line. The control flags and the order of neighbouring modules decide how a failure actually affects the service: required records a failure but lets the stack keep going, though the overall request still fails unless a later rule changes that. One line never tells the whole story.
5. Test from a second session
Open a second terminal, SSH session or local console before you test, and leave the original root shell alone. Try an account that should be denied and one that should stay allowed, and never paste passwords into a guide or a shell history.
Use the normal client for the service rather than calling the module directly. A denied account should get the service's ordinary authentication failure, an allowed one should keep passing through the rest of the stack. Exact wording depends on the service and its other modules, so treat the exit result and the service log as your real evidence:
$ ssh ACCOUNT_TO_BLOCK@HOSTNAME
Permission denied (publickey,password).
$ printf 'ssh exit status: %s\n' "$?"
ssh exit status: 255
That sample is illustrative: SSH may use different methods and wording depending on setup. If your test never actually reaches the login service, it has not tested this rule at all, so check the PAM file the service uses and review its logs with your usual tools.
Test an allowed administrator immediately after any edit. If both accounts fail, stop and restore the backup:
# cp --preserve=all /etc/pam.d/login.before-pam-listfile /etc/pam.d/login
# grep -n -F 'pam_listfile.so' /etc/pam.d/login || echo 'pam_listfile rule removed'
pam_listfile rule removed
Restoring the service file is the undo for this whole example. Keep the list file until you have confirmed nothing else refers to it, then remove it once it is genuinely no longer needed:
# rm -- /etc/login-deny-users
Destructive action
That last command deletes the policy file for good. Check references first with grep -R -F '/etc/login-deny-users' /etc/pam.conf /etc/pam.d 2>/dev/null and only remove it once that comes back empty.
6. Use an allow list only with a recovery plan
An allow list suits a service that only named accounts should be able to use:
auth required pam_listfile.so onerr=fail item=user sense=allow file=/etc/login-allow-users
Every user who must get through this module needs an entry in /etc/login-allow-users, including the recovery account, unless you have another tested route to root. The manual specifically warns about preserving a way for root to log in, whether by listing root directly or by listing someone who can become root. Build the file, check its owner and mode, and test a permitted account before you close the original session.
apply=USERNAME or apply=@GROUPNAME can restrict a rule to a class of user, but the manual says that restriction only makes sense with tty, rhost and shell, not with user, ruser or group. Use it only when you can name exactly which PAM value is being matched.
Done means
- Confirmed the module. The installed module and package version were checked.
- Locked down the list. It is a root-owned, non-world-writable plain file with one intended item per line.
- Spelled out the rule. The PAM rule sets
item,sense,fileandonerrexplicitly. - Tested both outcomes. A second session proved the expected denied and allowed results.
- Kept a way back. A tested backup remains, and the original recovery shell was never closed early.
- Decided the failure mode. You know whether a missing list should fail or succeed, and can explain why.