Stale accounts are a classic attack surface, so this script disables logins that have gone unused for too long. It targets only real people by selecting UIDs of 1000 or higher with genuine login shells from /etc/passwd, while a whitelist protects critical names like root. For each candidate it reads lastlog to find the most recent login, treating "Never logged in" as inactive and otherwise converting the timestamp to epoch seconds to compare against the threshold. Accounts already locked (shown by passwd -S as status L) are reported and skipped. By default the script runs in dry-run mode, printing what it would do so you can review before committing. Passing apply makes it act: usermod -L locks the password and the shell is switched to nologin for defence in depth. The aligned table of user, last login, and action gives a clear, auditable result every run.
Lock inactive users
Finds human accounts with no login activity beyond a chosen threshold and locks them, with a safe dry-run mode by default.
13 views
Share
Script
Download .shbash
#!/usr/bin/env bash
# Lock interactive accounts that have not logged in for N days.
#
# Usage:
# sudo ./lock_inactive_users.sh # default: 90 days, dry-run
# sudo ./lock_inactive_users.sh 60 # 60-day threshold, dry-run
# sudo ./lock_inactive_users.sh 60 apply # actually lock the accounts
set -euo pipefail
DAYS="${1:-90}" # inactivity threshold in days
MODE="${2:-dryrun}" # 'apply' to lock; anything else = dry run
if [[ "$(id -u)" -ne 0 ]]; then
echo "Error: must run as root." >&2
exit 1
fi
# Accounts we never want to touch (system/service accounts).
WHITELIST="root|sync|halt|shutdown|nobody"
# Only consider "human" accounts: UID >= 1000 and a real login shell.
# We pull these from /etc/passwd.
mapfile -t USERS < <(
awk -F: '$3 >= 1000 && $7 !~ /(nologin|false)$/ {print $1}' /etc/passwd
)
now_epoch=$(date +%s)
threshold_secs=$(( DAYS * 86400 ))
printf "%-16s %-22s %-10s\n" "USER" "LAST LOGIN" "ACTION"
printf '%.0s-' {1..50}; echo
for user in "${USERS[@]}"; do
# Never lock whitelisted accounts.
[[ "$user" =~ ^(${WHITELIST})$ ]] && continue
# 'lastlog -u' shows the most recent login for one user.
# "**Never logged in**" means the account has never been used.
last_line=$(lastlog -u "$user" | tail -n1)
if echo "$last_line" | grep -q "Never logged in"; then
last_display="never"
inactive=1
else
# Extract the date portion (everything after the port/host columns).
last_date=$(echo "$last_line" | awk '{print $(NF-5), $(NF-4), $(NF-3), $(NF-2), $NF}')
last_epoch=$(date -d "$last_date" +%s 2>/dev/null || echo 0)
last_display=$(date -d "@$last_epoch" "+%Y-%m-%d" 2>/dev/null || echo "unknown")
if (( last_epoch > 0 && (now_epoch - last_epoch) > threshold_secs )); then
inactive=1
else
inactive=0
fi
fi
# Skip accounts that are already locked (passwd -S shows status 'L').
status=$(passwd -S "$user" 2>/dev/null | awk '{print $2}')
if [[ "$status" == "L" ]]; then
printf "%-16s %-22s %-10s\n" "$user" "$last_display" "already-locked"
continue
fi
if [[ "$inactive" -eq 1 ]]; then
if [[ "$MODE" == "apply" ]]; then
# Lock the password AND set the shell to nologin for defence-in-depth.
usermod -L "$user"
usermod -s /usr/sbin/nologin "$user" 2>/dev/null || \
usermod -s /sbin/nologin "$user"
printf "%-16s %-22s %-10s\n" "$user" "$last_display" "LOCKED"
else
printf "%-16s %-22s %-10s\n" "$user" "$last_display" "would-lock"
fi
else
printf "%-16s %-22s %-10s\n" "$user" "$last_display" "active"
fi
done
[[ "$MODE" != "apply" ]] && echo -e "\n(dry run — re-run with 'apply' to enforce)"How to run
Review the script first, then download or copy it and run it in your environment.
You might also like
pythoncross-platformIntermediate
Validate a .env File Against a Schema
Check that a .env file contains all required variables with the correct types before your application starts.
pythoncross-platformIntermediate
Cron Expression Next-Run Calculator
Given a cron expression, print the next several times it will fire along with the countdown to each.
bashlinuxIntermediate
Backup All MySQL Databases with mysqldump
Dumps each MySQL database to its own compressed SQL file.