Password Age
password aging¶
Password aging is another technique used by system administrators to defend against bad passwords within an organization. Password aging means that after a set amount of time (usually 90 days) the user is prompted to create a new password. The theory behind this is that if a user is forced to change his password periodically, a cracked password is only useful to an intruder for a limited amount of time. The downside to password aging, however, is that users are more likely to write their passwords down.
Why turn off Linux password aging information? Isn’t that a security risk?¶
It is true that turning off password aging increases the risk of unauthorized access, especially if a password is compromised or leaked online. Enforcing regular password changes is generally recommended as part of the best security practices. However, here are some scenarios where turning off password aging might be considered useful:
- Certain systems or user accounts might have their passwords managed by other mechanisms, making password aging unnecessary. These mechanisms can be automated scripts, third-party auth systems, or centralized directory servers.
- Service or application-level accounts, such as those by PHP, Python, Nginx, Apache, and MySQL servers, don’t have direct shell access. As humans do not directly access them, such service accounts usually do not need regular password changes.
- In development or testing environments, frequent password changes can be cumbersome and not required, as developers often don’t have direct access to the system. Some testing environments reset accounts every week, and a new copy of the application from the git repo might be pulled, depending upon the policy.
- Please note that there is growing evidence that constantly remembering and changing passwords can be frustrating for users. Often, they choose bad passwords or need password management to maintain a list of complex passwords. In such cases, turning off password aging can improve the user experience, but it comes at the cost of reduced security. Each use case differs, so you must evaluate your security risk profile.
Check Password Age¶
#!/bin/bash
# Check Password Age
for account in $(awk -F':' '{ print $1}' /etc/passwd)
do
expiers_string="$(sudo chage -l "$account" | grep 'Account expires' | awk '{print $4, $5, $6}')"
changed_date="$(sudo chage -l "$account" | grep 'Last password change' | awk '{print $5, $6, $7}')"
echo "ACCOUNT: $account , EXPIRES: $expiers_string, CHANGED: $changed_date"
done
Find number of days left before expire¶
#!/bin/bash
# Name: days.sh
# Purpose: Find days left to expire the Linux user account
# Author: Vivek Gite {https://www.cyberciti.biz/} under GPL v2.x+
# ----------------------------------------------------------------
_user="${1?:User name missing. Bye}"
current_date="$(date +"%b %d, %Y")"
# Check for the root user
if [[ $EUID -ne 0 ]]
then
echo "Error: $0 - script must be run as root." 1>&2
exit 1
fi
# Calculate the days between two Linux dates
calculate_days(){
local e_date t1 t2 t_diff xdays
# input
e_date="$1"
# convert the dates to Unix timestamps:
t1=$(date -d "$current_date" +%s)
t2=$(date -d "$e_date" +%s)
# calculate the difference in Unix timestamps
t_diff=$((t2 - t1))
# convert the difference to days:
xdays=$((t_diff / 86400))
# print it
echo "$xdays"
}
Validate user exist¶
# Ensure user exits else die
if grep -q -w "^${_user}" /etc/passwd
then
account_expire_date=$(chage -l "$_user" | awk -F':' '/Account expires/{ gsub(/^ /, "", $2); print $2}')
password_expire_date=$(chage -l "$_user"| awk -F':' '/Password expires/{ gsub(/^ /, "", $2); print $2}')
if [ "$account_expire_date" == "never" ]
then
echo "Warning: $0 - account aging [account expire date] not set for the $_user account."
else
output=$(calculate_days "$account_expire_date")
echo "The $_user account has $output day(s) left to expire their account."
fi
if [ "$password_expire_date" == "never" ]
then
echo "Warning: $0 - account aging [password expire date] not set for the $_user account."
else
output=$(calculate_days "$password_expire_date")
echo "The $_user account has $output day(s) left to expire their password."
fi
else
echo "Error: $0 - $_user username not found in the /etc/passwd file."
fi
man chage
chage --help
Options:
-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
Next, set the password expiry policy for the user using the chage command as follows:
Where,
- chage: Run the chage command to set the Linux password aging policy.
- -E "$(date -d "+365days" +%F)": Sets the password expiration date for the user tkhrl to 365 days from today
- -m 0: Sets the minimum number of days that must pass between password changes. When I set it to the 0 days, which means that the user can change their password as often as they want.
- -M 365: Sets the maximum number of days that a password can be used before it expires. I set to 365 days, which means that the user’s password will expire after 365 days.
- -W 7: The user will be warned 7 days before their password expires.
- -I 14: The user’s account will be locked after 14 days if they do not change their password.
- tkhrl: Linux user name.
Finally, confirm all details:
Python validation sccript¶
This Python script (it must be run as root, since it reads /etc/shadow and pokes into everyone's home directory) will print the login names of all unlocked users. This is usually more useful than the list of all locked users, since that includes a bunch of uninteresting system accounts.
#! /usr/bin/python3
import os
import stat
import sys
def get_homes_and_uids_for_users_with_shells():
users = {}
with open("/etc/passwd", "rt") as pf:
for line in pf:
login, x, uid, gid, gecos, home, shell = line.split(':')
if x != 'x':
sys.stderr.write("*** Account '{!r}' not properly shadowed\n"
.format(login))
if shell not in ('/bin/false', '/usr/bin/false',
'/sbin/nologin', '/usr/sbin/nologin'):
users[login] = (int(uid), home)
return users
def check_ssh_auth_perms(path, owner):
owners = (0, owner)
badwritebits = stat.S_IWGRP | stat.S_IWOTH # 0022
# FIXME: I'm not sure whether sshd allows symlinks at any point in this
# path. Conservatively assume it does.
# FIXME: Doesn't check for ACLs.
try:
st = os.stat(path)
except FileNotFoundError:
return False
if (st.st_size == 0 or st.st_uid not in owners
or not stat.S_ISREG(st.st_mode)
or stat.S_IMODE(st.st_mode) & badwritebits):
return False
while True:
path = os.path.dirname(path)
# Not necessary to check for ENOENT; if we got here at all,
# the entire chain of parent dirs must exist
st = os.stat(path)
if (st.st_uid not in owners
or not stat.S_ISDIR(st.st_mode)
or stat.S_IMODE(st.st_mode) & badwritebits):
return False
if path == '/': break
return True
def get_users_with_ssh_keys(all_users):
ssh_users = set()
already_processed_homes = {}
for login, (uid, home) in all_users.items():
if home in already_processed_homes:
if already_processed_homes[home]:
ssh_users.add(login)
continue
if (check_ssh_auth_perms(home + "/.ssh/authorized_keys", uid)
or check_ssh_auth_perms(home + "/.ssh/authorized_keys2", uid)):
already_processed_homes[home] = True
ssh_users.add(login)
else:
already_processed_homes[home] = False
return ssh_users
def get_users_with_possible_passwords():
pw_users = set()
with open("/etc/shadow", "rt") as sf:
for line in sf:
login, phash, chg, mina, maxa, warn, inact, expir, res \
= line.split(':')
if not phash:
sys.stderr.write("*** Account '{!r}' has blank password\n"
.format(login))
if phash[0] != '*' and phash[0] != '!':
pw_users.add(login)
return pw_users
def main():
users = get_homes_and_uids_for_users_with_shells()
ssh_users = get_users_with_ssh_keys(users)
pw_users = get_users_with_possible_passwords()
active_users = set(users.keys()) & (ssh_users | pw_users)
for u in sorted(active_users):
sys.stdout.write(u + "\n")
main()