Control PAM Access with a Carefully Scoped List File
You will add a PAM rule that allows or denies requests according to one item per line in a protected file, then check the configuration without accidentally locking out the only administrator. This guide uses pam_listfile 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 you are changing 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 is available for auth, account, password and session stacks, but the examples below use auth.
Safety warning
PAM changes can deny access immediately. Keep an existing root shell open, test from a separate session, and never remove your last recovery path before the new rule has been checked.
1. Confirm the installed module and contract
First check the package and shared object. These are read-only commands and do not need 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 does not run as a standalone command. PAM loads it from a service configuration line such as /etc/pam.d/login. Its required arguments are item, sense, file and onerr. Do not rely on defaults: the manual explicitly says to specify the arguments.
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, which contains one item per line.
2. Create a protected list
Choose a purpose before choosing the sense. For a deny list, an item found in the file causes the request to fail. For an allow list, an item found in the file causes the request to succeed and an item absent from the file takes the opposite action.
The following 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 intend:
# 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 such as 0644 lets PAM read it while preventing ordinary users from changing the policy. If the list contains usernames, use the exact names that the service supplies to PAM. Do not add explanatory comments unless you have verified how the installed module treats them; the documented format is one item per line.
Checkpoint: confirm the file type, owner and mode before editing 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. The privilege escalation is required because /etc/pam.d/login is system configuration:
# 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 is refused, while an unlisted username takes the opposite path and is not refused by this module. onerr=fail makes a missing or unusable list fail closed. That is usually the safer choice for an access-control list, but it also means a missing file can stop authentication for everyone who reaches this rule.
Do not add quiet merely to hide a problem. It suppresses logging for service refusals or missing list files, which can make an operational failure harder to diagnose. Keep the line separate from other module arguments so it is easy to review.
4. Check the rule before testing access
Inspect the resulting service file and verify that the module path is still present. These checks do not authenticate anyone and do not change 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 line. PAM control flags and the order of neighbouring modules decide how a failure affects the service. required records a failure while allowing the stack to continue, but the overall request still fails if no later rule changes that outcome. Do not assume that one line describes the complete policy.
5. Test from a second session
Open a second terminal, SSH session or local console before testing. Keep the original root shell untouched. Try the service with an account that should be denied and one that should remain allowed. Do not paste passwords into a guide or a shell history.
For a login service, use the normal client for the service rather than calling the module directly. A denied account should receive the service's normal authentication failure. An allowed account should continue through the rest of the PAM stack. Exact prompts and messages depend on the service and its other modules, so use the exit result and the service log as your evidence.
$ ssh ACCOUNT_TO_BLOCK@HOSTNAME
Permission denied (publickey,password).
$ printf 'ssh exit status: %s\n' "$?"
ssh exit status: 255
The sample output is illustrative: SSH may use different authentication methods and wording. If your test does not reach the login service, it does not test this rule. Check the PAM file used by the service and review its logs with your normal system logging tools.
After an edit, test an allowed administrator immediately. If both accounts fail, stop making changes 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 example. Keep the list file until you have confirmed that no other service refers to it, then remove it only if it is no longer needed:
# rm -- /etc/login-deny-users
Destructive action
That final command deletes the policy file. Check references first with grep -R -F '/etc/login-deny-users' /etc/pam.conf /etc/pam.d 2>/dev/null and remove it only when the result is empty.
6. Use an allow list only with a recovery plan
An allow list is useful when only named accounts should use a service:
auth required pam_listfile.so onerr=fail item=user sense=allow file=/etc/login-allow-users
Every user who must pass this module must appear 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, either by listing root or by listing a user who can become root. Build the file first, verify its owner and mode, and test a permitted account before closing the original session.
apply=USERNAME or apply=@GROUPNAME can restrict a rule to a user class, but the manual says this restriction is meaningful only with tty, rhost and shell. It does not make sense with user, ruser or group. Use it only when you can identify the PAM value being matched.
Done means
- The installed module and package version were confirmed.
- The list is a root-owned, non-world-writable plain file with one intended item per line.
- The PAM rule specifies
item,sense,fileandonerrexplicitly. - A second session proved the expected denied and allowed outcomes.
- A tested backup remains available, and the original recovery shell was not closed prematurely.
- You know whether a missing list should fail or succeed, and can explain that choice.