Primer · Linux · Authentication

PAM Primer

Every login, sudo, and password change on Linux runs through PAM — a stack of small, swappable modules, not one hardcoded password check. This is a focused read on how that stack is built and read, so that the FIDO lab's biggest change — adding a hardware key to the exact same stack — makes sense before you ever open a config file.

No VM required Linux Mint / Debian 10 sections Bridges to the FIDO Lab
Contents
Where this fits. This primer assumes you can already find your way around a shell and use sudo. If that's not solid yet, read the Linux Command & File Reference primer first — this one picks up exactly where its PAM section leaves off and goes deeper, on purpose, because PAM is what the FIDO lab modifies. Command blocks with a copy button are things you could type; dimmed blocks headed OUTPUT are representative results, nothing to run.

1. What PAM actually is

Before PAM, "can this user log in?" was a question every program answered for itself, usually by reading /etc/passwd directly. Change how authentication works, and you had to patch every program that authenticated anyone. Pluggable Authentication Modules fixes that by putting one layer in between: programs like login, sudo, su, passwd, and sshd don't check credentials themselves — they ask PAM, and PAM runs whatever modules are configured for that program.

The payoff is that none of those programs need to know or care how identity gets proven. A module can check a password, a fingerprint, a time-of-day restriction, or — the whole reason this primer exists — a touch on a hardware key. Swap the module, and every program that defers to PAM picks up the new behavior automatically.

PAM organizes its work into four facilities, each answering a different question:

FacilityQuestion it answers
authAre you who you say you are?
accountIs this account currently allowed to log in? (expired, locked, time-restricted…)
passwordHow is a new password set or checked?
sessionWhat has to be set up (or torn down) around a login — home directory, environment, logging?
Keep these four straight. Section 4 shows the same module, pam_unix.so, doing different jobs depending on which facility calls it — that only makes sense once the four are distinct in your head.

2. Anatomy of a rule

Every line in a PAM config is a rule with four columns:

ColumnMeaning
facilityWhich of the four questions from Section 1 this line answers.
controlWhat a success or failure from this module should do to the stack.
moduleThe .so file that does the actual work, e.g. pam_unix.so.
argsOptions passed straight through to that module.

Control: what a result means to the stack

A facility is rarely just one module — it's a short stack of them, evaluated top to bottom. The control field decides how one module's result affects the rest of the stack.

  • required — must succeed for the overall facility to succeed, but on failure PAM keeps evaluating the remaining modules before reporting failure. This is deliberate: it stops an attacker from timing responses to figure out which specific check rejected them.
  • requisite — same as required, but on failure it stops immediately and reports failure right away, skipping whatever comes after.
  • sufficient — if this module succeeds and no earlier required module already failed, the whole facility succeeds immediately, skipping whatever comes after.
  • optional — its result usually doesn't affect the outcome, unless it's the only module in the stack.

There's also a more precise bracket syntax, [value1=action1 value2=action2 …], for when the four keywords above aren't specific enough. You'll see it in real configs — for example, the common-password line on a stock system:

Example — /etc/pam.d/common-password
password  [success=1 default=ignore]  pam_unix.so obscure yescrypt minlen=10 remember=5

success=1 means "on success, skip the next 1 rule in this stack" (there's a fallback module below it that only runs if this one didn't handle things). default=ignore means "for any other result, don't let it affect the overall outcome by itself — move on." It's the same idea as the four keywords, just spelled out precisely instead of relying on a preset meaning.

Security lens. sufficient is the control that adds a hardware key as an alternative way to log in rather than an additional requirement — succeed with the key, and the password check underneath never runs. Whether that's what you want, or whether you want the key to be required alongside the password, is a real design decision, not a default to accept blindly. The FIDO lab uses the bracket syntax to be explicit about which one it means — you'll recognize it in Section 8.

3. The stack files

PAM configuration lives in /etc/pam.d/, one file per "service" — the name a program passes when it talks to PAM. login, sudo, sshd, and passwd are all separate services with their own files. But on Debian and Ubuntu, those per-service files are usually short: most of the real rules live in four shared files — common-auth, common-account, common-password, common-session — and each service file just pulls them in with @include.

Normal user shell
cat /etc/pam.d/sudo
Example output
# Set up user-specific environment
session    required   pam_env.so readenv=1 user_readenv=0
session    required   pam_env.so readenv=1 envfile=/etc/default/locale user_readenv=0
@include common-auth
@include common-account
@include common-session-noninteractive

