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
bashlinuxIntermediate
CPU Load Average Alert
Warns by email when the one-minute load average per CPU core exceeds a configurable ratio.
bashlinuxIntermediate
Bulk Rename Files by Pattern
Renames many files at once using a sed substitution, with dry-run preview.
bashlinuxBeginner
List GCP compute instances
Show every Compute Engine VM in a project with zone, machine type, status, and IPs.