Pluggable Authentication Modules (PAM) decide how almost every Linux login is authenticated, and a single misordered line in a stack can silently turn a mandatory check into an optional one. SSH, console login, su, sudo, cron, screen lockers and many daemons all hand their authentication, account and session decisions to the same libpam stacks under /etc/pam.d. That makes PAM the place where MFA, lockout, password quality and access rules are enforced, and also a favourite target for persistence because a patched module sees every password in clear text. This guide explains how stacks are evaluated, how to add MFA and lockout without locking yourself out, how to keep the files under configuration management, and how to detect tampering. It is written for Linux administrators, platform engineers and detection engineers.
Key takeaways
- Control flags are the security logic.
required,requisite,sufficientand the bracket form[success=1 default=bad]decide whether a module can be skipped; read them before reading module names. - Let the distribution tool own the stack. On RHEL family systems authselect generates
system-authandpassword-auth; on Debian and Ubuntu pam-auth-update builds thecommon-*files. Hand edits get overwritten or drift. - MFA lives in the auth stack and in sshd. A TOTP or smart card module only protects SSH if sshd routes the session through PAM and
AuthenticationMethodsrequires both factors. - Lockout needs a persistent, well placed pam_faillock. The defaults (3 failures in 900 seconds, 600 second unlock) and the tmpfs tally directory are rarely what the policy says.
- Treat PAM files and modules as integrity critical. ATT&CK T1556.003 covers backdoored
pam_unix.soand credential harvesting modules; watch/etc/pam.dand the module directories and verify packages.
How identity and access work in Linux PAM
An application that links libpam names a service (for example sshd, login, sudo). libpam reads /etc/pam.d/<service>; when the /etc/pam.d directory exists, /etc/pam.conf is ignored, and vendor defaults in /usr/lib/pam.d are only used when no file of the same name exists in /etc/pam.d. Each line has a type, a control and a module with arguments, and the lines of one type form a stack evaluated top to bottom.
The four management groups
| Type | What it decides | Typical modules | Security relevance |
|---|---|---|---|
| auth | Is this the claimed user, and which credentials or groups do they get | pam_unix, pam_sss, pam_faillock, OTP or smart card modules | Where MFA and lockout are enforced |
| account | Is the account allowed now (expiry, time, origin, access rules) | pam_unix, pam_sss, pam_access, pam_faillock | Where host access rules and AD or IdM access policy apply; runs even for SSH key logins when sshd uses PAM |
| password | Changing the authentication token | pam_pwquality, pam_pwhistory, pam_unix | Password quality, history and hash algorithm |
| session | Set-up and tear-down around the service | pam_limits, pam_systemd, pam_mkhomedir, pam_loginuid | Audit identity (loginuid), resource limits, home directory creation |
Control flags, read precisely
The pam.conf(5) manual defines each keyword as shorthand for a bracket expression, and those equivalents are the clearest way to reason about a stack.
| Control | Bracket equivalent | Effect on failure | Effect on success |
|---|---|---|---|
| required | [success=ok new_authtok_reqd=ok ignore=ignore default=bad] | Stack fails, but remaining modules still run (no early signal to the attacker) | Continue |
| requisite | [success=ok new_authtok_reqd=ok ignore=ignore default=die] | Return immediately | Continue |
| sufficient | [success=done new_authtok_reqd=done default=ignore] | Ignored | Return success at once if no earlier required module failed |
| optional | [success=ok new_authtok_reqd=ok default=ignore] | Ignored unless it is the only module of that type | Continue |
| include / substack | Pulls in lines of the same type from another file | substack contains done and die inside the included file | Same |
- Jumps:
[success=2 default=ignore]skips the next two modules on success. Adding or removing a line below a jump silently changes what is skipped, which is the most common way hand edits weaken a stack. - Missing modules: a leading
-on the type (for example-auth) only suppresses the log entry when a module is not installed. Do not use it on a security control you expect to be present. - Return codes without an action: when no
default=is given, an unlisted return code is treated asbad, so test unusual paths such as unknown users and expired passwords.
Distribution tooling
- RHEL, Rocky, Alma, Fedora: authselect selects a profile (
sssdis the default,winbind,minimal) and optional features such aswith-faillock,with-mkhomedir,with-smartcardandwith-pamaccess. Red Hat advises against editing the generated files by hand and against changing profiles thatipa-client-installorrealm joinconfigured; useauthselect currentto record the state first andauthselect create-profilefor site changes. - Debian and Ubuntu: package profiles in
/usr/share/pam-configsare merged bypam-auth-updateintocommon-auth,common-account,common-passwordandcommon-session. Local changes outside the managed block are kept, changes inside it are replaced. - SUSE:
pam-configmaintains thecommon-*-pcfiles in a similar way.
The practical rule: change PAM through the tool and its custom profile mechanism, then commit the resulting files to configuration management so drift is visible.
Admin roles and privileged access
PAM is the enforcement point for several privilege boundaries, and each has its own service file.
- su:
auth required pam_wheel.so use_uidrestricts su to members ofwheel(or a namedgroup=). Avoid thetrustoption: it returns success for wheel members and, stacked above the password module, lets them become root without a password. - sudo:
/etc/pam.d/sudocontrols whether sudo re-authentication uses the same MFA and lockout as login. If the policy says privileged elevation needs MFA, the sudo stack must include the MFA module, not only sshd. The rule design itself is covered in the sudo and sudoers Security Guide. - Root and lockout: pam_faillock does not lock root unless
even_deny_rootis set, androot_unlock_timecan give root a shorter unlock. Decide this deliberately: locking root on a server with no console access can turn a password spray into an outage, so pair it with a tested break-glass path. - Who can log in at all:
pam_accessin the account stack reads/etc/security/access.confandaccess.d, first match wins. A rule such as-:ALL EXCEPT (linux-admins) (app-operators):ALLfollowed by nothing else restricts a server to two groups. When SSSD is used, itsaccess_provideroften does the same job; pick one place and document it. - Break-glass: keep one local, vaulted account that bypasses directory dependencies, with a long random password, logging and alerting on use. Test it after every PAM change.
Adding MFA without weakening the stack
Common options are TOTP or HOTP modules (for example the open source google-authenticator-libpam module), smart card and PIV through pam_sss with SSSD, RADIUS modules that call an existing MFA server, and vendor PAM modules for push MFA. Whatever the module, the same design questions apply.
| Decision | Safer choice | Why |
|---|---|---|
| Control flag for the second factor | required | The google-authenticator-libpam project recommends both the password module and the OTP module be required rather than requisite, so failures are not revealed early |
| Users not yet enrolled | Enrol before enforcing; use nullok only for a dated rollout window | With nullok the module returns IGNORE, so another module must supply success, and unenrolled users effectively have single factor |
| SSH key logins | AuthenticationMethods publickey,keyboard-interactive | sshd only runs the PAM auth stack for keyboard-interactive or password; with publickey alone PAM runs account and session only |
| Password plus OTP over SSH | Disable either PasswordAuthentication or KbdInteractiveAuthentication | The sshd_config manual warns both usually reach the same PAM path |
| Where the secret lives | Root owned path via secret= or a central MFA service | Per-user ~/.google_authenticator files are readable by anyone who compromises that account |
| Phishing resistance | Smart card, PIV or FIDO-backed SSH keys for administrators | OTP codes can be relayed; see the Phishing-Resistant MFA Guide |
Note that the upstream sshd default for UsePAM is no; most distributions ship yes. Check the effective value with sshd -T rather than reading the file.
Integrations, API tokens and service accounts
PAM is not only for people. Several non-human paths run through it, and they are often where exceptions accumulate.
- Directory integration: pam_sss (SSSD) or pam_winbind hand authentication to AD, FreeIPA or LDAP. The machine account and keytab that make this work are non-human credentials; see the SSSD and Linux Active Directory Integration Guide and the Linux Service Accounts and Keytab Management Guide.
- Service accounts and cron:
/etc/pam.d/crondruns account and session modules for scheduled jobs. A pam_access rule that denies a service account interactive login should still let its cron jobs run; test both. - Automation over SSH: configuration management and backup tools usually log in with keys. Requiring MFA for humans while exempting automation is reasonable, but do it with an explicit sshd
Match UserorMatch Groupblock and a pam_access origin restriction, not by addingnullokor asufficientbypass to the shared stack. - Application logins: web consoles, databases and VPN servers that authenticate against PAM (for example a
/etc/pam.d/openvpnfile) inherit whateversystem-authorcommon-authincludes. Review these services when you change the shared stack. - MFA service credentials: RADIUS shared secrets and push MFA API keys stored in module configuration files are secrets: restrict file permissions, rotate them and keep them out of images.
Logging and detection
PAM modules log through syslog (/var/log/secure on RHEL family systems, /var/log/auth.log on Debian family systems, or the journal), and auditd records USER_AUTH, USER_ACCT, CRED_ACQ, USER_LOGIN and USER_START events for PAM-aware services. The Linux Logging and auditd for Defenders Guide covers collection; the PAM specific signals are below.
| Signal | Source | What it can mean |
|---|---|---|
Write to /etc/pam.d/*, /etc/security/*, /etc/authselect | auditd watch (-w /etc/pam.d -p wa), FIM | Unapproved stack change or backdoor insertion |
New or changed file in the module directory (/usr/lib64/security, /lib/x86_64-linux-gnu/security) | FIM, rpm -V pam, dpkg --verify libpam-modules | Replaced pam_unix.so (Skidmap pattern) or a credential logging module |
| Burst of pam_faillock lock messages | syslog, faillock --user | Password spraying or a broken service account |
| Successful auth with no matching failure history from new source | auditd USER_AUTH with addr, sshd logs | Backdoor password, as ATT&CK DET0454 suggests correlating file changes with odd logins |
| Module not found or dlopen errors | syslog | Removed or broken control, possible fail-open |
| Break-glass account authentication | syslog, SIEM rule | Emergency use or misuse; always alert |
Metrics that show it is working
- Stack conformance: percentage of hosts whose PAM files match the approved authselect or pam-auth-update profile hash.
- MFA coverage: percentage of interactive admin logins (SSH and sudo) that completed a second factor, from auth logs.
- nullok exposure: count of hosts with
nullokon pam_unix or an MFA module; target zero after the rollout date. - Integrity: hosts with any failed package verification of PAM modules; target zero, each one investigated.
- Lockout noise: weekly lockout events by account type; service accounts appearing here usually mean a stale stored password.
Hardening baseline
A baseline that fits most enterprise fleets, expressed as settings you can check. Pair it with the wider Linux Server Hardening Guide.
| Area | Setting | Check |
|---|---|---|
| Blank passwords | No nullok on pam_unix; PermitEmptyPasswords no in sshd | grep -r nullok /etc/pam.d |
| Lockout | pam_faillock via authselect enable-feature with-faillock or pam-auth-update; deny, fail_interval, unlock_time set in /etc/security/faillock.conf to policy | Values in faillock.conf, not module arguments |
| Persistent tally | dir= in faillock.conf pointing to a persistent directory if lockout must survive reboot | Default /var/run/faillock is tmpfs on most systems |
| Password hashing | yescrypt or SHA-512 on the pam_unix password line, aligned with ENCRYPT_METHOD in login.defs | Hash prefix in /etc/shadow ($y$ or $6$) |
| Password history | pam_pwhistory rather than pam_unix remember, which stores MD5 hashes in /etc/security/opasswd | Module present in password stack |
| Quality | pam_pwquality with length and dictionary settings matching your policy, enforced for root if required | /etc/security/pwquality.conf |
| Who can log in | pam_access or SSSD access provider limiting each server class to named groups | Test with a non-member account |
| su | pam_wheel with use_uid, no trust | /etc/pam.d/su |
| Audit identity | pam_loginuid in session stacks so auditd records the original user | auid in audit records |
| Integrity | FIM and package verification on PAM files and modules; SELinux or AppArmor enforcing | Scheduled report |
Rolling out a stack change safely
- Record the current state:
authselect currentor a copy of/etc/pam.d, plussshd -Toutput. Commit it so rollback is a known file set. - Keep a root session open on every host you change, and confirm console or out-of-band access exists before touching auth stacks.
- Build the change in a custom profile (authselect) or a pam-configs snippet, never as ad hoc edits.
- Test on a lab host with a matrix: local user, directory user, service account cron job, SSH key plus OTP, sudo, su, wrong password, unknown user, locked user, expired password.
- Pilot on a small ring (one server class, one week) with MFA modules in a dated
nullokwindow only if enrolment is incomplete. - Roll out by configuration management and remove
nullokon the announced date. - Rollback:
authselect select <previous profile>with its features, or restore the committed files, then restart sshd and re-run the test matrix.
Common misconfigurations
- A sufficient line above the real check: placing a module such as pam_permit, a loosely configured pam_succeed_if or an MFA module with
sufficientabove pam_unix means its success ends the stack. Read everysufficientanddoneas a bypass and justify it. - MFA configured in PAM but never reached: sshd accepts publickey alone because
AuthenticationMethodswas not set, or sudo uses a different stack. Test the real path, not the file. - nullok left after rollout: on pam_unix it permits blank passwords; on an MFA module it turns the second factor off for everyone who never enrolled.
- Hand edits fighting the tool: an edited
system-authis overwritten by the nextauthselect apply-changesor package update, or the tool refuses to run, leaving hosts in different states. - Lockout that does not lock: pam_faillock missing from
password-auth(which sshd uses on RHEL), placed after asufficientmodule, or relying on tally files that vanish at reboot. - No integrity monitoring of modules: backdoored PAM modules are a documented persistence method (ATT&CK T1556.003; Skidmap replaced pam_unix.so to accept a fixed password). The MITRE ATT&CK T1556 Modify Authentication Process Guide covers the technique family; for Linux, file integrity monitoring and package verification are the practical controls.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | ATT&CK |
|---|---|---|---|
| MFA for privileged and remote access | IA-2(1), AC-17 | PR.AA-03 | T1110, T1078.003 |
| Lockout | AC-7 | PR.AA-03 | T1110.001 |
| Password quality and hashing | IA-5 | PR.AA-01 | T1003.008 |
| Who may log in, su restriction | AC-6 | PR.AA-05 | T1548.003 |
| Stack under configuration management | CM-6 | PR.PS-01 | T1556.003 |
| Module and file integrity | SI-7 | DE.CM-09 | T1556.003 |
| Authentication logging | AU-2, AU-12 | PR.PS-04 | T1556 |
Practitioner checklist
- Inventory PAM stacks per host class and record the authselect profile or pam-auth-update state in configuration management.
- Annotate every
sufficient,doneandsuccess=Njump with why it exists; remove the ones nobody can explain. - Remove
nullokfrom pam_unix and set a removal date for any MFA rollout exception. - Enforce MFA for administrators on both SSH (
AuthenticationMethods) and sudo, and prefer phishing-resistant factors for privileged roles. - Set pam_faillock values in faillock.conf to match policy, decide on root lockout explicitly and make the tally persistent if required.
- Restrict logins per server class with pam_access or the SSSD access provider, and test service account cron jobs afterwards.
- Watch
/etc/pam.d,/etc/securityand the module directories with auditd or FIM, and run package verification of PAM on a schedule. - Keep and test a vaulted break-glass account and an open root session for every PAM change.
Standards and references
- Linux-PAM: pam.conf(5) PAM configuration files
- Linux-PAM: pam_faillock(8) module for locking after failed attempts
- Linux-PAM: access.conf(5) login access control table
- Red Hat: Configuring user authentication using authselect (RHEL 9)
- OpenSSH: sshd_config(5)
- MITRE ATT&CK: Modify Authentication Process: Pluggable Authentication Modules
- Google: google-authenticator-libpam PAM module
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: sudo and sudoers Security Guide · SSSD and Linux Active Directory Integration Guide · MITRE ATT&CK T1556 Modify Authentication Process Guide · Linux File Integrity Monitoring Guide · MFA Guide