Read that as: "for authentication and account checks, do whatever common-auth and common-account say." That indirection is exactly why editing PAM is so consequential — a single change to common-auth changes how every service that includes it behaves, not just one.

Why common-session-noninteractive and not common-session? A handful of services (like sudo and cron) don't want the interactive parts of session setup — a message-of-the-day banner, for instance — running every time. The -noninteractive variant is the same idea, trimmed for a service that isn't a real login.

4. pam_unix.so — the password module

pam_unix.so is the traditional Unix password check, and it's a good example of how one module can serve more than one facility with different behavior in each: as an auth module it checks a typed password against the stored hash; as a password module it sets a new hash when one is changed; as an account module it can enforce expiry. Same .so, different job, depending on which stack calls it.

Normal user shell
grep pam_unix /etc/pam.d/common-password
Example output
password  [success=1 default=ignore]  pam_unix.so obscure yescrypt minlen=10 remember=5
  • obscure — run basic strength checks (not just a dictionary word, not the username, etc).
  • yescrypt — the hashing scheme used to store the password.
  • minlen=10 — require at least ten characters.
  • remember=5 — block reuse of the last five passwords.
Security lens. minlen and remember both raise the cost of a guessed or cracked credential — minlen by making brute-force more expensive, remember by stopping the "change it, then change it right back" workaround.

5. pam_faillock.so — lockout

pam_unix.so checks whether a password is correct, but it doesn't count how many times someone got it wrong. pam_faillock.so adds that: after too many consecutive failures, the account is locked out for a while. It wraps the existing auth line rather than replacing it, which is a good second example of the control keywords from Section 2 in real use:

Example — /etc/pam.d/common-auth
auth  requisite                    pam_faillock.so preauth silent deny=5 unlock_time=900
auth  [success=1 default=ignore]   pam_unix.so
auth  [default=die]                pam_faillock.so authfail deny=5 unlock_time=900
auth  required                     pam_faillock.so authsucc
  • The first line is requisite on purpose: if the account is already locked, stop right there — there's no point running the password check at all.
  • deny=5 — lock after 5 consecutive failures.
  • unlock_time=900 — unlock automatically after 900 seconds (15 minutes).
  • Check or clear counts with faillock --user user1 and faillock --user user1 --reset.
Security lens. Lockout is the direct counter to password-guessing — but it's also a denial-of-service switch pointed at every account on the box, including yours, if the numbers are set wrong. Section 6 covers how to change this without becoming your own attacker.

6. Editing PAM safely

A typo in /etc/pam.d/ doesn't throw an error you'll see before it matters — it can lock out every account, including root, the next time someone tries to authenticate. The rule that makes this safe to work in is simple and non-negotiable:

Before you edit anything here, open a second terminal and get a root shell in it (sudo -i is enough). Make your change, save, and test login/sudo from a third terminal — while the root shell from step one stays open. If the change locked you out, that still-open root shell is how you undo it. Never close your editor on a PAM change you haven't tested.

Two more things worth knowing before you touch common-* files directly:

  • On Debian and Ubuntu, pam-auth-update manages the common-* files for you from small "profile" fragments, and is the preferred way to enable or disable a whole authentication method — including pam_u2f, which is how the FIDO lab plugs it in. Hand-editing is for understanding what's there and for fine control the profile system doesn't expose.
  • Keep a break-glass account or password-only login path available while you experiment. The FIDO lab insists on this for the same reason this section does: authentication changes are the one class of mistake where "just fix the file" isn't an option if you're the one locked out.

7. Watching it happen: journalctl for PAM

None of the modules above print anything to the terminal you're testing in. Success or failure both happen silently from your point of view — every PAM module logs through syslog instead, which on a systemd system means the journal. Section 6 already told you to test from a second terminal before trusting an edit; this is what you'd have open in a third one, watching it happen live.

Uses sudo
journalctl -f

That alone is the single most useful command while testing a PAM edit — it streams new log lines as they arrive, so a failed sudo attempt in one terminal shows up in this one within a second. A few more ways to narrow it down:

# Only lines from one program's PAM stack
journalctl -t sudo
journalctl _COMM=sshd

# Grep the message text directly — finds a module by name regardless of what invoked it
journalctl -g pam_faillock

# Errors and worse, since the current boot
journalctl -b -p err

# Just the last few minutes — narrows to your actual test window
journalctl --since "5 minutes ago"

A real pam_unix failure followed by a pam_faillock lockout — the two modules from Sections 4 and 5 — reads like this in the journal:

