Password Policies
Users and Groups already covered chage, the tool for reading and setting one account's password expiration. That's the per-account mechanism; it says nothing about whether a new password is actually strong, whether it's one the account used two months ago, or what the organization-wide default should be for every account created from now on. This article covers the system-wide policy layer that sits above chage: enforcing password quality at the moment it's set, remembering password history, and setting sane defaults that apply automatically instead of relying on someone running chage correctly for every new account.
What You Will Learn
By the end of this article you will be able to:
- enforce password complexity rules with
pam_pwquality; - prevent password reuse with
pam_pwhistory; - set organization-wide password aging defaults in
/etc/login.defs; - lock, unlock, and force an immediate password change on an account;
- explain how account lockout after failed logins (
pam_faillock) fits alongside password strength as a related but distinct control.
Where a Password Policy Actually Lives
A password policy on Linux is enforced at three different points, and confusing them is a common source of "I set a policy and it didn't take effect":
| Point | What it controls | Where |
|---|---|---|
| When a password is set | Whether the new password meets complexity requirements | pam_pwquality (password type PAM module) |
| When a password is reused | Whether it matches one of the last N passwords for that account | pam_pwhistory |
| Over time, for an existing password | How long it remains valid before a change is forced | chage per account, /etc/login.defs as the system-wide default |
A change to /etc/login.defs only affects accounts created after the change, unless chage is also run on existing accounts — this is the single most common reason a "new" password policy appears to do nothing for accounts that already existed.
Enforcing Complexity: pam_pwquality
pam_pwquality (the successor to the older pam_cracklib) checks a proposed password against a set of rules at the moment passwd tries to set it — length, character variety, and resistance to simple patterns like a reversed username.
minlen— minimum total length;minclass— how many of the four character classes (upper, lower, digit, symbol) must be present;dcredit— a negative-style credit system left over frompam_cracklib's design:0here means digits provide no automatic length bonus, effectively requiringminlento be met by real character diversity rather than padding with digits;maxrepeat— blocks a password with more than this many identical characters in a row (aaaa1234fails atmaxrepeat = 3);dictcheck— rejects passwords found in a dictionary word list (requires thecracklib-runtime/libpwqualitydictionary to be installed).
The credit system behind dcredit is worth understanding past the 0 case, because it's a frequent point of confusion in interviews. Four settings — dcredit, ucredit, lcredit, ocredit (digits, uppercase, lowercase, and "other"/symbols) — each take a value that is read differently depending on its sign:
- a positive value like
dcredit=1grants a length credit: each digit counts as up to that many extra characters towardminlen, so a slightly shorter password containing digits can still satisfy the length rule; - a negative value like
dcredit=-1instead imposes a requirement: at least that many characters of that class must be present, independent of length (dcredit=-1means "at least one digit is mandatory"); 0disables both — no length bonus and no per-class requirement — which is why the config above leans onminlenandminclassalone.
So to require at least one uppercase letter and one digit outright, the setting is ucredit=-1 dcredit=-1, not a larger minclass — minclass only counts how many classes appear, without letting you mandate a specific one.
pam_pwquality needs to actually be wired into the password stack to take effect:
requisite here means a password that fails the quality check stops the stack immediately, before pam_unix.so (further down the same stack) ever gets a chance to actually store it — the rejected password is never written to /etc/shadow.
New password:
BAD PASSWORD: The password contains less than 3 character classes
passwd: Authentication token manipulation error
root is exempt by design
pam_pwquality's checks apply to the account changing its own password through passwd; root setting another account's password is not subject to the same complexity check, on the reasoning that an administrator resetting a forgotten password needs to be able to do so even with a temporary, simple value, provided the account is then forced to change it at next login (covered below).
Preventing Reuse: pam_pwhistory
Complexity rules alone don't stop someone from cycling between two passwords every time a change is forced. pam_pwhistory remembers a configurable number of previous password hashes per account and rejects a match:
remember=5 rejects any of the last five passwords used by that account; use_authtok tells this module to reuse the password already validated by an earlier line in the stack rather than prompting for it again — necessary when pam_pwquality and pam_pwhistory are stacked together, so the user isn't asked to type the same new password twice.
History is stored as hashes, and only from the point it's enabled
pam_pwhistory cannot retroactively know passwords used before it was turned on — the history starts accumulating from the moment the module is added to the stack. It stores salted hashes, not plaintext, in /etc/security/opasswd, which itself needs the same restrictive permissions as /etc/shadow (600, owned by root) since it holds material an offline cracking attempt could target.
System-Wide Aging Defaults: /etc/login.defs
chage sets aging values per account; /etc/login.defs sets what a newly created account gets by default, before anyone runs chage on it at all:
The Ubuntu default of PASS_MAX_DAYS 99999 effectively means "never expires" — a value worth changing deliberately on any server where password rotation is part of the organization's policy:
PASS_MAX_DAYS— the account's password must be changed within this many days;PASS_MIN_DAYS— how soon after a change the password can be changed again, set above0specifically to stop someone from immediately cycling throughpam_pwhistory's remembered passwords back to their original one;PASS_WARN_AGE— how many days before expiration the user starts seeing a warning at login.
As noted above, this file only affects accounts created after the change. To apply the same values to an account that already exists, chage is still the tool — see Users and Groups for its full syntax.
Forcing an Immediate Change, and Locking an Account
Two related but distinct account states come up constantly in practice:
-e immediately expires the current password, forcing a change at the very next login — the standard move after an administrator sets a temporary password for someone, or after a suspected credential leak where the account itself should still remain usable.
Both commands lock the account by prefixing the password hash in /etc/shadow with !, which makes password authentication fail without deleting the password itself — reversible with passwd -u or usermod -U. This is a password-authentication lock specifically; it does not, by itself, block SSH key-based login to the same account, which is why a fuller "disable this account entirely" procedure also sets the shell to /usr/sbin/nologin or removes the account's authorized keys.
How This Relates to pam_faillock
Password strength and lockout after failed attempts are complementary, not overlapping, controls: a strong, unique password that's never guessed correctly gains nothing from lockout, while an account with a weak but never-yet-compromised password benefits from both a stronger requirement going forward and a lockout that stops a brute-force attempt from ever finding it. Root and sudo covers configuring pam_faillock, the module responsible for the lockout side — the two are normally deployed together, not as alternatives.
Practical Scenario: Rolling Out a New Password Policy
Problem. A server currently has no password expiration and no complexity requirement. The organization wants: 12-character minimum with at least 3 character classes, no reuse of the last 5 passwords, and expiration after 90 days — applied to every account, not just new ones.
Step 1 — set the defaults for future accounts:
Step 2 — enforce quality and history at the PAM level:
password requisite pam_pwquality.so retry=3
password required pam_pwhistory.so remember=5 use_authtok
password [success=1 default=ignore] pam_unix.so obscure use_authtok try_first_pass sha512
Step 3 — apply aging to existing accounts individually (/etc/login.defs alone doesn't touch them):
Verify:
Last password change : Jul 29, 2026
Password expires : Oct 27, 2026
Password inactive : never
Minimum number of days between password change : 1
Maximum number of days between password change : 90
Number of days of warning before password expires : 14
Conclusion. The new policy applies going forward for password changes (quality, history) immediately for every account, since PAM is checked at change time regardless of when the account was created; the aging schedule itself needed a separate, explicit chage pass across existing accounts, since /etc/login.defs only seeds new ones.
Common Mistakes
- Changing
/etc/login.defsand assuming it applies to existing accounts. As covered above, it only seeds new account creation; existing accounts needchagerun against them explicitly. - Setting
PASS_MIN_DAYSto0alongsidepam_pwhistory. Without a minimum age, a user can defeat password history by changing their password five times in a row back to the original one immediately. - Enforcing strict complexity without also enabling
pam_faillock. A strong password policy reduces the chance of a successful guess; it does nothing to slow down the guessing attempts themselves. - Forgetting that
rootbypassespam_pwquality. An administrator setting a simple temporary password for someone and forgetting to also runpasswd -eleaves the account with a weak, non-expiring password.
Exercises
- Set
minlen = 12andminclass = 3inpwquality.confon a lab system, and confirm both a too-short and a too-simple password are rejected bypasswd. - Enable
pam_pwhistorywithremember=3, change a test account's password three times, and confirm the fourth attempt to reuse the first password is rejected. - Explain why
PASS_MIN_DAYSneeds to be greater than0for password history to be effective, in your own words. - For an account that already exists, write the
chagecommand that applies a 90-day maximum age and a 14-day warning period, matching a policy defined in/etc/login.defs.
Summary
A password policy has three layers: pam_pwquality enforces strength at the moment a password is set, pam_pwhistory blocks reuse, and /etc/login.defs together with per-account chage values control expiration over time. Changing the system-wide defaults doesn't retroactively apply to existing accounts, and password strength and pam_faillock-style lockout are complementary controls that address different attack patterns. The next article turns to mandatory access control — SELinux and AppArmor — which enforces policy independent of what any account's password looks like.