Skip to content

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.

sudo nano /etc/security/pwquality.conf
minlen = 12
minclass = 3
dcredit = 0
maxrepeat = 3
dictcheck = 1
  • 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 from pam_cracklib's design: 0 here means digits provide no automatic length bonus, effectively requiring minlen to 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 (aaaa1234 fails at maxrepeat = 3);
  • dictcheck — rejects passwords found in a dictionary word list (requires the cracklib-runtime/libpwquality dictionary 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=1 grants a length credit: each digit counts as up to that many extra characters toward minlen, so a slightly shorter password containing digits can still satisfy the length rule;
  • a negative value like dcredit=-1 instead imposes a requirement: at least that many characters of that class must be present, independent of length (dcredit=-1 means "at least one digit is mandatory");
  • 0 disables both — no length bonus and no per-class requirement — which is why the config above leans on minlen and minclass alone.

So to require at least one uppercase letter and one digit outright, the setting is ucredit=-1 dcredit=-1, not a larger minclassminclass 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:

grep pwquality /etc/pam.d/common-password
password  requisite  pam_pwquality.so retry=3

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.

passwd testuser
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:

password  required  pam_pwhistory.so remember=5 use_authtok

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:

grep -E '^PASS_(MAX|MIN|WARN)' /etc/login.defs
PASS_MAX_DAYS   99999
PASS_MIN_DAYS   0
PASS_WARN_AGE   7

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   90
PASS_MIN_DAYS   1
PASS_WARN_AGE   14
  • 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 above 0 specifically to stop someone from immediately cycling through pam_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:

sudo passwd -e testuser
passwd: password expiry information changed.

-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.

sudo passwd -l testuser
sudo usermod -L testuser

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:

sudo nano /etc/login.defs
PASS_MAX_DAYS   90
PASS_MIN_DAYS   1
PASS_WARN_AGE   14

Step 2 — enforce quality and history at the PAM level:

sudo nano /etc/security/pwquality.conf
minlen = 12
minclass = 3
sudo nano /etc/pam.d/common-password
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):

sudo chage -M 90 -m 1 -W 14 existinguser

Verify:

sudo chage -l existinguser
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.defs and assuming it applies to existing accounts. As covered above, it only seeds new account creation; existing accounts need chage run against them explicitly.
  • Setting PASS_MIN_DAYS to 0 alongside pam_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 root bypasses pam_pwquality. An administrator setting a simple temporary password for someone and forgetting to also run passwd -e leaves the account with a weak, non-expiring password.

Exercises

  1. Set minlen = 12 and minclass = 3 in pwquality.conf on a lab system, and confirm both a too-short and a too-simple password are rejected by passwd.
  2. Enable pam_pwhistory with remember=3, change a test account's password three times, and confirm the fourth attempt to reuse the first password is rejected.
  3. Explain why PASS_MIN_DAYS needs to be greater than 0 for password history to be effective, in your own words.
  4. For an account that already exists, write the chage command 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.

References