PAM Explained: How Linux Actually Decides Who Gets In
When you type a password, the program asking for it does not check it. It calls PAM, which runs a configured list of modules and reports back. That indirection is why you can add two-factor authentication to ssh without recompiling ssh.
The problem it solves
Before PAM, every program that authenticated users implemented the checking itself. Adding a new method meant patching login, su, sudo, sshd, the display manager and everything else.
Pluggable Authentication Modules moves the decision out of the programs. They ask PAM; PAM consults a config file and runs the modules it names.
ls /etc/pam.d/
# sshd sudo login su passwd common-auth common-account ...
# is a program PAM aware
ldd $(which sudo) | grep libpam
One file per service. The name matches what the program passes to PAM, which is usually but not always its own name.
The four types
Each line begins with a type. These are four independent stacks, and a rule in one has no bearing on the others.
| Type | Question it answers |
|---|---|
auth | Are you who you claim to be |
account | Is this account allowed in right now |
password | How do credentials get changed |
session | What is set up before and torn down after |
The account and auth split is the one people miss. A correct password satisfies auth. An expired account, a login outside permitted hours, or a group restriction fails account, and the result is a rejection that looks identical to a wrong password unless you read the logs.
session is why your home directory can be mounted on demand, why limits from /etc/security/limits.conf apply, and why systemd knows you have a session at all. Our ulimit guide covers the limits side.
Control flags, which are the actual logic
auth required pam_unix.so
auth sufficient pam_sss.so
auth requisite pam_deny.so
auth optional pam_cap.so
required must succeed. If it fails, the failure is recorded and the rest of the stack still runs, then an error is returned.
requisite must succeed, and failure returns immediately.
sufficient succeeding ends the stack successfully, provided no earlier required module failed. Failure is ignored and processing continues.
optional affects the result only if it is the sole module in the stack.
The reason required is the common choice over requisite is subtle and deliberate: continuing through the whole stack means the time taken and the error returned do not reveal which check failed. requisite short-circuits, which leaks that information.
There is also a richer bracket syntax for cases the four keywords cannot express:
auth [success=1 default=ignore] pam_succeed_if.so uid >= 1000 quiet
The numbers are how many subsequent lines to skip. It is dense, it is used heavily in distribution-generated files, and it is not something to write by hand unless you have a specific reason.
Order is the policy
# this works
auth sufficient pam_ssh_agent_auth.so
auth required pam_unix.so
# this never reaches the first line
auth required pam_unix.so
auth sufficient pam_ssh_agent_auth.so
PAM stacks run top to bottom. Moving a line changes the policy, and a rule placed after the one that succeeds is never consulted.
This is the single most common source of PAM configurations that do not do what their author intended.
Reading a real file
cat /etc/pam.d/sudo
#%PAM-1.0
auth sufficient pam_unix.so try_first_pass
auth required pam_deny.so
account required pam_unix.so
session required pam_limits.so
On Debian and Ubuntu you will more often see includes:
@include common-auth
@include common-account
@include common-session-noninteractive
@include pulls in a shared stack, which is how a change to password policy applies to every service at once. The trade-off is that a mistake in common-auth affects everything.
Modules worth knowing
| Module | What it does |
|---|---|
pam_unix.so | Traditional /etc/shadow passwords |
pam_pwquality.so | Password strength rules |
pam_faillock.so | Lock an account after repeated failures |
pam_limits.so | Applies limits.conf |
pam_wheel.so | Restricts su to a group |
pam_time.so | Time-of-day restrictions |
pam_exec.so | Runs an external command |
pam_env.so | Sets environment variables |
pam_mkhomedir.so | Creates a home directory on first login |
pam_google_authenticator.so | TOTP two-factor |
# what is installed
ls /lib/x86_64-linux-gnu/security/
ls /usr/lib64/security/
# documentation for one
man pam_faillock
Every module has a man page, and they are unusually good. man pam.conf covers the syntax.
Failed logins and lockouts
# current state
faillock --user alice
# clear it
sudo faillock --user alice --reset
auth required pam_faillock.so preauth silent deny=5 unlock_time=900
auth [default=die] pam_faillock.so authfail deny=5 unlock_time=900
account required pam_faillock.so
pam_faillock needs three lines in two different stacks, which is why half-configured lockout policies are common. The preauth line checks, authfail records, and the account line enforces.
Weigh this against denial of service: an attacker who knows your usernames can lock out every account by failing repeatedly. On an internet-facing box, key-only ssh plus fail2ban is usually the better shape, as our ssh hardening guide argues.
Editing without locking yourself out
This is the part that matters more than any of the above.
Open a second terminal with a working root shell and leave it open. An existing session is not re-authenticated, so it survives a broken config and can undo it.
# terminal one, keep this alive
sudo -i
# terminal two, make the change
sudo nano /etc/pam.d/sshd
# terminal three, test without closing anything
ssh localhost
Other protections:
# back up first
sudo cp -a /etc/pam.d /root/pam.d.bak
# watch the failures as they happen
sudo journalctl -f | grep -i pam
sudo tail -f /var/log/auth.log
A PAM file with a syntax error or a missing module fails the whole stack, which means nothing authenticates. There is no partial success. On a remote machine with no console access, that is a rebuild.
On Debian and Ubuntu, prefer the vendor tooling:
sudo pam-auth-update
It regenerates the common-* files from profiles, which means your hand edits to those files get overwritten on the next update. Local changes belong in the service-specific files or the locations your distribution designates.
Adding two-factor to ssh
A concrete example, and one worth doing carefully.
sudo apt install libpam-google-authenticator
google-authenticator # as the user, not root
# /etc/pam.d/sshd
auth required pam_google_authenticator.so nullok
# /etc/ssh/sshd_config
KbdInteractiveAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive
nullok lets users who have not enrolled yet still log in, which you remove once everyone has. AuthenticationMethods requiring both means a key alone is not enough.
Test in a new session before closing the old one. Every time.
Debugging
# most modules accept this, and it is noisy
auth required pam_unix.so debug
# watch
sudo journalctl -f -u sshd
sudo journalctl -f _COMM=sudo
The log line naming the module that rejected you is usually the whole answer. Our journalctl guide covers filtering it.
Remove debug afterwards. It logs a great deal, some of it about authentication attempts you would rather not keep.
Frequently Asked Questions
What does PAM actually do?
It separates authentication policy from the programs that need it. Instead of login, sudo and sshd each implementing password checking, they call PAM, which runs a configured stack of modules. Changing the policy means editing a config file rather than patching every program.
What is the difference between required and requisite?
Both must succeed. A required module that fails records the failure and continues running the rest of the stack before returning an error. A requisite module that fails returns immediately. Required is the usual choice because stopping early can reveal which check failed.
What are the four PAM module types?
auth verifies identity, account checks whether the verified identity is currently allowed in, password handles changing credentials, and session sets up and tears down the environment around a login. They are independent stacks, so a rule in one has no effect on the others.
Why did my sudo stop working after I edited a PAM file?
A syntax error or a missing module in a PAM stack causes the whole stack to fail, which means no authentication succeeds. This is why you keep a root shell open in a second terminal before editing anything under /etc/pam.d, since that session stays alive and can undo the change.
Does editing /etc/pam.d survive a package update?
Not reliably. Distributions manage these files, and Debian and Ubuntu regenerate the common files through pam-auth-update. Put local changes in the places your distribution designates for them and use the vendor tooling where it exists, rather than editing the common files directly.
How do I add two-factor authentication to ssh with PAM?
Add a module such as pam_google_authenticator to the auth stack for sshd and set ssh to use keyboard-interactive authentication. The ordering and the ssh settings both matter, and you should test it in a second session before closing the one you configured it from.