Story / scenario opener
You inherited a Fedora server that hasn't had a security audit in three years. The compliance report demands ninety-day password rotation, but the current users are still on passwords set during the initial installation. You need to enforce a new expiration policy immediately, apply it to existing accounts, and make sure new accounts follow the rule automatically.
What's actually happening
Password expiration on Fedora is not a single toggle. It lives in two separate layers. The /etc/login.defs file acts as the blueprint for new accounts. When useradd or adduser creates a profile, it reads the defaults from that file and writes them into the shadow database. Existing accounts ignore the blueprint. They keep their own independent expiration dates stored in /etc/shadow. Changing the blueprint does nothing for accounts that already exist. You have to update the blueprint for the future and run a targeted tool to rewrite the shadow entries for the past.
Think of it like updating a building's fire code. The new code applies to every new floor you construct. The existing floors still follow the old rules until you send an inspector to update their permits. Fedora's user management works the same way. The login.defs file sets the architectural standard. The chage command inspects and rewrites individual permits.
Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. The setup package owns /etc/login.defs, so a dnf upgrade will not overwrite your manual changes unless the package maintainer explicitly ships a new default. You are safe to modify it directly.
Run the blueprint update first. Apply the existing account changes second. Reboot before you debug. Half the time the symptom is gone.
The fix or how-to
Here's how to update the system-wide defaults so every new account inherits the ninety-day rotation rule.
# Back up the original file before modifying system defaults
sudo cp /etc/login.defs /etc/login.defs.bak
# Replace the maximum password age line with a ninety-day limit
sudo sed -i 's/^PASS_MAX_DAYS .*/PASS_MAX_DAYS 90/' /etc/login.defs
# Set a seven-day minimum to prevent users from cycling passwords too quickly
sudo sed -i 's/^PASS_MIN_DAYS .*/PASS_MIN_DAYS 7/' /etc/login.defs
# Configure a fourteen-day warning window so users get advance notice
sudo sed -i 's/^PASS_WARN_AGE .*/PASS_WARN_AGE 14/' /etc/login.defs
The sed commands target only uncommented lines starting with the exact variable name. The ^ anchor prevents accidental overwrites of commented examples. The .* wildcard matches whatever value currently sits there. This approach is safe for repeated runs.
Here's how to apply the same ninety-day rotation rule to an existing user account.
# Apply the ninety-day maximum to the target account
sudo chage -M 90 username
# Enforce the seven-day minimum change interval
sudo chage -m 7 username
# Set the fourteen-day warning period for expiration notices
sudo chage -W 14 username
You can combine those flags into a single command if you prefer fewer keystrokes. The chage utility reads the current shadow entry, applies the new values, and writes the updated line back to /etc/shadow. It does not touch other accounts. It does not modify /etc/login.defs. It only changes the specific user you name.
If you need to force a password change at the next login, add the -d 0 flag. This sets the last password change date to epoch zero. The system treats the password as immediately expired. Users will be prompted to create a new password before they get a shell. Use this sparingly. It locks users out until they comply.
Run chage for each account that needs the policy. Snapshot the system before the upgrade. Future-you will thank you.
Verify it worked
Here's how to confirm the shadow database reflects the new expiration schedule.
# Display the expiration policy for a specific user
sudo chage -l username
# Check the system-wide defaults that new accounts will inherit
grep -E '^PASS_(MAX|MIN|WARN)_AGE' /etc/login.defs
The chage -l output shows four dates. The "Maximum number of days a password may be used" line should read 90. The "Minimum number of days between password change" line should read 7. The "Number of days of warning before password expires" line should read 14. If any value shows 99999, the change did not apply. Run the chage command again and verify the username spelling.
The grep command confirms the blueprint is updated. New accounts created after this point will automatically inherit the ninety-day limit. The PAM stack does not enforce expiration. The shadow database does. PAM only handles authentication and password quality. Keep that separation clear.
Run journalctl -xe first. Read the actual error before guessing.
Common pitfalls and what the error looks like
The sed command will silently skip lines that do not match the pattern. If your /etc/login.defs file contains PASS_MAX_DAYS 90 with a tab instead of spaces, the .* wildcard still matches it. The command is resilient. If you accidentally type PASS_MAXDAYS without the underscore, the line stays unchanged. Check the file with grep after running the command.
Users sometimes expect chage to expire the password immediately. It does not. It only sets the calendar boundaries. If a user changed their password yesterday, the ninety-day countdown starts from yesterday. The system will not force a change until the window closes. If you need immediate rotation, use sudo chage -d 0 username. The login prompt will display You are required to change your password immediately (root enforced).
The passwd command can also set expiration, but it overrides other fields if you are not careful. Running sudo passwd -x 90 username sets the maximum days but leaves the minimum and warning fields untouched. This creates inconsistent policies across accounts. Stick to chage when you need full control over all three boundaries.
SELinux does not block chage or login.defs modifications. The shadow_t domain handles password files. Denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. Trust the package manager. Manual file edits drift, snapshots stay.
When to use this vs alternatives
Use /etc/login.defs and chage when you need system-wide rotation on standard Workstation or Server installs. Use LDAP or FreeIPA when you are managing hundreds of accounts across multiple machines. Use passwd -x when you only need to change one account quickly without touching global defaults. Stick to local shadow management when you are running a single isolated host. Stay on the upstream Workstation if you only deviate from the defaults occasionally.