Where to start First steps track · 15 stages
Linux Labs

PAM: login checks, password policy and lockout

15 min Ubuntu 24.04 · Linux-PAM 1.5.3 · libpwquality 1.4.5

The module stack in /etc/pam.d decides whether to let a user in, which password to accept and when to lock them out.

Cheat sheet

7 commands Printable version
  • grep -v '^#' /etc/pam.d/common-auth | grep . the login check stack
  • sudo pam-auth-update --enable pwquality turn on pwquality
  • echo 'Lab-Pass-2026' | pwscore rate a password first
  • sudoedit /etc/security/pwquality.conf password rules
  • sudo faillock --user alice alice's failed logins
  • sudo faillock --user alice --reset lift the lockout
  • sudo journalctl -t su | grep -i fail su refusals in the log

Overview

PAM (Pluggable Authentication Modules) is the library that programs hand user checks to. login, su, sudo, sshd and passwd do not check passwords themselves: they call PAM, and PAM reads the file named after the program in /etc/pam.d/ and runs through its modules. To change the policy — password length, lockout after failures — you change the module stack and leave the programs alone. The files are read at every login, so an edit takes effect at once.

A program's file usually pulls in shared files with @include:

text
student@ubuntu:~$ grep -E '^(auth|@include)' /etc/pam.d/su
auth       sufficient pam_rootok.so
@include common-auth
@include common-account
@include common-session

sudo and sshd are built the same way: editing common-auth changes the password check for all of them at once.

Four stacks

Every line starts with a type — which stage of the login it belongs to:

Type When it runs Modules on the machine
auth checking who you are: the password pam_unix, pam_faillock
account whether this account may log in now: expiry, lockout pam_unix, pam_nologin
password changing the password pam_pwquality, pam_unix
session what to do at login and logout pam_limits, pam_umask, pam_systemd

Key login in sshd skips the auth stack: sshd checks the key itself. The account and session stacks run on key login too.

Reading a line

text
auth	[success=1 default=ignore]	pam_unix.so nullok

Type, control flag, module and its arguments. Modules run top to bottom, and the flag says what to do with their answer:

Flag Module says "yes" Module says "no"
required go on refuse, but the stack runs to the end
requisite go on refuse at once
sufficient success at once if nothing failed before go on
optional matters only if it is the only module in the stack —
[success=N default=ignore] skip the next N lines ignore, go on

The auth stack on the machine:

text
student@ubuntu:~$ grep -v '^#' /etc/pam.d/common-auth | grep .
auth	[success=1 default=ignore]	pam_unix.so nullok
auth	requisite			pam_deny.so
auth	required			pam_permit.so
auth	optional			pam_cap.so

Right password: pam_unix jumps over one line — pam_deny — to pam_permit, and the login is allowed. Wrong password: the next line, pam_deny, refuses. Line order is part of the logic here: a line inserted between pam_unix and pam_deny changes where success=1 leads.

pam-auth-update

On Ubuntu the common-* files are assembled by pam-auth-update from the profiles in /usr/share/pam-configs/. The machine has five: capability, mkhomedir, pwquality, systemd, unix. A profile is the lines plus where to put them:

text
student@ubuntu:~$ cat /usr/share/pam-configs/pwquality
Name: Pwquality password strength checking
Default: yes
Priority: 1024
Conflicts: cracklib
Password-Type: Primary
Password:
	requisite			pam_pwquality.so retry=3

On the lab machine the profile is off. sudo pam-auth-update --enable pwquality turns it on and rebuilds common-password:

Type Flag Module and arguments
password requisite pam_pwquality.so retry=3
password [success=1 default=ignore] pam_unix.so obscure use_authtok try_first_pass yescrypt
password requisite pam_deny.so
password required pam_permit.so

The first line is new. use_authtok tells pam_unix to take the password pam_pwquality has already checked instead of asking again.

If the common-* files were edited by hand, pam-auth-update first asks Override local changes to /etc/pam.d/common-*?. yes rebuilds the files from the profiles and wipes the manual lines; no leaves the files as they are — and the profile does not get enabled:

text
pam-auth-update: Local modifications to /etc/pam.d/common-*,
  not updating.
pam-auth-update: Run pam-auth-update --force to override.

pwquality: password quality

