Password policy
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.
The module stack in /etc/pam.d decides whether to let a user in, which password to accept and when to lock them out.
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
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:
sudo and sshd are built the same way: editing common-auth changes
the password check for all of them at once.
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.
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:
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.
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:
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:
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:
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:
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.
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:
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:
The password is not even asked for — preauth refused before it. The
log says:
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.
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.
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.
sudo -i and keep it until the check is done.sudo cp /etc/pam.d/common-auth /root/.su - alice, ssh alice@localhost.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.
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.
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.
pam_faillock lines are missing from common-auth or sit in the
wrong place: authfail must come right after pam_unix, before
pam_deny.even_deny_root is off.auth stack does not run at all.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.
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.
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.
/etc/shadow.UsePAM, password and key login.su and sshd refusals in the log.Found an inaccuracy? Report an error in the article