Pluggable Authentication Modules (PAM) in Linux: Complete Authentication Framework Configuration Guide

Pluggable authentication modules (PAMs) in Linux

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 correct
  • password — handles updating credentials (what happens when a user runs passwd)
  • 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 prior required rule 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 like common-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

ModulePurpose
pam_unix.soTraditional Unix password authentication against /etc/passwd//etc/shadow
pam_env.soSets environment variables for the session
pam_limits.soEnforces resource limits (ulimit-style) defined in /etc/security/limits.conf
pam_nologin.soBlocks non-root logins if /etc/nologin exists
pam_faillock.soLocks accounts after repeated failed login attempts
pam_google_authenticator.soTOTP-based two-factor authentication
pam_ldap.soAuthenticate against an LDAP directory
pam_mkhomedir.soAutomatically create a home directory on first login if missing
pam_cracklib.so / pam_pwquality.soPassword strength/complexity enforcement
pam_deny.soAlways denies — used as a catch-all/fallback
pam_permit.soAlways 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 length
  • dcredit=-1 — require at least 1 digit
  • ucredit=-1 — require at least 1 uppercase letter
  • ocredit=-1 — require at least 1 special character
  • lcredit=-1 — require at least 1 lowercase letter
  • retry=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:

  1. Keep a root shell open in a separate session/terminal that you don’t close until you’ve verified the change works.
  2. Test the syntax where the tooling supports it:
sudo pam-auth-update --dry-run   # Debian-family, if applicable to your change
  1. Test in a new session, not your existing one — open a fresh SSH connection (or fresh su/sudo attempt) to confirm the change actually works as intended, while your original safety-net session stays open.
  2. 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 1 or 2) — 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

FrameworkPlatformConceptual similarity
PAMLinux/UnixPluggable auth modules, stack-based evaluation
Windows Credential ProvidersWindowsSimilarly pluggable, different configuration model entirely
macOS Authorization ServicesmacOSConceptually similar plugin-based policy evaluation
NSS (separate from PAM)Linux/UnixHandles 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.

References

Total
0
Shares

Leave a Reply

Previous Post
how to use shadow password in linux

How to Use Shadow Passwords in Linux: Complete Password Security and Management Guide

Next Post
umask command in Linux and it perimeters

umask Command in Linux: Complete Guide to Default File Permissions and Parameters

Related Posts