Why PAM exists
Without PAM, every application read /etc/passwd and /etc/shadow
directly. To add LDAP, Kerberos, 2FA, or fail-after-N-attempts you would
have to patch EVERY program. PAM solves this by moving the logic into a
shared library and a stack of modules.
user → ssh login
↓
sshd calls pam_authenticate("sshd")↓
PAM reads /etc/pam.d/sshd
↓
Module stack, one by one:
pam_unix.so → check /etc/shadow
pam_faillock.so → failure counter
pam_google_authenticator.so → 2FA
↓
"authenticated" / "failed" → return to sshd
Where things live
/etc/pam.d/<service>holds configs for specific applications (one per service:/etc/pam.d/sshd,/etc/pam.d/sudo,/etc/pam.d/login)./etc/pam.d/otheris the fallback for applications without their own config. The default is paranoid: deny everything. Do not change it to permissive./lib64/security/*.so(RHEL/Fedora) or/lib/x86_64-linux-gnu/security/*.so(Debian/Ubuntu) are the modules themselves./etc/security/*.confholds configs for individual modules (limits.conf, faillock.conf, and so on).
The service name equals the file name in /etc/pam.d/. When sudo calls PAM,
/etc/pam.d/sudo is read.
Config line syntax
type control module [arguments]
Example from /etc/pam.d/sshd:
auth required pam_env.so
auth substack password-auth
account required pam_nologin.so
session required pam_loginuid.so
password include system-auth
The four type values (what you check)
| type | What it does |
|---|---|
auth | Checks who you are (password, token, biometrics) |
account | Whether you may log in right now (user not locked, not expired, in the right group) |
password | Password change (called on passwd/chpasswd) |
session | What to do before/after login: mounting home, limits, audit |
One service file usually contains all four types. PAM runs only the type needed by the calling application.
Control flags (what to do with a module result)
| flag | Behavior on success | Behavior on fail |
|---|---|---|
required | continue the stack | return fail AT THE END of the stack (deferred) |
requisite | continue the stack | exit with fail IMMEDIATELY (do not run further) |
sufficient | exit with success IMMEDIATELY (if no required-fail happened before) | continue the stack |
optional | continue the stack | continue the stack, fail is ignored |
include | insert the whole stack from another file | same |
substack | like include, but isolates the "done" flag for the return | same |
The key difference between required and requisite: both demand success, but
requisite fails instantly. You use this when you need to avoid showing
the next prompt (if the username does not exist, do not ask for a password).
sufficient skips the rest of the stack on success, a common pattern for
"either a key or a password":
auth sufficient pam_unix.so # password succeeds → let in
auth required pam_deny.so # otherwise fallback fail
The most common modules
| Module | What it does |
|---|---|
pam_unix.so | Checks /etc/passwd + /etc/shadow (the standard) |
pam_pwquality.so | Policy for a new password (length, classes, dictionary) |
pam_faillock.so | Lockout after N failed attempts (was pam_tally2) |
pam_nologin.so | If /etc/nologin exists, allows only root |
pam_limits.so | Applies /etc/security/limits.conf (ulimits) |
pam_loginuid.so | Writes the audit uid to /proc/self/loginuid |
pam_mkhomedir.so | Creates ~/ on first login (LDAP users) |
pam_env.so | Loads env from /etc/environment |
pam_succeed_if.so | Conditional skip: "if uid >= 1000" |
pam_selinux.so | Sets the SELinux context for the session (see selinux-apparmor) |
pam_systemd.so | Creates user.slice / XDG_RUNTIME_DIR |
pam_google_authenticator | TOTP 2FA |
The shared stack: common-auth / system-auth
So you do not duplicate the same modules in every pam.d/<service>,
there are shared files:
- Debian/Ubuntu:
/etc/pam.d/common-{auth,account,password,session}, pulled in via@include common-auth. - RHEL/Fedora:
/etc/pam.d/system-auth,password-auth, pulled in viaauth substack system-auth.
To change global behavior (for example, requiring 2FA for all console logins), edit the shared file, not each service.
Typical cases
Lock a user after 5 failed attempts
# /etc/pam.d/system-auth (RHEL) or /etc/pam.d/common-auth (Debian)
auth required pam_faillock.so preauth silent deny=5 unlock_time=900
auth sufficient pam_unix.so
auth [default=die] pam_faillock.so authfail deny=5 unlock_time=900
account required pam_faillock.so
Check: faillock --user serge. Reset: faillock --user serge --reset.
Strengthen passwords: minimum 12 chars, 3 classes
# /etc/security/pwquality.conf
minlen = 12
minclass = 3
Applied through pam_pwquality.so, which is already wired into the password stack.
Forbid root login over SSH
Not through PAM but through PermitRootLogin no in sshd_config. Through PAM, like this:
# /etc/pam.d/sshd, in the auth stack:
auth required pam_succeed_if.so user != root
Debugging: what to do when you break it
The main rule: NEVER edit a PAM config without a second root session open. If you break it, there will be no logins, including through sudo.
# 1. Logs
sudo journalctl -t sshd -t sudo --since "10 min ago"
sudo tail -f /var/log/auth.log # Debian
sudo tail -f /var/log/secure # RHEL
# 2. Test without logging in again
sudo -k && sudo -v # drop the sudo cache and re-authenticate
# 3. Check that the module loads at all
ls /lib64/security/pam_unix.so # or /lib/x86_64-linux-gnu/security/
# 4. Documentation for a specific module
man pam_faillock
man pam_unix
If you break it so badly you cannot log in, boot into single-user mode (kernel cmdline:
systemd.unit=rescue.target) and fix the file.
A safe fallback for /etc/pam.d/<service>:
auth required pam_unix.so
account required pam_unix.so
password required pam_unix.so
session required pam_unix.so
This returns things to "the way they were before PAM": only /etc/shadow is checked.