Example output
sudo: pam_unix(sudo:auth): authentication failure; logname= uid=1000 euid=0 tty=/dev/pts/1 ruser= rhost= user=user1
sudo: pam_faillock(sudo:auth): Consecutive login failures for user user1 account temporarily locked
Security lens. The same stream you're watching to confirm your own test worked is what you'd pull during an incident to reconstruct an authentication attack timeline — journalctl -g pam_ across a wider --since window shows every module's decisions, not just the one you're testing. And practically: journalctl -f running in real time is the fastest confirmation that a break-glass path still works, before you ever close the editor on a common-auth change.

8. Preview: pam_u2f

Everything above is general-purpose PAM literacy. This section previews one specific module — pam_u2f.so — so its shape is already familiar when the FIDO lab has you install and configure it for real. This is a preview, not a walkthrough: no VM is required here, and nothing below needs to be typed.

pam_u2f plugs into the auth facility exactly like pam_unix.so or pam_faillock.so above — same four columns, same stack. What it checks is a hardware security key instead of a typed password:

Example — an auth line using pam_u2f
auth  [success=done default=die]  pam_u2f.so authfile=/etc/security/u2f_keys cue pinverification=1
  • authfile=/etc/security/u2f_keys — a central, root-owned file mapping usernames to registered key credentials, instead of each user managing their own.
  • cue — prompt the user to touch the key. If you need custom prompt text, it takes square brackets, not quotes: cue [cue_prompt=touch your key now] — PAM splits module arguments on whitespace and does not understand shell-style quoting, so a quoted cue="..." silently breaks into several unrecognized options instead of one prompt.
  • pinverification=1 — require the key's PIN in addition to the touch.
  • [success=done default=die] — the bracket syntax from Section 2, but stricter than the common-password example: succeed here and the entire auth stack succeeds immediately (done); fail here and the entire stack fails immediately (die). No password fallback happens either way.
Security lens — check the version before you trust it. PIN verification (pinverification=1) is the control the FIDO lab teaches as its second factor, and it is only trustworthy on libpam-u2f 1.1.1 or later. Version 1.1.0, which introduced the feature, shipped a PIN-authentication bypass (CVE-2021-31924), fixed in 1.1.1. The lab will have you verify this on the actual VM with dpkg -l libpam-u2f — worth remembering why that check exists.

9. After the decision: pam_systemd & loginctl

Section 1 split PAM into four facilities. pam_u2f above lives in auth — it's part of the decision. This module lives in session, the facility that only runs after auth has already succeeded: it doesn't decide whether you get in, it sets things up once you have.

Example — /etc/pam.d/common-session
session  optional  pam_systemd.so

optional here isn't an accident — it's the control keyword from Section 2 doing exactly what it's for. pam_systemd isn't gating anything, so its result shouldn't be able to affect whether the session as a whole succeeds. What it actually does: registers the now-successful login with systemd-logind, which creates a session object, a session-N.scope unit that puts every process from that login into its own cgroup, and sets XDG_RUNTIME_DIR / XDG_SESSION_ID for it.

loginctl is the inspection tool for what pam_systemd just created — effectively systemctl for logged-in humans instead of services:

Normal user shell
loginctl list-sessions
Example output
SESSION  UID  USER   SEAT     TTY
      3 1000  user1  seat0    tty2
      7 1000  user1           pts/1
loginctl session-status 3
Security lens. loginctl list-sessions is a more complete "who's on this box" than who or w — it shows session type (console, X11, Wayland, or a bare SSH pts) and whether each one is active, and every row corresponds to a real cgroup you could inspect or terminate with loginctl terminate-session <id>. It's the same unit-and-cgroup machinery systemd uses for ordinary services — applied to logins instead of daemons.

10. Where this goes next

That's the whole shape of PAM: four facilities, four-column rules, a small set of control keywords that decide how a stack behaves, and shared common-* files that make one edit apply everywhere. Everything the FIDO lab does — installing pam_u2f, wiring it into common-auth with a specific control, gating it to a group, testing it from a break-glass session before trusting it — is an application of exactly what's above, not new machinery.

Next step. Head to the FIDO Lab to install and configure pam_u2f for real, on a VM, with a break-glass account preserved throughout. If you want the account/permissions/services context PAM sits inside, the Linux Command & File Reference primer covers that ground, and the Linux Hardening Checklist turns this material into a scored self-audit once you're ready to test it.