PAM (Pluggable Authentication Modules) is the layer between every Linux login and the password, key, or token that proves identity. A misconfigured PAM stack can lock out the entire ops team during a maintenance window or โ worse โ silently allow logins that should fail. Because PAM is configured per-service in /etc/pam.d/, and changes are applied without restart, the failure mode is fast and uncomfortable. This guide explains the stack, the audit commands that surface weak rules, and the modern replacements for legacy modules.
The PAM concept in 60 seconds
For each service (sshd, sudo, login, su, gdm, etc.), PAM evaluates four module groups in order: auth (verify identity), account (is this user allowed?), password (change credentials), session (set up environment). Each group is a sequence of modules with control flags: required, requisite, sufficient, optional, or the modern [default=โฆ] bracket syntax.
ls /etc/pam.d/ # one file per service
cat /etc/pam.d/sshd
grep -r pam_unix /etc/pam.d/ # find usages of a module
man pam_faillock # per-module documentation
Inventory: what is currently in your stack
sudo grep -rE '^[^#]' /etc/pam.d/ | grep -v '^[^:]*:#' | sort
sudo grep -rE 'pam_(unix|sss|ldap|krb5|tally|faillock|pwquality|cracklib)' /etc/pam.d/
The first command is a complete dump of every active PAM rule on the host โ paste it into your audit notebook. The second highlights authentication backends and policy modules.
Account lockout: the must-have policy
Without a lockout module, an attacker can brute-force a local account indefinitely. The modern replacement for the deprecated pam_tally2 is pam_faillock:
# /etc/security/faillock.conf
deny = 5
unlock_time = 900
even_deny_root
admin_group = wheel
Then in /etc/pam.d/system-auth (RHEL family) or by running pam-auth-update (Debian/Ubuntu). Verify with:
sudo faillock --user alice # current count for a user
sudo faillock --user alice --reset # clear after legitimate fail
Password quality enforcement
pam_pwquality rejects weak new passwords at the time they are set:
# /etc/security/pwquality.conf
minlen = 14
minclass = 4
maxrepeat = 3
maxsequence = 3
dictcheck = 1
enforce_for_root
pam_pwhistory remembers previous hashes so users cannot recycle the last N passwords. Both layered on top of the existing password stack via pam-auth-update on Debian/Ubuntu or by editing /etc/pam.d/system-auth on RHEL.
Time- and source-based access
pam_access in the account stack enforces who can log in from where:
# /etc/security/access.conf
+ : root : 10.0.0.0/8 LOCAL
+ : ops admin : ALL
- : ALL : ALL
pam_time restricts logins to specific hours per user. Both are useful on shared bastion hosts where you want a defence-in-depth layer above SSH AllowUsers.
Limits and session hardening
The session stack is where ulimits, cgroup placement, and umask are applied. The standard line:
session required pam_limits.so
โฆ reads /etc/security/limits.conf and /etc/security/limits.d/ for every PAM-authenticated session. Without it, your carefully tuned ulimit drop-ins do nothing for SSH logins. Confirm presence:
grep pam_limits /etc/pam.d/sshd /etc/pam.d/login /etc/pam.d/system-auth
The audit script
#!/bin/bash
echo "== Lockout module present =="
grep -lE 'pam_(faillock|tally2)' /etc/pam.d/* || echo "FAIL: no lockout"
echo "== Password quality present =="
grep -lE 'pam_(pwquality|cracklib)' /etc/pam.d/* || echo "FAIL: no pwquality"
echo "== pam_limits in session stack =="
grep -lE '^session.*pam_limits' /etc/pam.d/* || echo "FAIL: no pam_limits"
echo "== Empty password allowed =="
grep -rE 'nullok' /etc/pam.d/ && echo "WARN: nullok found"
echo "== Faillock counters =="
faillock --user-list 2>/dev/null | head
Multi-factor authentication
Layer TOTP onto SSH key auth via pam_google_authenticator or pam_oath:
# /etc/pam.d/sshd โ at top
auth required pam_google_authenticator.so
Combined with AuthenticationMethods publickey,keyboard-interactive in sshd_config, the server requires both a private key and a fresh code, defeating credential theft.
Recovery: the safety net
A typo in /etc/pam.d/sshd can lock you out instantly. Three precautions:
- Always keep an open root SSH session in a second terminal while editing PAM.
- Test the change against a fresh login โ do not trust the open session.
- Keep
/etc/pam.d/under version control, ideally with the rest of/etc/inetckeeper:sudo apt install etckeeper sudo etckeeper init sudo etckeeper commit "before pam edit"
Common pitfalls
- Using
auth sufficient pam_unix.so nullokโ allows blank passwords. Removenullokon every internet-facing host. - Editing per-service files (
/etc/pam.d/sshd) when the change should be in the sharedsystem-auth; the result is policy that applies to SSH but notsu. - Forgetting
even_deny_rootin faillock โ root is the most attacked account, and root can clear its own counter trivially. - Running
pam-auth-updateon Debian without reading the diff โ it can quietly remove a custom module you added by hand.
PAM rewards investment. A properly audited stack with lockout, password quality, source restrictions, and MFA closes more attack vectors than every other Linux hardening combined. Spend an afternoon, document every line, and put it under change control.