Linux Administration Lesson 12 – Password Policies and Aging | Dataplexa
Section II — User, Process & Package Management

Password Policies and Aging

In this lesson

passwd and chage Password aging fields PAM complexity rules login.defs Account lockout

Password policies and aging are the rules that govern how long passwords remain valid, how complex they must be, and what happens when a user fails to change them on time or enters them incorrectly too many times. On a system with many users, a password that never expires is a permanent security liability — Linux provides a full set of tools to enforce expiry, warn users in advance, and lock accounts that fall out of compliance.

The /etc/shadow Aging Fields

Every password policy on Linux is ultimately enforced through the fields stored in /etc/shadow. Each line contains nine colon-separated fields — the first two are the username and hashed password, and the remaining seven control the entire lifecycle of that password.

alice : $6$salt$hashvalue... : 19800 : 0 : 90 : 7 : 14 : 20000 : (reserved) Field 1 username Field 2 password hash Field 3 last changed Field 4 min days Field 5 max days Field 6 warn days Field 7 inactive days Field 8 account expire Days since epoch (Jan 1 1970). Field 3 value 19800 = day 19800 since epoch. Field 5 = 90 means password expires 90 days after last change. Field 6 = 7 means user is warned 7 days before expiry. Field 7 = 14 means account locked 14 days after password expires. All dates stored as integer days since Unix epoch — not human-readable dates.

Fig 1 — The nine fields of an /etc/shadow entry and what each controls

# View the full shadow entry for a user (root only)
sudo getent shadow alice

# A cleaner way to view just the aging fields
sudo chage -l alice

What just happened? chage -l translated the raw epoch-day integers in /etc/shadow into human-readable dates. Alice's password expires 90 days after her last change, she gets a 7-day warning, and her account will be locked a further 14 days after the password expires if she still has not changed it.

Setting Password Aging with chage

chage (change age) is the primary tool for setting and reading password aging policies on individual accounts. It writes directly to /etc/shadow and accepts both flag-based and interactive modes.

chage — Flag Reference
Flag Purpose Example
-l List current aging settings (human-readable) chage -l alice
-M Maximum days before password must change chage -M 90 alice
-m Minimum days before a password can be changed again chage -m 1 alice
-W Days before expiry to start warning the user chage -W 14 alice
-I Days after expiry before account is locked (inactive period) chage -I 7 alice
-E Set an absolute account expiry date chage -E 2025-12-31 alice
-d 0 Force password change on next login chage -d 0 alice
# Apply a full aging policy to an account in one command
sudo chage -M 90 -m 1 -W 14 -I 7 alice

# Force a user to change their password at next login (common after account creation)
sudo chage -d 0 alice

# Set an account to expire on a specific date (useful for contractors)
sudo chage -E 2025-12-31 contractor1

# Remove an account expiry date (set to -1 or "never")
sudo chage -E -1 alice

# Use interactive mode — chage prompts for each field
sudo chage alice

# Apply the same aging policy to all users in a loop
for user in alice bob charlie; do
  sudo chage -M 90 -m 1 -W 14 -I 7 "$user"
  echo "Policy applied to $user"
done

Analogy: Password aging is like a gym membership card with an expiry date printed on it. The card still opens the door right up until expiry day — but for the two weeks before that date, the front desk reminds you every time you swipe. After it expires, you have a short grace period to renew, then access is cut off entirely.

System-Wide Defaults with /etc/login.defs

Rather than setting aging policies user by user, /etc/login.defs sets the defaults that apply to every new account created with useradd going forward. Existing accounts are not affected — only accounts created after the change inherit these defaults.

# /etc/login.defs — relevant password aging settings

# Maximum number of days a password is valid
PASS_MAX_DAYS   90

# Minimum number of days between password changes
PASS_MIN_DAYS   1

# Number of days warning given before a password expires
PASS_WARN_AGE   14

# Minimum acceptable password length
PASS_MIN_LEN    12

# UID and GID ranges for regular users
UID_MIN         1000
UID_MAX         60000
GID_MIN         1000
GID_MAX         60000

