Recently encountered a scenario where multiple users on a Debian server suddenly couldn't authenticate via SFTP, despite having no shell access. The auth logs revealed failed attempts due to password expiration - a detail I'd overlooked during initial setup.
Here's the most thorough command to check password expiration for all users:
for user in $(cut -d: -f1 /etc/passwd); do
echo "=== $user ==="
chage -l $user 2>/dev/null || echo "No password expiration information"
done
To extract just the critical information and sort by expiration date:
getent passwd | cut -d: -f1 | xargs -I {} sh -c '
echo "User: {}"
chage -l {} 2>/dev/null | grep "Password expires"
echo' | awk '/User:/ {user=$2} /Password expires/ {print user ": " $0}' |
sort -k5
Create a monitoring script that runs weekly via cron:
#!/bin/bash
EXPIRING_USERS=$(getent passwd | cut -d: -f1 | xargs -I {} sh -c '
expiry=$(chage -l {} 2>/dev/null | grep "Password expires")
if [[ "$expiry" != *"never"* ]]; then
echo "{}: $expiry"
fi
')
if [ -n "$EXPIRING_USERS" ]; then
echo "The following users have password expiration set:"
echo "$EXPIRING_USERS"
# Add mail/slack notification logic here
fi
To disable password expiration for SFTP-only users:
for user in sftpuser1 sftpuser2 webuser3; do
chage -m 0 -M 99999 -I -1 -E -1 $user
done
For systems without the chage command:
awk -F: '$1!="nobody"{system("passwd -S "$1)}' /etc/passwd |
awk '{print $1,$3,$5,$7}'
Generate a CSV report for spreadsheet analysis:
echo "Username,Last Change,Min Days,Max Days,Warn Days,Expiration Date" > expiry_report.csv
getent passwd | cut -d: -f1 | while read user; do
chage -l $user 2>/dev/null | awk -v user="$user" '
/Last password change/ {last=$NF}
/Minimum/ {min=$NF}
/Maximum/ {max=$NF}
/warning/ {warn=$NF}
/Password expires/ {expires=$0; sub(/.*: /,"",expires)}
END {print user "," last "," min "," max "," warn "," expires}
'
done >> expiry_report.csv
Recently on a Debian server deployment, I faced an unexpected issue where multiple users suddenly couldn't authenticate via SFTP. The auth logs revealed failed login attempts due to expired passwords - a classic case of overlooking default password policies.
What made this particularly problematic was that these were webspace users without shell access, so they:
- Received no expiry warnings
- Couldn't change passwords themselves
- Only saw generic authentication errors
Here are the most useful commands to check password expiration information:
# Check a single user's expiry info
chage -l username
# Batch check all users (GNU/Linux systems)
for user in $(cut -d: -f1 /etc/passwd); do
echo "=== $user ===";
chage -l $user 2>/dev/null || echo "No expiry information";
done
# Alternative using getent (more portable)
getent passwd | cut -d: -f1 | xargs -I {} sh -c 'echo "== {} =="; chage -l {} 2>/dev/null || echo "No expiry info"'
The chage
command outputs several important dates:
Last password change : Jan 01, 2023
Password expires : Jul 01, 2023
Password inactive : never
Account expires : never
Minimum number of days between password change : 0
Maximum number of days between password change : 180
Number of days of warning before password expires : 7
For regular monitoring, I created this script that emails when passwords will expire soon:
#!/bin/bash
WARN_DAYS=14
ADMIN_EMAIL="admin@example.com"
getent passwd | while IFS=: read -r username _ _ _ _ _ _; do
expiry=$(chage -l "$username" 2>/dev/null | awk -F: '/Password expires/{print $2}')
if [[ "$expiry" != *"never"* ]]; then
expiry_epoch=$(date -d "$expiry" +%s)
today_epoch=$(date +%s)
diff_days=$(( (expiry_epoch - today_epoch) / 86400 ))
if [ "$diff_days" -lt "$WARN_DAYS" ] && [ "$diff_days" -ge 0 ]; then
echo "User $username password expires in $diff_days days" | mail -s "Password Expiry Warning" "$ADMIN_EMAIL"
fi
fi
done
To prevent future issues, I adjusted the defaults in /etc/login.defs
:
PASS_MAX_DAYS 99999
PASS_MIN_DAYS 0
PASS_WARN_AGE 14
And set existing users to never expire:
# Set all users to never expire
getent passwd | cut -d: -f1 | xargs -I {} chage -M 99999 {}