pam_pwquality checks a new password when it changes. The rules live in /etc/security/pwquality.conf or in files under /etc/security/pwquality.conf.d/*.conf (the directory does not exist on the machine; create it with mkdir). Everything in the machine's pwquality.conf is commented out, so the defaults apply:

Setting Meaning Default
minlen minimum length 8
minclass how many of four classes: lower case, upper case, digits, others 0
dcredit, ucredit, lcredit, ocredit -N requires at least N digits, upper, lower, other characters 0
maxrepeat how many identical characters in a row are allowed 0 — no check
dictcheck dictionary check 1
enforce_for_root refuse root too off

A positive *credit value works differently: each such character, up to N of them, counts twice towards the length. retry=3 is an argument of the module in the pam.d line: how many times passwd asks again.

pwscore checks a password in advance. It reads the same pwquality.conf but does not look at PAM — a rule shows up in pwscore even before pam-auth-update. The answer is a score from 0 to 100 or the reason for refusal with exit code 1:

text
student@ubuntu:~$ echo 123 | pwscore
Password quality check failed:
 The password is shorter than 8 characters
student@ubuntu:~$ echo qwerty123 | pwscore
Password quality check failed:
 The password fails the dictionary check - it is based on a
 dictionary word
student@ubuntu:~$ echo 'Lab-Pass-2026' | pwscore
96

With minlen = 12, minclass = 3 and dcredit = -1 the refusals are:

Password Answer
labpassword2026 The password contains less than 3 character classes
Kd8#vq2Lm9 The password is shorter than 12 characters
Labpassword-xx The password contains less than 1 digits
aaaaaaaaaaaaB1 … it does not contain enough DIFFERENT characters
Kd8#vq2Lm9Zx 36 — accepted

passwd run by the user prints the same reasons after BAD PASSWORD: and gives up after three tries:

text
deploy@ubuntu:~$ passwd
Changing password for deploy.
Current password:
New password:
BAD PASSWORD: The password contains less than 3 character classes
New password:
BAD PASSWORD: The password is shorter than 12 characters
New password:
BAD PASSWORD: The password contains less than 1 digits
passwd: Have exhausted maximum number of retries for service
passwd: password unchanged

Root without enforce_for_root gets the same BAD PASSWORD, but the password changes: sudo passwd deploy prints the warning and password updated successfully, chpasswd prints only the warning and exits with 0. With enforce_for_root root is refused too — chpasswd exits with 1 and password not changed.

faillock: lockout after failures

pam_faillock counts failed logins. pam-configs has no profile for it — it is written into common-auth by hand, three lines around pam_unix:

text
auth	requisite			pam_faillock.so preauth
auth	[success=1 default=ignore]	pam_unix.so nullok
auth	[default=die]			pam_faillock.so authfail
auth	sufficient			pam_faillock.so authsucc
auth	requisite			pam_deny.so
auth	required			pam_permit.so
auth	optional			pam_cap.so

plus the line account required pam_faillock.so at the end of common-account.

Line What it does
preauth before the password: the account is locked — refuse at once
authfail after a wrong password: record it and refuse
authsucc after the right one: reset the counter, allow the login
account after a login without a password, by key: resets the counter too

The right password jumps over authfail (success=1) and lands on authsucc. There is a variant without authsucc: pam_unix has success=2, and the right password jumps over both authfail and pam_deny. Then only the account line resets the counter, so without it the counter keeps growing even after successful logins. Settings live in /etc/security/faillock.conf; on the machine everything there is commented out: deny = 3 failures, unlock_time = 600 seconds, fail_interval = 900 — the window in which failures are counted. Root is not locked until even_deny_root is on.

Three wrong su - deploy in a row, then the right password:

text
student@ubuntu:~$ su - deploy
Password:
su: Permission denied
student@ubuntu:~$ sudo faillock --user deploy
deploy:
When                Type  Source          Valid
2026-10-21 05:06:19 TTY   /dev/pts/0          V
2026-10-21 05:06:24 TTY   /dev/pts/0          V
2026-10-21 05:06:28 TTY   /dev/pts/0          V
student@ubuntu:~$ su - deploy
The account is locked due to 3 failed logins.
(10 minutes left to unlock)
su: Authentication failure

The password is not even asked for — preauth refused before it. The log says:

text
pam_faillock(su-l:auth): Consecutive login failures for user deploy
  account temporarily locked

The counters are files in /var/run/faillock/<user>. /run lives in memory, so a reboot wipes them. sudo faillock --user deploy --reset lifts the lockout earlier.

Key login through sshd lets the locked account in: the auth stack does not run, and the account line resets the counter.

Recipes

3

Password policy

bash
sudo pam-auth-update --enable pwquality
grep pwquality /etc/pam.d/common-password
sudo mkdir -p /etc/security/pwquality.conf.d
printf 'minlen = 12\nminclass = 3\n' \
  | sudo tee /etc/security/pwquality.conf.d/50-policy.conf
echo 'labpassword2026' | pwscore      # refused: 2 classes of 3

Test it by action: a password change by the user themselves (su - alice -c passwd). A change by root without enforce_for_root goes through with any password and proves nothing.

Lockout after three failures

bash
sudo cp /etc/pam.d/common-auth /root/common-auth.orig
sudo tee /etc/pam.d/common-auth <<'EOF'
auth  requisite                   pam_faillock.so preauth
auth  [success=1 default=ignore]  pam_unix.so nullok
auth  [default=die]               pam_faillock.so authfail
auth  sufficient                  pam_faillock.so authsucc
auth  requisite                   pam_deny.so
auth  required                    pam_permit.so
auth  optional                    pam_cap.so
EOF
echo 'account required pam_faillock.so' \
  | sudo tee -a /etc/pam.d/common-account
printf 'deny = 3\nunlock_time = 600\n' \
  | sudo tee -a /etc/security/faillock.conf

The file is replaced whole — compare its old stack with this one (grep -v '^#' /etc/pam.d/common-auth | grep .) and carry over any lines missing here. PAM reads spaces the same as tabs. Test: three wrong su - alice, then sudo faillock --user alice and a login with the right password — it must be refused. Afterwards run sudo faillock --user alice --reset.

Edit PAM without locking yourself out

  1. Open a second terminal with sudo -i and keep it until the check is done.
  2. Keep a copy of the file: sudo cp /etc/pam.d/common-auth /root/.
  3. Test with a new login: su - alice, ssh alice@localhost.
  4. If it fails, restore the copy from the open root session.

Troubleshooting

5

Password 123 is accepted

grep pwquality /etc/pam.d/common-password prints nothing — the module is not in the stack: sudo pam-auth-update --enable pwquality. The module is there, but root sets the password — without enforce_for_root root gets only the BAD PASSWORD warning, and the password changes.

BAD PASSWORD on a password change

The reason is the text after the colon: shorter than 12 characters — minlen, less than 3 character classes — minclass, less than 1 digits — dcredit, dictionary check — dictcheck. pwscore gives the same answer and is handy for picking a password.

The account is locked due to N failed logins

pam_faillock counted deny failures within fail_interval. Who failed and from where — sudo faillock --user <name> and journalctl -t su, journalctl -u ssh. The lockout lifts by itself after unlock_time or at once with faillock --user <name> --reset.

Many failures, no lockout

  • The pam_faillock lines are missing from common-auth or sit in the wrong place: authfail must come right after pam_unix, before pam_deny.
  • The failing user is root, and even_deny_root is off.
  • The logins are by key: the password auth stack does not run at all.

pam-auth-update: Local modifications

The common-* files were edited by hand, and pam-auth-update refuses to rebuild them — the profile does not get enabled. --force rebuilds the files from the profiles and wipes the manual lines, such as pam_faillock: add them again after --force.

Danger zone

2

Editing common-auth with no way back in

A typo in a required or requisite line closes password login for everyone: su, login, sshd with a password and sudo with a password all go through common-auth. Keep a root session open until the new login is tested.

even_deny_root

With it root gets locked too: a few wrong passwords, and nobody can log in to the server until unlock_time passes. Turn it on only if there is another way in — a console or key login.

Terms

2
pwquality (password quality check)
The PAM module pam_pwquality: on a password change it checks the new one against the rules in /etc/security/pwquality.conf (length minlen, character classes minclass, dictionary) and answers "BAD PASSWORD". pwscore checks a password in advance.
faillock (lockout after failed logins)
The PAM module pam_faillock: it counts a user's failed logins and after deny failures in a row refuses even the right password until unlock_time passes. faillock --user shows the counter, faillock --reset clears it.

See also

  • passwd — changing passwords and /etc/shadow.
  • chage — password expiry.
  • sudo — goes through PAM too.
  • sshd — UsePAM, password and key login.
  • fail2ban — banning an address after failed logins.
  • users-groups — accounts.
  • journalctl — su and sshd refusals in the log.
Loading…