Linux Administration
Password Policies and Aging
In this lesson
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.
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# sudo chage -l alice Last password change : Mar 01, 2025 Password expires : May 30, 2025 Password inactive : Jun 13, 2025 Account expires : never Minimum number of days between password change : 0 Maximum number of days between password change : 90 Number of days of warning before password expires : 7
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.
| 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"
doneAnalogy: 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# grep -E "^PASS" /etc/login.defs PASS_MAX_DAYS 99999 PASS_MIN_DAYS 0 PASS_WARN_AGE 7 PASS_MIN_LEN 8
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.
minlen— minimum character countmaxclassrepeat— max repeated char classmaxrepeat— max identical consecutive chars
ucredit— uppercase letterslcredit— lowercase lettersdcredit— digitsocredit— special characters
remember— previous passwords to rejectdifok— chars different from old passwordreject_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# pwscore <<< "mypassword123" Password quality check failed: The password fails the dictionary check - it is based on a dictionary word # pwscore <<< "T!m3$ecure#9xK" 92 # grep pwquality /etc/pam.d/common-password password requisite pam_pwquality.so retry=3
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# sudo faillock --user alice alice: When Type Source Valid 2025-03-12 14:32:11 RHOST 192.168.1.45 V 2025-03-12 14:32:19 RHOST 192.168.1.45 V 2025-03-12 14:32:27 RHOST 192.168.1.45 V 2025-03-12 14:32:35 RHOST 192.168.1.45 V 2025-03-12 14:32:41 RHOST 192.168.1.45 V # Account is now locked — 5 failures within 900 second window # sudo faillock --user alice --reset # (no output — reset successful) # sudo faillock --user alice alice: When Type Source Valid # (empty — counter cleared)
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# Accounts with non-expiring passwords svcaccount olduser # Accounts with no password set newuser1 tempaccount # Password expiry audit alice: May 30, 2025 bob: never charlie: Feb 01, 2025 ← already expired
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
/etc/shadow entry and explain what each aging field controls
chage to set maximum age, warning period, inactive period, and forced change on next login for any user account
/etc/login.defs sets defaults for new accounts only, and existing accounts require individual chage commands
pam_pwquality to enforce minimum length, character classes, and password history rules
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.
sudo chage -d 0 contractor1 (force change on first login) → sudo chage -M 60 -W 7 -I 5 -E 2025-06-30 contractor1 — or combine all flags in one command: sudo chage -d 0 -M 60 -W 7 -I 5 -E 2025-06-30 contractor1. Verify with sudo chage -l contractor1.
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.
/etc/login.defs only affect accounts created after the change — existing accounts retain their current shadow settings. To enforce the policy on existing accounts you must run chage on each one individually, e.g. using a loop: for user in $(awk -F: '$3 >= 1000 {print $1}' /etc/passwd); do sudo chage -M 90 "$user"; done.
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.
sudo awk -F: '($5 == 99999 || $5 == -1) && $3 >= 1000 {print $1}' /etc/shadow. Then apply the policy: sudo awk -F: '($5 == 99999 || $5 == -1) && $3 >= 1000 {print $1}' /etc/shadow | while read user; do sudo chage -M 90 "$user"; echo "Updated $user"; done.
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