Debian 13 dropped lastlogin, replaced with lastlogin2 which is an extra install. Switch to lslogins, which also makes parsing much easier
340 lines
12 KiB
Bash
Executable File
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__
|