2 of 3

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.

  • faillock
  • faillock --user
  • faillock --reset
  • /etc/security/faillock.conf
  • /etc/security/pwquality.conf
  • pwscore
  • pwmake
  • chage
  • chage -l
  • passwd -S
  • usermod -L
  • /etc/login.defs
  • useradd -D

55 min3 exercises

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:

ControlQuestion it answersConfigured inEnforced by
LockoutToo many wrong passwords?/etc/security/faillock.confpam_faillock (auth and account)
QualityIs this new password strong enough?/etc/security/pwquality.confpam_pwquality (password)
Aging and expiryIs 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/useraddpam_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
OptionMeaning
denyLock after this many consecutive failures
fail_intervalFailures count only within this window (seconds)
unlock_timeSeconds until the lock clears by itself (0 = never; admin must reset)
even_deny_rootApply the policy to root as well (default: root is exempt)
root_unlock_timeUnlock time for root, if different
audit, silentLog or hide the “account locked” message
dirWhere 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:

OptionMeaning
minlenMinimum length (a credit system counts character classes toward this, so set it to the real minimum you want)
minclassMinimum number of character classes (lower, upper, digit, other)
dcredit, ucredit, lcredit, ocreditPositive = credit, negative (-1) = require at least that many of that class
maxrepeat, maxclassrepeatLimit identical characters or class runs in a row
dictcheck, usercheckReject dictionary words or the user name
retryHow many times the user may try in one passwd run
enforce_for_rootApply 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:

SettingFileAffects
PASS_MAX_DAYS, PASS_MIN_DAYS, PASS_WARN_AGE/etc/login.defsAccounts 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.defsNew accounts
Anything on an existing accountchage -l USER / /etc/shadowOnly 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.

StateLook atRelease
Password lockedpasswd -S → L; ! in shadowusermod -U (or set a new password)
Faillockfaillock --userfaillock --user U --reset
Password expiredchage -l password expires in the pastNew password, or chage -d / -M
Inactive too longchage -l Password inactive passedAdmin must chage -I -1 and set a new password
Account expiredchage -l Account expireschage -E -1
No shellgetent passwd U shows /sbin/nologinusermod -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.

  1. Warm-up

    Read the state of an account

    Create a user labuser1 with a password. Show with passwd -S and chage -l its state. Lock the account with usermod -L, show the state change, unlock it, and then set an account expiry in the past. After each step say what passwd -S prints in the second field and why.

    Check your result
    • Normal: labuser1 P .... After usermod -L: labuser1 L ... and the hash in /etc/shadow starts with !.
    • After unlock: P again.
    • After chage -E 2020-01-01: chage -l shows Account expires : Jan 01, 2020 while passwd -S still says P (the password is fine; the account is expired).
    Hint

    passwd -S reports 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
  2. 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 - labuser1 with the wrong password, or ssh), view the record with faillock --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.conf shows deny = 3, fail_interval = 900, unlock_time = 600, even_deny_root and root_unlock_time = 60.
    • After three bad passwords, a fourth attempt with the correct password is refused; faillock --user labuser1 lists the failures.
    • sudo faillock --user labuser1 --reset empties the list and the next correct login works.
    Hint

    Settings in faillock.conf are read by pam_faillock; you do not edit system-auth for them. Lines are name = value; a flag option such as even_deny_root is written alone on a line. The record lives in /var/run/faillock/USER, shown by the faillock command.

    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
  3. 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 for labuser1, and the file where each setting lives.

    Check your result
    • grep -E '^(minlen|minclass|maxrepeat)' /etc/security/pwquality.conf shows minlen = 12, minclass = 3, maxrepeat = 2.
    • grep -E '^PASS_(MAX|MIN|WARN)' /etc/login.defs shows 90, 7, 14; sudo useradd -D shows INACTIVE=5.
    • sudo chage -l newuser and sudo chage -l labuser1 show Max 90, Min 7, warning 14, and Password inactive set.
    • echo 'password1' | pwscore fails; echo 'Tr0ub4dor&3x-Quartz' | pwscore prints a score.
    Hint

    /etc/login.defs and useradd -D -f are the defaults for accounts created later. Existing accounts keep their own values until you change them with chage. Quality rules (minlen, minclass) belong in pwquality.conf, and pam_pwquality enforces them when a password is changed through PAM (passwd, not chpasswd run 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