# Whether to create a home directory by default with useradd
CREATE_HOME     yes
# View the current login.defs password settings
grep -E "^PASS" /etc/login.defs

# Edit login.defs (always back up first)
sudo cp /etc/login.defs /etc/login.defs.bak
sudo nano /etc/login.defs

# Verify a specific setting after editing
grep "PASS_MAX_DAYS" /etc/login.defs

What just happened? The default PASS_MAX_DAYS of 99999 means passwords effectively never expire on a fresh Linux install — this is intentional for flexibility, but should be changed to enforce a real policy (typically 90 days) before provisioning user accounts on any production system.

Enforcing Password Complexity with PAM

PAM (Pluggable Authentication Modules) is the framework Linux uses to intercept and validate authentication events — including password changes. The pam_pwquality module (formerly pam_cracklib) enforces complexity rules at the point of password creation or change, before the hash is ever written to /etc/shadow.

Length controls
  • minlen — minimum character count
  • maxclassrepeat — max repeated char class
  • maxrepeat — max identical consecutive chars
Character classes
  • ucredit — uppercase letters
  • lcredit — lowercase letters
  • dcredit — digits
  • ocredit — special characters
History & reuse
  • remember — previous passwords to reject
  • difok — chars different from old password
  • reject_username — block username in password
# Install pwquality (Debian/Ubuntu)
sudo apt install libpam-pwquality -y

# Install pwquality (RHEL/Rocky)
sudo dnf install libpwquality -y
# /etc/security/pwquality.conf — password complexity rules

# Minimum password length
minlen = 12

# Require at least 1 uppercase letter (negative value = minimum required)
ucredit = -1

# Require at least 1 lowercase letter
lcredit = -1

# Require at least 1 digit
dcredit = -1

# Require at least 1 special character
ocredit = -1

# Reject if password contains the username
reject_username = 1

# Number of characters that must differ from the previous password
difok = 4

# Maximum number of identical consecutive characters allowed
maxrepeat = 3
# Test a password against the current pwquality rules without changing anything
pwscore <<< "mypassword123"

# View the PAM password stack for the passwd command
cat /etc/pam.d/passwd

# Check that pam_pwquality is loaded in the common-password PAM file (Debian/Ubuntu)
grep pwquality /etc/pam.d/common-password

What just happened? pwscore ran the candidate password through the same checks PAM would apply on a real password change. The first attempt failed the dictionary check — pam_pwquality includes a dictionary of common password bases and rejects them even with number substitutions. The second attempt scored 92 out of 100, indicating a strong password that would pass all rules.

Account Lockout with faillock and pam_tally2

Complexity rules and aging prevent weak and stale passwords, but they do not stop an attacker from simply trying thousands of passwords against an account. Account lockout limits failed login attempts, locking an account after a configurable threshold is reached.

faillock — modern (RHEL 8+, current)

Replaces pam_tally2 on modern systems. Stores failure records per-user in /var/run/faillock/. Configured via /etc/security/faillock.conf.

# Check failures for a user
faillock --user alice

# Unlock an account
faillock --user alice --reset

pam_tally2 — legacy (older Debian/Ubuntu)

Found on older Debian and Ubuntu systems. Stores a global tally file. Deprecated in favour of faillock on most current distributions.

# Check failure count
pam_tally2 --user alice

# Reset counter
pam_tally2 --user alice --reset
# /etc/security/faillock.conf — account lockout policy

# Number of failed attempts before the account is locked
deny = 5

# Time window (seconds) in which failures are counted
fail_interval = 900

# How long (seconds) the account stays locked (0 = until admin unlocks)
unlock_time = 600

# Also lock the root account (use with caution on servers you can reach physically)
# even_deny_root

# Audit log all authentication failures
audit
# Check how many failed login attempts alice has
sudo faillock --user alice

# Unlock alice's account after a lockout
sudo faillock --user alice --reset

# View failed login attempts in the auth log (Debian/Ubuntu)
sudo grep "Failed password" /var/log/auth.log | tail -20

