I still remember the first time a security audit flagged one of my servers because half the user accounts had passwords that had never expired in over three years. That was my real introduction to chage, and once I understood it properly, password policy stopped being a mystery and became just another part of my regular admin routine. In this guide I want to walk you through everything I know about chage, from the absolute basics to the internals of how Linux tracks password aging, so you can manage account security with confidence.
What chage Actually Does
chage stands for “change age,” and it’s the tool Linux gives you to view and modify password aging information for user accounts. Every local user account on a Linux system has aging data stored in /etc/shadow, and chage is essentially a friendly front-end for reading and writing those fields without you having to hand-edit that sensitive file.
Password aging isn’t just a bureaucratic checkbox. It’s a real security control. If an account’s credentials are compromised, forcing periodic password changes limits how long an attacker can use stolen credentials. It also gives you a way to automatically disable accounts that should no longer be active, which matters a lot for contractors, temporary staff, or service accounts.
Where the Data Lives: /etc/shadow Internals
To really understand chage, you need to understand /etc/shadow. Each line in that file corresponds to one user and has nine colon-separated fields:
username:password_hash:lastchange:min:max:warn:inactive:expire:reserved
Here’s what each field means:
- username — the login name
- password_hash — the encrypted password (or
!,*for locked/disabled) - lastchange — days since January 1, 1970 (Unix epoch) that the password was last changed
- min — minimum number of days required between password changes
- max — maximum number of days the password is valid before it must be changed
- warn — number of days before expiration that the user is warned
- inactive — number of days after password expiry that the account is disabled
- expire — the actual date the account will expire, also stored as days since epoch
- reserved — unused, reserved for future use
When you run chage, you’re really just reading and writing these fields in a controlled way, with input validation, instead of letting you (or a script) corrupt the shadow file directly. Because /etc/shadow is only readable by root, chage runs with elevated privileges when it needs to inspect or modify another user’s aging data.
Basic Syntax
chage [options] LOGIN
If you run chage with no options as root, it drops you into an interactive mode that prompts you for each value one at a time. I rarely use that mode in practice because it’s slow for scripting, but it’s worth knowing it exists, especially for one-off manual changes when you don’t want to remember flag names.
Viewing Account Aging Information
The most common use of chage for me, day to day, is simply checking the current state of an account:
chage -l username
On my test system, running this against root gave me:
Last password change : Apr 10, 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
That output tells me a lot in one glance: this account has no forced expiration, no minimum days between changes, and a default warning period of 7 days baked in from /etc/login.defs. The -l (or --list) flag requires read access to the shadow file, so unprivileged users can only run chage -l on their own account, while root can inspect anyone’s.
You can also get ISO 8601 formatted dates, which I find much easier to parse in scripts:
chage -i -l username
Full List of Parameters
Here’s the complete option set, straight from chage --help:
-d, --lastday LAST_DAY set date of last password change to LAST_DAY
-E, --expiredate EXPIRE_DATE set account expiration date to EXPIRE_DATE
-h, --help display this help message and exit
-i, --iso8601 use YYYY-MM-DD when printing dates
-I, --inactive INACTIVE set password inactive after expiration to INACTIVE
-l, --list show account aging information
-m, --mindays MIN_DAYS set minimum number of days before password change to MIN_DAYS
-M, --maxdays MAX_DAYS set maximum number of days before password change to MAX_DAYS
-R, --root CHROOT_DIR directory to chroot into
-W, --warndays WARN_DAYS set expiration warning days to WARN_DAYS
Let me go through the practically important ones with real examples.
Setting Maximum Password Age
This forces users to change their password after a set number of days:
sudo chage -M 90 username
I use 90 days as a common baseline for general staff accounts, though some compliance frameworks want 60 or even 30 for privileged accounts.
Setting Minimum Password Age
This prevents users from changing their password and then immediately changing it back to the old one, which is a classic workaround people use to dodge password history checks:
sudo chage -m 7 username
Setting the Warning Period
sudo chage -W 14 username
This tells the login process to start nagging the user 14 days before their password expires, giving them time to plan a change instead of getting locked out mid-workday.
Setting Inactivity Period
This is one of the more underused but powerful options. It defines how many days after a password expires that the account itself gets disabled if the user never bothered to change it:
sudo chage -I 30 username
Setting an Absolute Account Expiration Date
This is different from password expiry — this disables the account entirely on a specific calendar date, regardless of password state:
sudo chage -E 2027-12-31 username
I tested this combination on a scratch account and confirmed the fields update correctly:
sudo chage -M 90 -m 7 -W 14 testuser1
sudo chage -l testuser1
Output:
Last password change : Jul 31, 2026
Password expires : Oct 29, 2026
Password inactive : never
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
You can disable expiration entirely (make it never expire) by passing -1:
sudo chage -M -1 username
Setting Last Password Change Date Manually
Occasionally you need to backdate or reset the “last changed” timestamp, for example when migrating accounts from another system where the aging clock should start fresh:
sudo chage -d 0 username
Setting -d 0 is a well-known trick to force a password change on next login, since it makes the system think the password was last changed on the epoch itself, which combined with a max age immediately triggers expiration.
Combining Multiple Options
You don’t need separate commands for each field. I usually set everything in one shot:
sudo chage -m 7 -M 90 -W 14 -I 30 -E 2027-12-31 username
Comparison with Related Tools
chage isn’t the only way to touch these fields, and it helps to know how it relates to its siblings:
usermod -e/usermod -fcan set account expiration and inactivity, overlapping withchage -Eandchage -I, butusermodis meant for broader account attribute changes (shell, home directory, groups), whilechageis laser-focused on aging.passwd -x,passwd -n,passwd -w,passwd -ioffer a subset of the same aging controls through thepasswdcommand, but I findchageclearer and more explicit for scripting since the flag names map directly to shadow fields.- Direct editing with
vipw -slets you edit/etc/shadowby hand, which I avoid unless I’m troubleshooting a corrupted entry, because it bypasses all ofchage‘s validation.
For day-to-day administration, I default to chage for anything aging-related and usermod for anything else about the account.
System Defaults in /etc/login.defs
When you create a new user with useradd, the default aging values come from /etc/login.defs:
PASS_MAX_DAYS 99999
PASS_MIN_DAYS 0
PASS_WARN_AGE 7
If you want every new account to inherit a stricter policy automatically, edit these defaults before creating accounts, rather than running chage on every user after the fact. This is the setting I always change first on a fresh server build, right after locking down SSH.
Real-World Automation: Auditing and Enforcing Policy
Here’s a script I actually use to find accounts that don’t comply with a 90-day max password age policy:
#!/bin/bash
# audit_password_aging.sh
# Lists local users whose password never expires or exceeds policy
MAX_ALLOWED=90
for user in $(awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' /etc/passwd); do
maxdays=$(sudo chage -l "$user" | awk -F': ' '/Maximum number/{print $2}')
if [[ "$maxdays" == "99999" ]] || [[ "$maxdays" -gt "$MAX_ALLOWED" ]]; then
echo "Non-compliant: $user (max days: $maxdays)"
fi
done
And here’s a one-liner to bulk-apply a policy to every interactive user account:
for user in $(awk -F: '$3 >= 1000 {print $1}' /etc/passwd); do
sudo chage -M 90 -m 7 -W 14 "$user"
done
I run something close to this after onboarding batches of new accounts, right before I hand off credentials.
Troubleshooting Common Issues
“chage: permission denied” — this happens when a non-root user tries to modify another account’s aging, or even view it in some hardened configurations. Only root, or a user with the right sudo privileges, can change another account’s data.
Users getting locked out unexpectedly — check if -I (inactive) is set aggressively alongside a short -M. If a user misses the warning window entirely (maybe they were on vacation), the account can lock before they realize it expired. I’ve learned to pair short max-day policies with generous warning periods.
Password expired but user says they never changed it — verify with chage -l, and cross-check the lastchange field against actual login history in /var/log/auth.log or journalctl -u sshd. Sometimes account provisioning tools set -d 0 intentionally to force a first-login password change, which is expected behavior, not a bug.
Dates look wrong after -d 0 — this is expected. Setting last-change to epoch zero combined with a max-days value under the current day count will show the password as already expired, which is the intended forcing mechanism.
Security Implications
Password aging is a double-edged sword from a security research perspective. NIST’s more modern guidance (SP 800-63B) actually recommends against mandatory periodic password rotation for general users, because forced frequent changes often push people toward predictable patterns (like incrementing a number at the end of a password). Where chage still earns its keep is in:
- Disabling accounts on a known offboarding date via
-E - Enforcing a minimum password age via
-mto stop history-cycling workarounds - Automatically deactivating stale accounts via
-Iwhen no one logs in to change an expired password
I personally treat -M conservatively now, favoring longer max-age windows combined with strong password complexity and multi-factor authentication elsewhere, rather than aggressive 30-day rotations that used to be considered best practice.
Compatibility Across Distributions
chage is part of the shadow-utils (RHEL/Fedora/CentOS) or passwd (Debian/Ubuntu) package, and its behavior is essentially identical across major distributions since it’s tied to the shadow password suite standard. I’ve used the same flags on Ubuntu, Debian, RHEL, CentOS, Rocky Linux, and openSUSE without any surprises. The only variance you might hit is in default values inside /etc/login.defs, which differ by distro and sometimes by version.
Summary
chage gives you precise control over password lifecycle and account expiration by managing the aging fields in /etc/shadow. Once you understand the nine shadow fields it manipulates, the command stops feeling like an obscure flag soup and starts feeling like a natural extension of account management. I use it constantly for compliance audits, onboarding/offboarding automation, and tightening security posture on accounts that don’t need indefinite access. Combine it with sane defaults in /etc/login.defs, and you’ve got a solid, low-maintenance password aging policy running quietly in the background of every server you manage.
References
- GNU/Linux
shadow-utilsdocumentation:man 1 chage man 5 shadowfor the shadow file formatman 5 login.defsfor system-wide defaults- NIST SP 800-63B, Digital Identity Guidelines
- Debian Administrator’s Handbook, chapter on user management
