Files
ServerUserCreate/bin/check_last_login.sh
Clemens Schwaighofer c801ef40b4 Switch from lastlogin to lsogins
Debian 13 dropped lastlogin, replaced with lastlogin2 which is an extra install.
Switch to lslogins, which also makes parsing much easier
2025-09-12 10:16:05 +09:00

340 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# Checks for last access of users in sshallow group
# if user login >30days, remoe user from sshallow group and write log
# base folder
BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/";
# which groups holds the ssh allowed login users (outside of admin users)
ssh_groups=('sshforward' 'sshallow' 'sshreject');
ssh_reject_group='sshreject';
# date now for compare
now=$(date +"%s");
# max age for last login or account create without login
max_age_login=90;
warn_age_login=80;
max_age_create=30;
# one day in seconds
day=86400;
# delete account strings
lock_accounts="";
unlock_flag=0;
unlock_accounts="";
# only needed for json output
first_run=1;
# log base folder
LOG="${BASE_FOLDER}/../log";
# auth log file user;date from collect_login_data script
AUTH_LOG="${BASE_FOLDER}/../auth-log/user_auth.log";
error=0;
if [ "$(whoami)" != "root" ]; then
echo "Script must be run as root user";
error=1;
fi;
if [ ! -d "${LOG}" ]; then
echo "log folder ${LOG} not found";
error=1;
fi;
if [ -z "$(command -v curl)" ]; then
echo "Missing curl application, aborting";
error=1;
fi;
if [ -z "$(command -v jq)" ]; then
echo "Missing jq application, aborting";
error=1;
fi;
# use lslogins instead of last log
if [ -z "$(command -v lslogins)" ]; then
echo "Missing lslogins application, aborting";
error=1;
fi;
if [ $error -eq 1 ]; then
exit;
fi;
# option 1 in list
case "${1,,}" in
text)
OUTPUT_TARGET="text";
;;
json)
OUTPUT_TARGET="json";
echo "{";
;;
csv)
CSV_LINE="%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n";
OUTPUT_TARGET="csv";
echo "Account ID,Region,Instance ID,Hostname,Username,Main Group,SSH Group,Account Created Date,Account Age,Last Login Date,Last Login Age,Never Logged In,Login Source,Status";
;;
*)
OUTPUT_TARGET="text";
;;
esac;
# collect info via: curl http://169.254.169.254/latest/meta-data/
instance_data=$(
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") &&
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/dynamic/instance-identity/document
)
instance_id=$(echo "${instance_data}" | jq .instanceId)
account_id=$(echo "${instance_data}" | jq .accountId)
region=$(echo "${instance_data}" | jq .region)
if [ "${OUTPUT_TARGET}" = "text" ]; then
LOG="${LOG}/check_ssh_user.$(date +"%F_%H%M%S").log";
exec &> >(tee -a "${LOG}");
echo "[START] =============>";
echo "AWS ID : ${account_id}";
echo "Region : ${region}";
echo "Instance ID : ${instance_id}";
echo "Hostname : $(hostname)";
echo "Run date : $(date +"%F %T")";
echo "Max age last login : ${max_age_login} days";
echo "Warn age last login: ${warn_age_login} days";
echo "Max age no login : ${max_age_create} days";
elif [ "${OUTPUT_TARGET}" = "json" ]; then
echo '"Info": {'
echo '"AccountId": '"${account_id}"',';
echo '"Region": '"${region}"',';
echo '"InstanceId": '"${instance_id}"',';
echo '"Hostname": "'"$(hostname)"'",';
echo '"Date": "'"$(date +"%F %T")"'",';
echo '"MaxAgeLogin": '${max_age_login}',';
echo '"WarnAgeLogin": '${warn_age_login}',';
echo '"MaxAgeCreate": '${max_age_create}'';
echo '},'
echo '"Users": ['
fi;
for ssh_group in "${ssh_groups[@]}"; do
if [ "${OUTPUT_TARGET}" = "text" ]; then
echo "--------------------->"
if [ "${ssh_group}" = "${ssh_reject_group}" ]; then
echo "Showing current SSH Reject users:";
unlock_flag=1
else
echo "Checking Group : ${ssh_group}";
fi;
fi;
while read -r username; do
# skip empty, if group exists but has no users
if [ "${username}" = "" ]; then
continue;
fi;
# check that user exists in passwd
if ! id "${username}" &>/dev/null; then
out_string="[!] User $username does not exists in /etc/passwd file";
case "${OUTPUT_TARGET}" in
text)
echo "${out_string}";
;;
json)
echo "{";
echo '"Username": "'"${username}"'",';
echo '"SshGroup": "'"${ssh_group}"'",';
echo '"MainGroup": "",';
echo '"SubGroups": [],';
echo '"AccountCreatedDate": "",';
echo '"AccountAge": "",';
echo '"LastLoginDate": "",';
echo '"LastLoginAge": "",';
echo '"LoginSource": "",';
echo '"NeverLoggedIn": true,';
echo '"Status": "'"${out_string}"'"';
echo "}";
;;
csv)
# shellcheck disable=SC2059
printf "${CSV_LINE}" "${account_id}" "${region}" "${instance_id}" "$(hostname)" "${username}" "" "${ssh_group}" "" "" "" "" "true" "${out_string}"
;;
esac;
continue;
fi;
# for json output, we need , between outputs
if [ "${OUTPUT_TARGET}" = "json" ] && [ $first_run -eq 0 ]; then
echo ",";
fi;
first_run=0;
# unlock lis of users
if [ $unlock_flag -eq 1 ]; then
unlock_accounts="${unlock_accounts} ${username}"
fi;
account_age=0;
lock_user=0;
out_string="";
# get the main group for this user
main_group=$(id -gn "${username}")
# get all the sub groups for this user, and remove the main groups
sub_groups=$(id -Gn "${username}" | sed -e "s/${main_group}//" | sed -e "s/${ssh_group}//")
#echo "* Checking user ${username}";
# check user create time, if we have set it in comment
user_create_date_string=$(grep "${username}:" /etc/passwd | cut -d ":" -f 5);
# if empty try last password set time
if ! [[ "${user_create_date_string}" =~ ^\d{4}-\d{2}-\{2} ]]; then
# user L 11/09/2020 0 99999 7 -1
user_create_date_string=$(passwd -S "${username}" | cut -d " " -f 3);
fi;
# last try is user home .bash_logout
if ! [[ "${user_create_date_string}" =~ ^\d{4}-\d{2}-\{2} ]]; then
# try logout or bash history
home_dir_bl=$(grep "${username}:" /etc/passwd | cut -d ":" -f 6)"/.bash_logout";
home_dir_bh=$(grep "${username}:" /etc/passwd | cut -d ":" -f 6)"/.bash_history";
# check that this file exists
if [ -f "${home_dir_bl}" ]; then
user_create_date_string=$(stat -c %Z "${home_dir_bl}");
elif [ -f "${home_dir_bh}" ]; then
user_create_date_string=$(stat -c %Z "${home_dir_bh}");
fi;
fi;
# still no date -> set empty
if ! [[ "${user_create_date_string}" =~ ^\d{4}-\d{2}-\{2} ]]; then
user_create_date_string="";
fi;
# below only works if the user logged in, a lot of them are just file upload
# users. Use the collect script from systemd-logind or /var/log/secure
# for the rest use lslogin, returns ":" separted list, not set is never logged in
# LAST LOGIN :FAILED LOGIN
# 2025-09-12T09:56:22+09:00:
last_login_string=$(
lslogins \
-c --noheadings --notruncate \
--time-format=iso \
-o LAST-LOGIN,FAILED-LOGIN \
-l "${username}"
);
last_login_date=$(echo "${last_login_string}" | cut -d ":" -f 1);
# search="Never logged in";
never_logged_in="false";
found="";
login_source="";
last_login_date="";
account_age="";
last_login="";
# problem with running rep check in if
if [ -f "${AUTH_LOG}" ]; then
found=$(grep "${username};" "${AUTH_LOG}");
fi;
# always pre work account dates if they exist, but output only if text
if [ -n "${user_create_date_string}" ]; then
user_create_date=$(echo "${user_create_date_string}" | date +"%s" -f -);
# if all empty, we continue with only check if user has last login date
# else get days since creation
#account_age=$[ ($(date +"%s")-$(date -d "${user_create_date}" +"%s"))/24 ];
account_age=$(awk '{printf("%.0f\n",($1-$2)/$3)}' <<<"${now} ${user_create_date} ${day}");
user_create_date_out=$(echo "${user_create_date_string}" | date +"%F" -f -);
fi;
if [ -n "${found}" ]; then
last_login_date_string=$(grep "${username};" "${AUTH_LOG}" | cut -d ";" -f 2);
last_login_date=$(echo "${last_login_date_string}" | date +"%s" -f -);
last_login=$(awk '{printf("%.0f\n",($1-$2)/$3)}' <<<"${now} ${last_login_date} ${day}");
if [ "${last_login}" -gt ${max_age_login} ]; then
out_string="[!] Last ssh log in ${last_login} days ago";
if [ "${ssh_group}" != "${ssh_reject_group}" ]; then
lock_user=1;
fi;
elif [ "${last_login}" -gt ${warn_age_login} ]; then
out_string="WARN [last ssh login ${last_login} days ago]";
else
out_string="OK [ssh, ${last_login} days ago]";
fi;
login_source="ssh";
# rewrite to Y-M-D, aka
last_login_date="${last_login_date_string}"
elif [ -n "${last_login_date}" ]; then
# if we have "** Never logged in**" the user never logged in
# we get an ISO DATE with timezone
last_login_date=$(echo "${last_login_string}" | date +"%s" -f -);
last_login=$(awk '{printf("%.0f\n",($1-$2)/$3)}' <<<"${now} ${last_login_date} ${day}");
if [ "${last_login}" -gt ${max_age_login} ]; then
out_string="[!] Last terminal log in ${last_login} days ago";
if [ "${ssh_group}" != "${ssh_reject_group}" ]; then
lock_user=1;
fi;
elif [ "${last_login}" -gt ${warn_age_login} ]; then
out_string="WARN [last terminal login ${last_login} days ago]";
else
out_string="OK [lastlog, ${last_login} days ago]";
fi;
login_source="lastlog";
last_login_date=$(echo "${last_login_string}" | date +"%F %T" -f -)
elif [ -n "${user_create_date}" ]; then
if [ "${account_age}" -gt ${max_age_create} ]; then
out_string="[!] Never logged in: account created ${account_age} days ago";
if [ "${ssh_group}" != "${ssh_reject_group}" ]; then
lock_user=1;
fi;
else
out_string="OK [Never logged in, created ${account_age} days ago]";
fi;
never_logged_in="true";
else
out_string="[!!!] Never logged in and we have no create date";
never_logged_in="true";
fi;
# build delete output
if [ ${lock_user} -eq 1 ]; then
lock_accounts="${lock_accounts} ${username}"
fi;
case "${OUTPUT_TARGET}" in
text)
printf "* Checking user %-20s (%-20s): %s\n" "${username}" "${main_group}" "${out_string}";
;;
json)
sub_groups_string="["
sub_group_first=1
for s_group in $sub_groups; do
if [ "${sub_group_first}" = 0 ]; then
sub_groups_string="${sub_groups_string},";
fi;
sub_groups_string="${sub_groups_string}\"${s_group}\"";
sub_group_first=0;
done;
sub_groups_string="${sub_groups_string}]";
echo "{";
echo '"Username": "'"${username}"'",';
echo '"SshGroup": "'"${ssh_group}"'",';
echo '"MainGroup": "'"${main_group}"'",';
echo '"SubGroups": '"${sub_groups_string}"',';
echo '"AccountCreatedDate": "'"${user_create_date_out}"'",';
echo '"AccountAge": "'"${account_age}"'",';
echo '"LastLoginDate": "'"${last_login_date}"'",';
echo '"LastLoginAge": "'"${last_login}"'",';
echo '"LoginSource": "'"${login_source}"'",';
echo '"NeverLoggedIn": '"${never_logged_in}"',';
echo '"Status": "'"${out_string}"'"';
echo "}";
;;
csv)
# shellcheck disable=SC2059
printf "${CSV_LINE}" "${account_id}" "${region}" "${instance_id}" "$(hostname)" "${username}" "${main_group}" "${ssh_group}" "${user_create_date_out}" "${account_age}" "${last_login_date}" "${last_login}" "${never_logged_in}" "${login_source}" "${out_string}"
;;
esac;
done <<< "$(grep "${ssh_group}:" /etc/group | cut -d ":" -f 4 | sed -e 's/,/\n/g')";
done;
if [ "${OUTPUT_TARGET}" = "text" ]; then
if [ -n "${lock_accounts}" ]; then
echo "--------------------->"
echo "% Run script below to move users to reject ssh group";
echo "";
echo "bin/lock_user.sh ${lock_accounts}";
fi;
if [ -n "${unlock_accounts}" ]; then
echo "--------------------->"
echo "% Run script below to move users to allow or forward ssh group";
echo "";
echo "For ALLOW:"
echo "bin/unlock_user.sh -s allow ${unlock_accounts}";
echo "";
echo "For FORWARDONLY:"
echo "bin/unlock_user.sh -s forward ${unlock_accounts}";
fi;
echo "[END] ===============>"
elif [ "${OUTPUT_TARGET}" = "json" ]; then
# users
echo "]";
# overall
echo "}";
fi;
# __END__