# View failed login attempts in the secure log (RHEL/Rocky)
sudo grep "Failed password" /var/log/secure | tail -20

# View who is currently locked out system-wide
sudo faillock

What just happened? The faillock output showed five failed attempts from the same IP address within seconds — a clear sign of a brute-force attempt rather than a forgotten password. After --reset, the counter is cleared and the account can accept logins again. The source IP should be investigated and potentially blocked at the firewall.

Auditing Password Policy Compliance

Setting policies is only half the job. Regularly auditing which accounts comply — and which do not — is what makes a policy real. The following commands give you a clear picture of the password health across your entire system.

# List all regular users and their password expiry status
sudo awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd | \
  while read user; do
    echo "=== $user ==="
    sudo chage -l "$user" | grep -E "expires|Password"
  done

# Find accounts with passwords that never expire (MAX_DAYS = 99999 or -1)
sudo awk -F: '($5 == 99999 || $5 == -1) && $3 >= 1000 {print $1}' /etc/shadow

# Find accounts with no password set at all (empty hash field)
sudo awk -F: '($2 == "" || $2 == "!!" ) && $3 >= 1000 {print $1}' /etc/shadow

# Find accounts with expired passwords
sudo awk -F: '$3 >= 1000 {print $1}' /etc/passwd | \
  while read user; do
    expiry=$(sudo chage -l "$user" | grep "Password expires" | cut -d: -f2)
    echo "$user: $expiry"
  done

What just happened? The audit surfaced three categories of concern: accounts whose passwords never expire (policy violation), accounts with no password at all (immediate security risk — these should be locked or have a password set before any service uses them), and accounts with already-expired passwords that may indicate abandoned or stale accounts that should be reviewed.

Setting PASS_MAX_DAYS in login.defs Does Not Apply to Existing Accounts

A very common misunderstanding: editing /etc/login.defs only affects accounts created after the change. Every existing account retains its current shadow settings unchanged. To enforce a new policy across all existing accounts, you must run chage against each one individually — or use a loop as shown in the code block above.

Lesson Checklist

I can read the nine fields of an /etc/shadow entry and explain what each aging field controls
I can use chage to set maximum age, warning period, inactive period, and forced change on next login for any user account
I understand that /etc/login.defs sets defaults for new accounts only, and existing accounts require individual chage commands
I can configure pam_pwquality to enforce minimum length, character classes, and password history rules
I can use faillock to review failed login attempts, identify lockouts, and reset accounts — and I know how to configure the lockout threshold and duration

Teacher's Note

Modern security guidance (NIST SP 800-63B) recommends against mandatory periodic password rotation for most accounts — forced regular changes tend to produce weaker passwords as users rotate through predictable patterns. In practice, combine a longer maximum age (180–365 days) with strong complexity rules, password history, and immediate forced change on any suspected compromise.

Practice Questions

1. A new contractor named contractor1 has just been created. Write the commands to: force a password change on first login, set a maximum password age of 60 days, a 7-day warning period, an inactive period of 5 days, and an account expiry date of 2025-06-30.

2. You update PASS_MAX_DAYS in /etc/login.defs from 99999 to 90. A colleague says this means all existing user passwords will now expire in 90 days. Are they correct? Explain why or why not, and describe what you would need to do to enforce the new policy on existing accounts.

3. The security team has asked you to identify all accounts on a server where passwords never expire, and then apply a 90-day maximum age to each of them without affecting other aging settings. Write a shell command or short script that accomplishes this.

Lesson Quiz

1. What does running sudo chage -d 0 alice do to alice's account?

2. In /etc/security/pwquality.conf, what does setting ucredit = -1 enforce?

3. An account has the following /etc/shadow aging values: MAX=90, WARN=7, INACTIVE=14. The password was last changed 91 days ago and has not been updated. What is the current state of the account?

Up Next

Lesson 13 — Package Management

Installing, updating, and removing software with apt, dnf, and rpm across major Linux distributions