Current File : //lib64/nagios/plugins/nccustom/check_unauthorized_user.sh |
#!/bin/bash
#
# Shell script for tracking unauthorized system users and their logins.
# The script checks if unauthorized system users with enabled shell are presented on the system.
# By comparing with authorized users.
# The authorized users can have or don't have SSH access allowed.
# SSH access controls by flags: ssh_allowed and ssh_denied in authorized users list file.
# The script includes several main checks:
# 1) Check for any user other than root with UID=0.
# 2) Check for unauthorized system users with enabled shell.
# 3) Check for recent logins of unauthorized system users with enabled shell.
# In case something is found script creates lock file, logs a message and exits with code 2.
# The lock file needed for avoiding self-resolved cases.
# Presence of the lock file doesn't skip the main checks.
# Bash strict mode.
set -uo pipefail
# Declare and assign global variables.
# Authorized users associative array.
declare -A AUTHORIZED_USERS
# Get the directory where the script is located.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
# Path to authorized users list file in the script's directory.
AUTHORIZED_USERS_FILE="${SCRIPT_DIR}/authorized_users.list"
# Auth log.
AUTH_LOG="/var/log/secure"
# cPanel users list.
CPANEL_USERS_LIST=""
# Shell patterns to exclude during the check.
EXCLUDED_SHELL_PATTERNS="(/[s]?bin/(false|nologin|halt|shutdown|sync)|/usr/local/cpanel/bin/noshell)"
# Determine a lock file.
LOCK_FILE="/var/lock/check_unauthorized_user.lock"
# Determine a log file.
LOG_FILE="/var/log/check_unauthorized_user.log"
# Set the PATH
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
# Systems users array.
SYSTEM_USERS=()
# System users with enabled shell array.
SYSTEM_USERS_WITH_ENABLED_SHELL=()
#######################################
# Logs a message with a timestamp.
# Globals:
# LOG_FILE - The file where the logs will be appended.
# Arguments:
# message - The message to be logged.
# no_log - Optional, if provided, the message will be printed without timestamp and not logged to log file.
# Outputs:
# Writes message to stdout or stdout and file.
#######################################
logger() {
# Retrieve the message to be logged from the first argument.
local message="${1}"
# Retrieve the optional 'no_log' argument, if provided.
local no_log="${2:-}"
# Check if 'no_log' is set (not empty).
if [[ -n "${no_log}" ]]; then
# Print the message to stdout without a timestamp.
echo "${message}"
else
# Check if the log file exists and is writable.
if [[ ! -w "${LOG_FILE}" ]]; then
echo "ERROR! Log file '${LOG_FILE}' does not exist or is not writable."
echo "HINT: Log file '${LOG_FILE}' should have the following permissions: 600, owner nrpe:nrpe (CloudLinux/AlmaLinux)."
exit 3
fi
# Generate a timestamp in the format: Month Day HH:MM:SS ±hhmm.
local timestamp
timestamp=$(date +"%b %d %H:%M:%S %z")
# Append the message with the timestamp to the log file.
echo "[${timestamp}] ${message}" >> "${LOG_FILE}"
fi
}
#######################################
# Check for the existence of a lock file and create it if it does not exist.
# Globals:
# LOCK_FILE
# LOG_FILE
# Arguments:
# None
# Returns:
# Exits with code 2 if the lock file exists.
#######################################
set_lock() {
# Check if the lock file exists.
if [[ -f "$LOCK_FILE" ]]; then
logger "CRITICAL! ${LOCK_FILE} exists. Investigation needed." "no_log"
logger "HINT: Check ${LOG_FILE} for details." "no_log"
exit 2
else
# If the lock file does not exist, create it.
touch "$LOCK_FILE"
logger "Lock file was created: ${LOCK_FILE}"
fi
}
#######################################
# Get the timestamp from a certain number of minutes ago
# Globals:
# None
# Arguments:
# Minutes ago (default: 30 minutes ago)
# Returns:
# Timestamp string representing a specific time in the past.
#######################################
get_minutes_ago_timestamp() {
# Retrieve the number of minutes ago from the first argument; default to 30 if not provided.
local minutes_ago=${1:-30}
# Use the `date` command to get the timestamp for the specified number of minutes ago.
# Format the output as: Month Day HH:MM:SS.
date --date="${minutes_ago} minutes ago" "+%b %e %H:%M:%S"
}
#######################################
# Get the user's shell based on the username.
# Globals:
# None
# Arguments:
# User to check.
# Returns:
# User shell, 3 on error.
#######################################
get_user_shell() {
local username="$1"
# Check if the username is provided (not empty).
if [[ -z "$username" ]]; then
# Log an error message and exit with code 3 if username is not provided.
logger "ERROR! Username is not provided" "no_log"
exit 3
fi
# Get the login shell for the user by querying the passwd database.
# `getent passwd "$username"` retrieves the user entry.
# `cut -d: -f7` extracts the 7th field (the shell) from the entry.
local user_shell
user_shell=$(getent passwd "$username" | cut -d: -f7)
echo "$user_shell"
}
#######################################
# Get the user's uid based on the username.
# Globals:
# None
# Arguments:
# User to check.
# Returns:
# User uid, 3 on error.
#######################################
get_user_uid() {
local username="$1"
# Check if the username is provided (not empty).
if [[ -z "$username" ]]; then
# Log an error message and exit with code 3 if username is not provided.
logger "ERROR! Username is not provided." "no_log"
exit 3
fi
# Get the uid for the user by querying the passwd database.
# `getent passwd "$username"` retrieves the user entry.
# `cut -d: -f3` extracts the 7th field (the user uid) from the entry.
local user_uid
user_uid=$(getent passwd "${username}" | cut -d: -f3)
echo "$user_uid"
}
#######################################
# Get any user other than root with UID=0.
# Globals:
# None
# Arguments:
# None
# Returns:
# Total amount of non-root users with UID=0, exit with code 2.
#######################################
# New function to check
get_users_with_uid0() {
local -i uid0_users_count=0
local uid0_users_details=""
# Loop through each line of the passwd file, splitting by colon.
while IFS=: read -r username _ uid _; do
# Check if the UID is 0 and the username is not root.
if [[ "${uid}" -eq 0 && "${username}" != "root" ]]; then
# Log a critical message if a non-root user with UID=0 is found.
logger "CRITICAL! Non-root user ${username} has UID=0"
# Increment the counter for users with UID=0.
(( uid0_users_count += 1 ))
# Append the user details.
uid0_users_details+="${username}, "
fi
done < <(getent passwd) # Use getent to read the passwd database.
# If any non-root users with UID=0 were found.
if [[ "${uid0_users_count}" -gt 0 ]]; then
# Invoke function to set lock file.
set_lock
# Remove trailing comma and space.
uid0_users_details="${uid0_users_details%, }"
# Log a critical message with the list of non-root users with UID=0.
logger "CRITICAL! Non-root users with UID=0 found (${uid0_users_count}): ${uid0_users_details}" "no_log"
# Exit with code 2 to indicate that any non-root users with UID=0 were found.
exit 2
fi
}
#######################################
# Get list of cPanel users from WHM API
# Globals:
# CPANEL_USERS_LIST
# Arguments:
# None
# Returns:
# cPanel users list.
#######################################
get_cpanel_users() {
# Get cPanel users using WHM API call and format the output.
CPANEL_USERS_LIST=$(whmapi1 --output=jsonpretty listaccts | jq '.data.acct[].user' | tr -d "\"" | sort)
}
#######################################
# Get list of system users excluding cPanel users
# Globals:
# CPANEL_USERS_LIST
# SYSTEM_USERS
# Arguments:
# None
# Returns:
# System users list.
#######################################
get_system_users() {
# Read each username from the list of system users.
while read -r username; do
# Check if the username is not present in the cPanel users list.
# `grep -q "^${username}$"` searches for an exact match of the username.
# `<<< "${CPANEL_USERS_LIST}"` provides the cPanel users list as input.
if ! grep -q "^${username}$" <<< "${CPANEL_USERS_LIST}"; then
# Add the username to the SYSTEM_USERS array if not in the cPanel users list.
SYSTEM_USERS+=("${username}")
fi
done < <(getent passwd | cut -d: -f1 | sort) # Get the sorted list of system usernames.
}
#######################################
# Get list of system users with enabled shell.
# Globals:
# EXCLUDED_SHELL_PATTERNS
# SYSTEM_USERS_WITH_ENABLED_SHELL
# SYSTEM_USERS
# Arguments:
# None
# Returns:
# System users with enabled shell list.
#######################################
get_system_users_with_enabled_shell() {
# Iterate over each user in the SYSTEM_USERS array.
for user in "${SYSTEM_USERS[@]}"; do
# Get the login shell for the user by calling the get_user_shell function.
local user_shell
user_shell=$(get_user_shell "${user}")
# Check if the user's shell does not match any of the excluded shell patterns.
# `grep -q -E "${EXCLUDED_SHELL_PATTERNS}"` searches for any match of the excluded patterns.
# `<<< "${user_shell}"` provides the user’s shell as input.
if ! grep -q -E "${EXCLUDED_SHELL_PATTERNS}" <<< "${user_shell}"; then
# Add the user to the SYSTEM_USERS_WITH_ENABLED_SHELL array if their shell is not excluded.
SYSTEM_USERS_WITH_ENABLED_SHELL+=("${user}")
fi
done
}
#######################################
# Get unauthorized system users with enabled shell
# Globals:
# AUTHORIZED_USERS
# SYSTEM_USERS_WITH_ENABLED_SHELL
# Arguments:
# None
# Returns:
# Total amount of unauthorized system users with exit code 2
#######################################
get_unauthorized_system_users() {
local -i unauthorized_system_users_count=0
local unauthorized_system_users_details=""
# Iterate over each user in the SYSTEM_USERS_WITH_ENABLED_SHELL array.
for user in "${SYSTEM_USERS_WITH_ENABLED_SHELL[@]}"; do
local authorized_user=false
# Check if the user is in the authorized users list.
if [[ -n "${AUTHORIZED_USERS[$user]+x}" ]]; then
authorized_user=true
fi
# If the user is not in the authorized users list.
if [[ "${authorized_user}" == false ]]; then
# Get the user's UID by calling the get_user_uid function.
local user_uid
user_uid=$(get_user_uid "${user}")
# Get the user's shell by calling the get_user_shell function.
local user_shell
user_shell=$(get_user_shell "${user}")
# Log a message to log file if the user is not in the authorized users list.
logger "CRITICAL! Found unauthorized system user ${user} with uid ${user_uid} and enabled ${user_shell} shell."
# Increment the counter for unauthorized system users.
(( unauthorized_system_users_count += 1 ))
# Append the unauthorized user details to the string.
unauthorized_system_users_details+="${user} (uid ${user_uid}), "
fi
done
# If any unauthorized system users were found.
if [[ "${unauthorized_system_users_count}" -gt 0 ]]; then
# Invoke function to set lock file.
set_lock
# Remove trailing comma and space from details string.
unauthorized_system_users_details="${unauthorized_system_users_details%, }"
# Log a critical message with the count and details of unauthorized users.
logger "CRITICAL! Unauthorized system users found (${unauthorized_system_users_count}): ${unauthorized_system_users_details}" "no_log"
# Exit with code 2 to indicate that unauthorized users were found.
exit 2
fi
}
#######################################
# Get unauthorized system users recent logins using authentication logs
# Globals:
# AUTHORIZED_USERS
# AUTH_LOG
# SYSTEM_USERS_WITH_ENABLED_SHELL
# Arguments:
# None
# Returns:
# Total amount of unauthorized system users to login exit code 2
#######################################
get_unauthorized_system_users_logins() {
local threshold_timestamp
# Get the timestamp from 20 minutes ago
threshold_timestamp=$(get_minutes_ago_timestamp 20)
local -i unauthorized_system_users_logins_count=0
local unauthorized_system_users_logins_details=""
# Iterate over each user in the SYSTEM_USERS_WITH_ENABLED_SHELL array.
for user in "${SYSTEM_USERS_WITH_ENABLED_SHELL[@]}"; do
local matches
# Search for log entries related to "sshd" where a session was opened for the specified user.
# Filter the results to include only those entries that are equal to or greater than the threshold timestamp.
matches=$(grep -E "sshd.*: session opened for user ${user}" "${AUTH_LOG}" | \
awk -v threshold="${threshold_timestamp}" '$0 >= threshold')
# If there are any log entries matching the criteria.
if [[ -n "${matches}" ]]; then
local ssh_login_allowed=false
# If the user is in the authorized users list and has ssh login allowed
if [[ -n "${AUTHORIZED_USERS[$user]+x}" ]]; then
if [[ "${AUTHORIZED_USERS[$user]}" == "ssh_allowed" ]]; then
ssh_login_allowed=true
fi
fi
# If the user is not in the authorized users list or has ssh_denied
if [[ "${ssh_login_allowed}" == false ]]; then
# Get the user's UID by calling the get_user_uid function.
local user_uid
user_uid=$(get_user_uid "${user}")
# Get the user's shell by calling the get_user_shell function.
local user_shell
user_shell=$(get_user_shell "${user}")
# Log messages to log file if unauthorized system user login('s) found.
logger "CRITICAL! Recent login found for unauthorized system user ${user} with uid ${user_uid} and enabled ${user_shell} shell."
logger "According to the following record('s) from ${AUTH_LOG} log: ${matches}"
# Increment the counter for unauthorized system users logins.
(( unauthorized_system_users_logins_count += 1 ))
# Append the unauthorized user logins details to the string.
unauthorized_system_users_logins_details+="${user} (uid ${user_uid}), "
fi
fi
done
# If any unauthorized system users logins were found.
if [[ "${unauthorized_system_users_logins_count}" -gt 0 ]]; then
# Invoke function to set lock file.
set_lock
# Remove trailing comma and space.
unauthorized_system_users_logins_details="${unauthorized_system_users_logins_details%, }"
logger "CRITICAL! Unauthorized system users SSH logins found (${unauthorized_system_users_logins_count}): ${unauthorized_system_users_logins_details}" "no_log"
# Exit with code 2 to indicate that unauthorized users logins were found.
exit 2
fi
}
#######################################
# Get unauthorized system users and their logins.
# Globals:
# AUTHORIZED_USERS_FILE
# CPANEL_USERS_LIST
# Arguments:
# None
#######################################
main() {
# Check if the file exists and is not empty
if [[ -f "${AUTHORIZED_USERS_FILE}" && -s "${AUTHORIZED_USERS_FILE}" ]]; then
# Read the file line by line
while IFS=, read -r user access; do
# Skip lines that start with a hash (#) or if either user or access is empty.
if [[ "$user" =~ ^# || -z "$user" || -z "$access" ]]; then
continue
fi
# Trim any extra whitespace around user and access
user=$(echo "$user" | xargs)
access=$(echo "$access" | xargs)
# Ensure both user and access are non-empty before updating the array
if [[ -n "$user" && -n "$access" ]]; then
# Update the array with valid user and access
AUTHORIZED_USERS["$user"]="$access"
else
# Log an error message and exit with code 3 if user or access is invalid
logger "ERROR! Invalid username or access in ${AUTHORIZED_USERS_FILE}. User: '${user}', Access: '${access}'." "no_log"
exit 3
fi
done < "${AUTHORIZED_USERS_FILE}"
else
# Log an error message and exit with code 3 if the file is empty or not found
logger "ERROR! File ${AUTHORIZED_USERS_FILE} is either empty or not found." "no_log"
exit 3
fi
# Invoke function to get any user other than root with UID=0.
get_users_with_uid0
# Check if cPanel binary exists before calling the function.
if [ ! -f /usr/local/cpanel/cpanel ]; then
# Define cPanel users list as empty in case if there is no cPanel.
CPANEL_USERS_LIST=""
else
# Invoke function to get cPanel users list if cPanel is installed.
get_cpanel_users
fi
# Invoke function to get list of system users excluding cPanel users.
get_system_users
# Invoke function to get list of system users with enabled shell.
get_system_users_with_enabled_shell
# Invoke function to get recent logins of unauthorized system users.
get_unauthorized_system_users_logins
# Invoke function to get unauthorized system users with enabled shell.
get_unauthorized_system_users
# Handle the case when no unauthorized users were found but the lock file exists.
if [[ -f "$LOCK_FILE" ]]; then
# Invoke function to control lock file.
set_lock
else
# Print a message if everything is ok and exit with code 0.
logger "OK. Unauthorized system users and logins are NOT detected." "no_log"
exit 0
fi
}
# Run a default mode.
if [[ -z $* ]]; then
# Invoke main function
main
fi