#!/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__