PAM is one of those systems that’s genuinely invisible until it breaks — and then it becomes the only thing you’re thinking about, usually at the worst possible time, because a bad PAM config can lock every single user out of a system simultaneously, including root. Understanding how PAM is structured, how its config files are actually evaluated, and how to test changes safely before committing to them is one of the more important pieces of Linux system administration knowledge, precisely because the failure mode is so severe.
What PAM Actually Is
Pluggable Authentication Modules is a framework that decouples applications from the specific mechanism used to authenticate users. Rather than every application (login, sshd, sudo, su, passwd) implementing its own password-checking, account-locking, and session-setup logic, they all call into PAM, which then dynamically loads whichever authentication modules are configured for that specific service — a local password check, LDAP, Kerberos, a TOTP module, a fingerprint reader, whatever’s configured.
This is what makes it possible to, say, add two-factor authentication to SSH without modifying sshd’s source code at all — you’re just changing which modules PAM loads for the sshd service.
PAM Configuration Layout
Configuration lives in /etc/pam.d/, with one file per PAM-aware service:
ls /etc/pam.d/
Typical output includes files like sshd, sudo, su, login, passwd, common-auth (Debian-style shared config), system-auth (RHEL-style shared config).
Older systems may use a single /etc/pam.conf instead, though per-service files under /etc/pam.d/ are the modern standard on essentially every current distro.
The Four Management Groups
Every PAM rule belongs to one of four groups, each handling a distinct phase of the authentication process:
auth— verifies the user is who they claim to be (password check, key check, TOTP, etc.)account— verifies the account itself is valid for this kind of access (not expired, not locked, allowed access at this time of day, etc.) — separate from whether the credentials were correctpassword— handles updating credentials (what happens when a user runspasswd)session— sets up/tears down anything needed for the session itself (mounting a home directory, setting resource limits, logging session start/end)
Control Flags
Each line in a PAM config specifies how the result of that module affects the overall outcome:
required— must succeed for overall authentication to succeed, but on failure, PAM still evaluates every remaining rule in the stack before reporting failure (this is deliberately non-obvious behavior, designed so an attacker can’t tell which specific rule failed).requisite— must succeed, and on failure, immediately stops processing and returns failure right away (no further rules evaluated).sufficient— if this succeeds AND no priorrequiredrule has failed, authentication succeeds immediately without evaluating further rules. If it fails, processing continues to the next rule.optional— generally doesn’t affect the overall outcome unless it’s the only rule in the stack.include— pulls in the rules from another PAM config file (heavily used for shared configs likecommon-auth/system-auth).
Example: Reading an Actual PAM Config
/etc/pam.d/sshd (Debian-style, using included common configs):
auth required pam_env.so
auth include common-auth
account required pam_nologin.so
account include common-account
password include common-password
session required pam_selinux.so close
session required pam_loginuid.so
session include common-session
session optional pam_motd.so
/etc/pam.d/common-auth (the shared piece that include pulls in):
auth [success=1 default=ignore] pam_unix.so nullok_secure
auth requisite pam_deny.so
auth required pam_permit.so
That [success=1 default=ignore] syntax is the more flexible “modern” control syntax (as opposed to the simple keyword flags), allowing precise jump-based control flow — here, “if this module succeeds, skip 1 rule forward (past the deny); otherwise, on any other result, ignore this rule’s outcome and let the stack continue.”
Common PAM Modules
| Module | Purpose |
|---|---|
pam_unix.so | Traditional Unix password authentication against /etc/passwd//etc/shadow |
pam_env.so | Sets environment variables for the session |
pam_limits.so | Enforces resource limits (ulimit-style) defined in /etc/security/limits.conf |
pam_nologin.so | Blocks non-root logins if /etc/nologin exists |
pam_faillock.so | Locks accounts after repeated failed login attempts |
pam_google_authenticator.so | TOTP-based two-factor authentication |
pam_ldap.so | Authenticate against an LDAP directory |
pam_mkhomedir.so | Automatically create a home directory on first login if missing |
pam_cracklib.so / pam_pwquality.so | Password strength/complexity enforcement |
pam_deny.so | Always denies — used as a catch-all/fallback |
pam_permit.so | Always succeeds — used deliberately as an explicit “allow” fallback |
Practical Configuration: Account Lockout After Failed Attempts
Using pam_faillock (the modern replacement for the older pam_tally2), add to the auth section of the relevant config (commonly done via authselect on RHEL/Fedora rather than hand-editing directly — see below):
auth required pam_faillock.so preauth silent deny=5 unlock_time=900
auth [success=1 default=bad] pam_unix.so
auth [default=die] pam_faillock.so authfail deny=5 unlock_time=900
auth sufficient pam_faillock.so authsucc
This locks an account for 900 seconds (15 minutes) after 5 consecutive failed attempts.
Check lockout status for a user:
sudo faillock --user someuser
Manually unlock:
sudo faillock --user someuser --reset
Practical Configuration: Password Complexity Requirements
Using pam_pwquality (modern) in the password section:
password requisite pam_pwquality.so retry=3 minlen=12 dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1
minlen=12— minimum password lengthdcredit=-1— require at least 1 digitucredit=-1— require at least 1 uppercase letterocredit=-1— require at least 1 special characterlcredit=-1— require at least 1 lowercase letterretry=3— allow 3 attempts before failing
Configuration also commonly lives in /etc/security/pwquality.conf directly rather than solely as module arguments.
Practical Configuration: Restricting Access by Group
Using pam_access and /etc/security/access.conf:
# /etc/security/access.conf
+ : admins : ALL
+ : deploy : 192.168.1.0/24
- : ALL : ALL
And ensure the relevant PAM service (e.g., sshd) includes:
account required pam_access.so
This allows the admins group from anywhere, allows the deploy group only from the internal subnet, and denies everyone else — evaluated top-to-bottom, first match wins.
RHEL/Fedora: authselect
Modern RHEL and Fedora releases discourage hand-editing PAM files directly in favor of authselect, a tool that manages consistent PAM/nsswitch profiles and reduces the risk of manual editing mistakes:
authselect current
authselect list
sudo authselect select sssd with-faillock --force
Custom modifications are layered on top via authselect’s own mechanism rather than editing /etc/pam.d/ files directly, specifically to avoid the class of self-inflicted lockouts that manual PAM editing is notorious for.
Testing PAM Changes Safely
This is the single most important operational practice in this entire article: never edit a PAM config for a service you’re currently using to access the system without a safety net.
Before making any change:
- Keep a root shell open in a separate session/terminal that you don’t close until you’ve verified the change works.
- Test the syntax where the tooling supports it:
sudo pam-auth-update --dry-run # Debian-family, if applicable to your change
- Test in a new session, not your existing one — open a fresh SSH connection (or fresh
su/sudoattempt) to confirm the change actually works as intended, while your original safety-net session stays open. - Have console/out-of-band access available if at all possible when testing anything on a remote server, since a PAM misconfiguration can lock out even root over SSH.
Reading the Modern Control Syntax in Full
The bracketed [value1=action1 value2=action2 ...] control syntax shown briefly earlier is worth understanding completely, since it’s what nearly all current distro-generated PAM configs actually use instead of the simpler keyword flags, and misreading it is a common source of “I don’t understand why this rule behaves the way it does.”
Each module returns a specific result code (success, deny, expired, new_authtok_reqd, and several others), and the bracketed syntax maps each possible result to an explicit action:
auth [success=1 default=ignore] pam_unix.so nullok_secure
This reads as: “if pam_unix.so returns success, skip forward 1 rule in the stack; for any other result not explicitly listed (default), ignore this module’s outcome entirely and continue to the very next rule as if this one hadn’t run at all.”
Available actions in this syntax include:
ignore— this module’s result doesn’t affect the overall outcome.ok— this module’s result becomes the new overall running result, but only if the running result so far was itself a “neutral” state.done— immediately return this result as the final decision for the entire stack.die— immediately fail the entire stack with this result, skipping all remaining modules.- A number (like
1or2) — skip that many rules forward in the stack.
auth required pam_faillock.so preauth silent deny=5 unlock_time=900
auth [success=1 default=bad] pam_unix.so
auth [default=die] pam_faillock.so authfail deny=5 unlock_time=900
auth sufficient pam_faillock.so authsucc
Walking through this exact stack (from the account-lockout example earlier): the preauth call checks if the account is already locked before even trying the password; then pam_unix.so attempts the actual password check — on success it jumps forward past the failure-handling line entirely, on any other result it’s explicitly treated as bad; then, if we reach it, authfail records the failure and immediately dies (fails the whole stack) if the fail count threshold is hit; and finally, authsucc (only reached on the success path) records the success and clears the failure counter.
This precision is exactly why the bracketed syntax exists — the simple required/sufficient keywords can’t express “on success, skip past this specific cleanup step” or “immediately abort the entire stack right here” as directly.
PAM and NSS: Two Separate but Related Systems
A distinction worth being crystal clear on, since the two systems are frequently confused: PAM handles authentication and session setup (is this password correct, should this session be allowed, what should happen when the session starts/ends), while NSS (Name Service Switch, configured in /etc/nsswitch.conf) handles identity lookup (does this username exist at all, what’s their UID, what groups do they belong to) — and the source of that lookup (local files, LDAP, SSSD, etc.) is independent of how PAM subsequently authenticates them.
cat /etc/nsswitch.conf | grep -E "passwd|group|shadow"
A common real-world configuration point where both systems must be aligned: integrating with a central directory service (LDAP, Active Directory via SSSD) requires both an NSS configuration change (so the system can even find and resolve the user’s account information) and a corresponding PAM configuration change (so authentication actually validates against that directory rather than only local files) — changing one without the other typically produces a partially-working, confusing state where the account seems to exist but authentication behaves unexpectedly, or vice versa.
sudo authselect select sssd --force
sudo systemctl enable --now sssd
id someuser@domain.example.com # tests NSS resolution specifically
Session Management: What Actually Happens Between Login and Logout
The session management group is often the least understood of the four, largely because its effects are less immediately visible than a failed password check. Session modules run both at the start and end of an authenticated session, handling things like:
session required pam_limits.so # apply resource limits from /etc/security/limits.conf
session required pam_lastlog.so # record login time/source for `lastlog` reporting
session optional pam_motd.so # display the message of the day
session required pam_mkhomedir.so skel=/etc/skel umask=0077 # create home dir on first login, if missing
pam_limits.so in particular is worth knowing about beyond just PAM configuration — it’s the actual enforcement mechanism behind /etc/security/limits.conf, which is where you’d set things like maximum open file descriptors or maximum processes per user:
# /etc/security/limits.conf
@developers soft nofile 4096
@developers hard nofile 8192
someuser soft nproc 100
Without the corresponding session required pam_limits.so line actually present in the relevant service’s PAM config, these limits.conf entries are silently ignored entirely — a genuinely common source of “I set the limit but it’s not being applied” confusion, since the limits.conf file itself gives no indication that it depends on a specific PAM module actually being wired in to take effect.
Debugging PAM With Direct Module Testing
Beyond watching live logs during an actual login attempt, pamtester (where available) lets you test a specific PAM service/module stack directly, without needing a full login session:
sudo apt install libpam-pamtester 2>/dev/null || sudo dnf install pamtester 2>/dev/null
pamtester sshd someuser authenticate
This runs just the auth stack for the sshd service against someuser, prompting for whatever credential the configured modules require, and reports success/failure directly — genuinely useful for isolating whether a problem lives in the PAM stack itself versus somewhere else in the broader authentication path (the service’s own connection handling, network access controls, or NSS resolution).
Comparing PAM’s Role to Similar Frameworks
| Framework | Platform | Conceptual similarity |
|---|---|---|
| PAM | Linux/Unix | Pluggable auth modules, stack-based evaluation |
| Windows Credential Providers | Windows | Similarly pluggable, different configuration model entirely |
| macOS Authorization Services | macOS | Conceptually similar plugin-based policy evaluation |
| NSS (separate from PAM) | Linux/Unix | Handles identity lookup rather than authentication itself, often configured alongside PAM |
Recognizing PAM’s basic pattern — a pluggable stack of modules, each contributing to an overall pass/fail decision through configurable control logic — makes it easier to reason about analogous systems on other platforms, even though the specific configuration syntax and module ecosystems differ substantially.
Troubleshooting
Locked out of the entire system after a PAM change — this is why the safety-net session matters; from that still-open root shell, revert the change:
cp /etc/pam.d/sshd.bak /etc/pam.d/sshd
If you don’t have a safety-net session and are truly locked out, you’ll need console/recovery access (single-user mode, a rescue ISO, or cloud provider console access) to fix the PAM config from outside the normal login path.
Debugging why authentication is failing:
sudo journalctl -u sshd -f
PAM logs its decision process through syslog/journald, generally showing exactly which module in the stack succeeded, failed, or denied the request.
“Module is unknown” errors — the module package isn’t installed:
dpkg -l | grep libpam # Debian
rpm -qa | grep pam # RHEL
Faillock not resetting/behaving unexpectedly — check the actual current state directly:
sudo faillock --user someuser
Security Implications
PAM sits at the center of nearly every authentication decision on the system, which makes it both extremely powerful and genuinely dangerous to misconfigure. A sufficient rule placed carelessly can accidentally allow authentication to succeed via a weaker mechanism than intended. An overly permissive pam_access.so rule can open access far wider than planned. And because PAM failures can be catastrophic (locking out every user, including root), changes here deserve the same discipline as firewall changes on a remote box — test from a second session, keep a rollback path open, and never assume a config “looks right” is the same as “behaves right.”
Summary
PAM decouples Linux authentication logic from the applications that need it, organizing rules into four management groups (auth, account, password, session) evaluated as a stack with control flags (required, requisite, sufficient, optional) that determine how each module’s result affects the overall outcome. Real-world configuration commonly layers modules like pam_faillock for lockout policies, pam_pwquality for password strength, and pam_access for network/group-based restrictions — all of which should be tested from a separate safety-net session before you trust them on a system you can’t afford to be locked out of.