PAM and Account PolicyLesson 2 of 3
Lockout, password quality and password aging
Configure faillock, pwquality and password aging, read the state of an account and unlock it correctly.
faillockfaillock --userfaillock --reset/etc/security/faillock.conf/etc/security/pwquality.confpwscorepwmakechagechage -lpasswd -Susermod -L/etc/login.defsuseradd -D
After this lesson you can
- Set a lockout policy in faillock.conf, trigger it, read the failure record and reset it.
- Set password quality rules in pwquality.conf and test passwords with pwscore.
- Apply aging and expiry to new and existing accounts and tell apart a locked password, an expired password and an expired account.
The idea
Three controls shape how an account may be used, and each has its own file and its own place in the PAM stack:
| Control | Question it answers | Configured in | Enforced by |
|---|---|---|---|
| Lockout | Too many wrong passwords? | /etc/security/faillock.conf | pam_faillock (auth and account) |
| Quality | Is this new password strong enough? | /etc/security/pwquality.conf | pam_pwquality (password) |
| Aging and expiry | Is it time to change the password? Is the account still valid? | /etc/shadow fields (set with chage), defaults in /etc/login.defs and /etc/default/useradd | pam_unix (account) |
They are different checks, so a locked-out user, an expired password and an expired account give different symptoms and need different fixes.
Lockout with pam_faillock
pam_faillock counts failed authentications per user, stored under /var/run/faillock/, and refuses logins once the count passes a limit. RHEL needs it enabled through authselect (with-faillock), then you tune it in /etc/security/faillock.conf:
deny = 3
fail_interval = 900
unlock_time = 600
even_deny_root
root_unlock_time = 60
| Option | Meaning |
|---|---|
deny | Lock after this many consecutive failures |
fail_interval | Failures count only within this window (seconds) |
unlock_time | Seconds until the lock clears by itself (0 = never; admin must reset) |
even_deny_root | Apply the policy to root as well (default: root is exempt) |
root_unlock_time | Unlock time for root, if different |
audit, silent | Log or hide the “account locked” message |
dir | Where the records are kept |
sudo faillock --user labuser1 # list failures
sudo faillock --user labuser1 --reset # clear them
sudo faillock # everyone
labuser1:
When Type Source Valid
2026-11-04 12:51:03 TTY /dev/pts/2 V
2026-11-04 12:51:07 TTY /dev/pts/2 V
2026-11-04 12:51:11 TTY /dev/pts/2 V
(Representative output.) Three valid (V) failures with deny = 3 means the user is locked right now. Entries flip to I (invalid) once they age out of the interval.
Password quality with pam_pwquality
When a user changes a password with passwd, pam_pwquality rates it. Settings in /etc/security/pwquality.conf:
| Option | Meaning |
|---|---|
minlen | Minimum length (a credit system counts character classes toward this, so set it to the real minimum you want) |
minclass | Minimum number of character classes (lower, upper, digit, other) |
dcredit, ucredit, lcredit, ocredit | Positive = credit, negative (-1) = require at least that many of that class |
maxrepeat, maxclassrepeat | Limit identical characters or class runs in a row |
dictcheck, usercheck | Reject dictionary words or the user name |
retry | How many times the user may try in one passwd run |
enforce_for_root | Apply the rules when root sets a password (root may otherwise override with a warning) |
Test the rules without changing a password:
echo 'password1' | pwscore
echo 'Tr0ub4dor&3x-Quartz' | pwscore
pwmake 128 # generate a random password with 128 bits of entropy
Password quality check failed:
The password fails the dictionary check - it is based on a dictionary word
69
(Representative output.) pwscore prints the reason for a rejection or a score from 0 to 100. Quality is checked on change; it does not examine passwords already set. And chpasswd/useradd -p set the hash directly: whether they pass through the quality check depends on the PAM configuration, so test the real passwd path.
Aging and expiry
An account has two independent time limits stored in /etc/shadow:
- Password aging: min days between changes, max days before a change is forced, warning period, and an inactivity grace period after expiry before the account locks.
- Account expiry: a date after which the account cannot be used at all.
sudo chage -l labuser1
sudo chage -M 90 -m 7 -W 14 -I 5 labuser1 # max, min, warn, inactive days
sudo chage -E 2027-06-30 labuser1 # account expires on this date (-1 removes it)
sudo chage -d 0 labuser1 # force a change at the next login
Real output from a test account, before and after chage -M 90 -m 7 -W 14 -I 5:
Last password change : Nov 04, 2026
Password expires : never
Password inactive : never
Account expires : never
Minimum number of days between password change : 0
Maximum number of days between password change : 99999
Number of days of warning before password expires : 7
---
Last password change : Nov 04, 2026
Password expires : Feb 02, 2027
Password inactive : Feb 07, 2027
Account expires : never
Minimum number of days between password change : 7
Maximum number of days between password change : 90
Number of days of warning before password expires : 14
The expiry dates are computed: 90 days after the last change gives Feb 02, 2027, and 5 inactive days after that Feb 07, 2027. Between those two dates the user can still log in but must change the password; after the second date the account is locked.
Where defaults for new accounts come from:
| Setting | File | Affects |
|---|---|---|
PASS_MAX_DAYS, PASS_MIN_DAYS, PASS_WARN_AGE | /etc/login.defs | Accounts created afterwards (and changes through passwd) |
INACTIVE (inactivity days) | /etc/default/useradd (set with useradd -D -f DAYS) | New accounts |
UMASK, UID/GID ranges | /etc/login.defs | New accounts |
| Anything on an existing account | chage -l USER / /etc/shadow | Only that account |
Real output of the defaults on the test machine (the values shown are the unmodified defaults):
PASS_MAX_DAYS 99999
PASS_MIN_DAYS 0
PASS_WARN_AGE 7
Locked, expired or disabled?
Real output of a test account through these states:
passwd -S labuser1 # usable password
usermod -L labuser1; passwd -S labuser1
usermod -U labuser1
chage -E 2020-01-01 labuser1; chage -l labuser1 | sed -n 4p; passwd -S labuser1
labuser1 P 2026-11-04 7 90 14 5
labuser1 L 2026-11-04 7 90 14 5
Account expires : Jan 01, 2020
labuser1 P 2026-11-04 7 90 14 5
passwd -S fields: name, status (P usable, L locked, NP no password), last change, min, max, warn, inactive. After usermod -L the status is L (the hash in /etc/shadow now starts with !). After setting a past expiry date the status is still P: the password is fine, the account is expired. The user cannot log in, but the reason is in chage -l, not in passwd -S.
| State | Look at | Release |
|---|---|---|
| Password locked | passwd -S → L; ! in shadow | usermod -U (or set a new password) |
| Faillock | faillock --user | faillock --user U --reset |
| Password expired | chage -l password expires in the past | New password, or chage -d / -M |
| Inactive too long | chage -l Password inactive passed | Admin must chage -I -1 and set a new password |
| Account expired | chage -l Account expires | chage -E -1 |
| No shell | getent passwd U shows /sbin/nologin | usermod -s /bin/bash U |
Going further
Why root can set weak passwords. pam_pwquality warns when root sets a weak password but accepts it unless enforce_for_root is set. Test policies as an ordinary user. Beyond the exam: pam_pwhistory stops reuse of recent passwords (remember = 5), pam_faillock records can be audited with ausearch -m USER_AUTH, and for centrally managed accounts (IdM, Active Directory) the equivalent policies live on the server side, not in these files.
Practice
0 of 3 done
Do these on your own system. They get harder. Check each result, then tick it off.
- Warm-up
Read the state of an account
Create a user
labuser1with a password. Show withpasswd -Sandchage -lits state. Lock the account withusermod -L, show the state change, unlock it, and then set an account expiry in the past. After each step say whatpasswd -Sprints in the second field and why.Check your result
- Normal:
labuser1 P .... Afterusermod -L:labuser1 L ...and the hash in/etc/shadowstarts with!. - After unlock:
Pagain. - After
chage -E 2020-01-01:chage -lshowsAccount expires : Jan 01, 2020whilepasswd -Sstill saysP(the password is fine; the account is expired).
Hint
passwd -Sreports the status of the password (P usable, L locked, NP none). The expiry date of the account is a separate field (chage -E), enforced by PAM's account stack, not by the password status.Solution
sudo useradd -m labuser1 echo 'labuser1:Aa1!xxxxxxxx' | sudo chpasswd sudo passwd -S labuser1 sudo usermod -L labuser1; sudo passwd -S labuser1 sudo usermod -U labuser1; sudo passwd -S labuser1 sudo chage -E 2020-01-01 labuser1 sudo chage -l labuser1 | head -4; sudo passwd -S labuser1 - Normal:
- Practice
Lock after three failures
Enable faillock through authselect (
with-faillock). Configure the policy in/etc/security/faillock.conf: lock after 3 failed attempts within 15 minutes, unlock automatically after 10 minutes, and apply the lock to root too, with a shorter unlock time of 60 seconds. Test with a normal user (su - labuser1with the wrong password, or ssh), view the record withfaillock --user labuser1, then reset it. Keep a root shell open while testing.Check your result
grep -E '^(deny|unlock_time|fail_interval|even_deny_root|root_unlock_time)' /etc/security/faillock.confshowsdeny = 3,fail_interval = 900,unlock_time = 600,even_deny_rootandroot_unlock_time = 60.- After three bad passwords, a fourth attempt with the correct password is refused;
faillock --user labuser1lists the failures. sudo faillock --user labuser1 --resetempties the list and the next correct login works.
Hint
Settings in
faillock.confare read bypam_faillock; you do not editsystem-authfor them. Lines arename = value; a flag option such aseven_deny_rootis written alone on a line. The record lives in/var/run/faillock/USER, shown by thefaillockcommand.Solution
sudo authselect enable-feature with-faillock && sudo authselect apply-changes sudo sed -i -E 's/^#?\s*deny\s*=.*/deny = 3/; s/^#?\s*fail_interval\s*=.*/fail_interval = 900/; s/^#?\s*unlock_time\s*=.*/unlock_time = 600/; s/^#?\s*root_unlock_time\s*=.*/root_unlock_time = 60/; s/^#?\s*even_deny_root.*/even_deny_root/' /etc/security/faillock.conf grep -vE '^\s*(#|$)' /etc/security/faillock.conf # test: three wrong passwords, then the right one su - labuser1 # wrong x3 from another user; then try the right one sudo faillock --user labuser1 sudo faillock --user labuser1 --reset - Challenge
A complete password policy
Implement and prove this policy for new accounts and for one existing account (
labuser1): passwords at least 12 characters with at least 3 of the 4 character classes and no more than 2 identical characters in a row; changed at most every 7 days and at least every 90 days; warning 14 days before expiry; account locked 5 days after the password expires; new accounts start with the same aging. Show a test password that fails the quality check (pwscore), one that passes, the aging for a new user and forlabuser1, and the file where each setting lives.Check your result
grep -E '^(minlen|minclass|maxrepeat)' /etc/security/pwquality.confshowsminlen = 12,minclass = 3,maxrepeat = 2.grep -E '^PASS_(MAX|MIN|WARN)' /etc/login.defsshows 90, 7, 14;sudo useradd -DshowsINACTIVE=5.sudo chage -l newuserandsudo chage -l labuser1show Max 90, Min 7, warning 14, andPassword inactiveset.echo 'password1' | pwscorefails;echo 'Tr0ub4dor&3x-Quartz' | pwscoreprints a score.
Hint
/etc/login.defsanduseradd -D -fare the defaults for accounts created later. Existing accounts keep their own values until you change them withchage. Quality rules (minlen,minclass) belong inpwquality.conf, andpam_pwqualityenforces them when a password is changed through PAM (passwd, notchpasswdrun by root in some configurations).Solution
sudo sed -i -E 's/^#?\s*minlen\s*=.*/minlen = 12/; s/^#?\s*minclass\s*=.*/minclass = 3/; s/^#?\s*maxrepeat\s*=.*/maxrepeat = 2/' /etc/security/pwquality.conf sudo sed -i -E 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/; s/^PASS_MIN_DAYS.*/PASS_MIN_DAYS 7/; s/^PASS_WARN_AGE.*/PASS_WARN_AGE 14/' /etc/login.defs sudo useradd -D -f 5 sudo useradd newuser && sudo chage -l newuser sudo chage -M 90 -m 7 -W 14 -I 5 labuser1 && sudo chage -l labuser1 echo 'password1' | pwscore echo 'Tr0ub4dor&3x-Quartz' | pwscore