version 3.0
This commit is contained in:
83
README.md
83
README.md
@@ -1,33 +1,72 @@
|
||||
# ssh-login-notification
|
||||
**Видео инструкция**
|
||||
[alt text](img/image.png)!
|
||||
|
||||
[](https://youtu.be/a6gkXZ-2pQI)
|
||||
[alt text](img/image.png)
|
||||
|
||||
|
||||
Данный скрипт, при каждом новом входе по SSH, отправляет Вам уведомление в телеграм.
|
||||

|
||||
Сохраните скрипт
|
||||
sudo nano /usr/local/bin/telegram-ssh-notify.sh
|
||||
|
||||
Для работы скрипта Вам понадобится jq
|
||||
Вставьте код и сделайте исполняемым: sudo chmod +x /usr/local/bin/telegram-ssh-notify.sh
|
||||
|
||||
Установка jq Ubuntu / Linux Mint / Debian
|
||||
> sudo apt install jq
|
||||
Создайте конфигурационный файл
|
||||
|
||||
Установка jq CentOS
|
||||
> sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm<br />
|
||||
> sudo yum install jq
|
||||
sudo nano /etc/telegram-ssh-notify.conf
|
||||
|
||||
Установка :
|
||||
Копируем скрипт в /usr/local/bin/
|
||||
> wget -P /usr/local/bin/ https://raw.githubusercontent.com/unixhostpro/ssh-login-notification/master/ssh_login_info.sh
|
||||
|
||||
Устанавливаем права на запуск
|
||||
> chmod +x /usr/local/bin/ssh_login_info.sh
|
||||
TELEGRAM_TOKEN=
|
||||
TELEGRAM_CHAT_ID=
|
||||
TELEGRAM_TOPIC_ID=
|
||||
MAX_ATTEMPTS_BEFORE_CRITICAL=20
|
||||
CRITICAL_TIME_WINDOW=300
|
||||
AUTO_BLOCK_CRITICAL=false
|
||||
WHITELIST_IPS="127.0.0.1 10.0.0.0/8"
|
||||
BLACKLIST_IPS="1.2.3.4"
|
||||
|
||||
Ubuntu
|
||||
В файл /etc/pam.d/common-session добавляем следующую строку
|
||||
> echo "session optional pam_exec.so type=open_session seteuid /usr/local/bin/ssh_login_info.sh" >> /etc/pam.d/common-session
|
||||
Откройте файл /etc/pam.d/sshd в редакторе (например, nano или vim):
|
||||
|
||||
CentOS
|
||||
В файл /etc/pam.d/sshd добавляем следующую строку
|
||||
> echo "session optional pam_exec.so type=open_session seteuid /usr/local/bin/ssh_login_info.sh" >> /etc/pam.d/common-session
|
||||
sudo nano /etc/pam.d/sshd
|
||||
|
||||
Добавьте в конец файла строку:
|
||||
|
||||
session optional pam_exec.so /usr/local/bin/telegram-ssh-notify.sh #(обратите внимание: если вы переименовали скрипт, укажите правильное имя)
|
||||
|
||||
Сохраните файл и закройте редактор.
|
||||
|
||||
Перезапускать SSH не обязательно — PAM читает конфигурацию при каждой новой сессии.
|
||||
|
||||
Создайте файл юнита (например, /etc/systemd/system/ssh-check.service):
|
||||
|
||||
sudo nano /etc/systemd/system/ssh-check.service
|
||||
|
||||
Вставьте следующее содержимое (подставьте правильный путь к вашему скрипту, если он называется иначе, например, telegram-ssh-notify.sh):
|
||||
|
||||
[Unit]
|
||||
Description=SSH Login Monitor for Telegram Notifications
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/ваш_скрипт.sh monitor
|
||||
Restart=always
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Обратите внимание: в ExecStart нужно указать команду с аргументом monitor (как в вашем скрипте).
|
||||
Если ваш скрипт называется telegram-ssh-notify.sh, то строка будет:
|
||||
|
||||
ExecStart=/usr/local/bin/telegram-ssh-notify.sh monitor
|
||||
|
||||
Перечитайте конфигурацию systemd и запустите сервис:
|
||||
|
||||
bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable ssh-check.service # автозапуск при загрузке
|
||||
sudo systemctl start ssh-check.service
|
||||
|
||||
Проверьте статус:
|
||||
|
||||
bash
|
||||
sudo systemctl status ssh-check.service
|
||||
|
||||
Теперь ваш монитор будет работать как демон.
|
||||
BIN
img/image copy.png
Normal file
BIN
img/image copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
img/image.png
Normal file
BIN
img/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,19 +1,735 @@
|
||||
#!/bin/bash
|
||||
# Telegram notification
|
||||
# Send msg when your server load to high
|
||||
token="123456:AasdE8asdaKNiradb1wRZT87pwErerc6biTsVcPE" # put your token here
|
||||
chat_id="1234567" # your chat_id for sending notification
|
||||
sendmsg="https://api.telegram.org/bot$token/sendMessage?parse_mode=markdown" # url for sending msg
|
||||
sendfile="https://api.telegram.org/bot$token/sendDocument?parse_mode=markdown" # url for sending files
|
||||
date="$(date "+%d-%b-%Y-%H:%M")"
|
||||
caption_file=/tmp/ssh_caption_file.txt
|
||||
msg=/tmp/ssh_msg_info.txt
|
||||
curl http://ip-api.com/json/$PAM_RHOST -s -o $caption_file
|
||||
country=$(cat $caption_file | jq '.country' | sed 's/"//g')
|
||||
city=$(cat $caption_file | jq '.city' | sed 's/"//g')
|
||||
org=$(cat $caption_file | jq '.as' | sed 's/"//g')
|
||||
echo -e "📡New SSH login\n*🤖$PAM_USER* logged in on 🖥*$HOSTNAME* at $date from $PAM_RHOST\n🌎Country:*$country*\n🏙City=*$city*\n🕋Organisation=*$org*" > $msg
|
||||
#curl -d text=$message -d chat_id=$chat_id $sendmsg
|
||||
curl $sendmsg -d chat_id=$chat_id -d text="$(<$msg)"
|
||||
rm /tmp/ssh_caption_file.txt
|
||||
rm /tmp/ssh_msg_info.txt
|
||||
#!/usr/bin/env bash
|
||||
# Telegram SSH Notifications Script
|
||||
# Version: 3.0.0
|
||||
# Author: System Administrator
|
||||
# Description: Уведомления о SSH-входах и попытках взлома через Telegram
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
# ============================================================================
|
||||
# CONSTANTS
|
||||
# ============================================================================
|
||||
readonly SCRIPT_NAME="$(basename "${0}")"
|
||||
readonly SCRIPT_VERSION="3.0.0"
|
||||
readonly CONFIG_FILE="/etc/telegram-ssh-notify.conf"
|
||||
readonly DEFAULT_LOG_FILE="/var/log/telegram-ssh-notify.log"
|
||||
readonly FAILED_LOGINS_DIR="/var/log/ssh_failed_logins"
|
||||
readonly IP_CACHE_DIR="/tmp/telegram-ssh-notify-cache"
|
||||
readonly LOCK_FILE="/var/run/telegram-ssh-notify.lock"
|
||||
|
||||
# ============================================================================
|
||||
# DEFAULT CONFIGURATION (может быть переопределено в файле или env)
|
||||
# ============================================================================
|
||||
declare -gA CONFIG=(
|
||||
[TELEGRAM_TOKEN]=""
|
||||
[TELEGRAM_CHAT_ID]=""
|
||||
[TELEGRAM_TOPIC_ID]=""
|
||||
[MAX_ATTEMPTS_BEFORE_CRITICAL]="20"
|
||||
[CRITICAL_TIME_WINDOW]="300" # секунд
|
||||
[CLEANUP_DAYS]="7"
|
||||
[IP_API_TIMEOUT]="3"
|
||||
[ENABLE_GEOIP]="true"
|
||||
[ENABLE_NOTIFICATIONS]="true"
|
||||
[LOG_FILE]="${DEFAULT_LOG_FILE}"
|
||||
[DEBUG]="false"
|
||||
[AUTO_BLOCK_CRITICAL]="false" # автоматически блокировать IP при критической атаке
|
||||
[BLOCK_COMMAND]="iptables -A INPUT -s {ip} -j DROP"
|
||||
[WHITELIST_IPS]="" # разделённые пробелом IP или подсети
|
||||
[BLACKLIST_IPS]="" # сразу блокировать эти IP
|
||||
[RATE_LIMIT_SEC]="60" # минимальный интервал между одинаковыми уведомлениями
|
||||
[USE_JOURNAL]="false" # использовать journalctl вместо файлов логов
|
||||
)
|
||||
|
||||
# Эмодзи для сообщений
|
||||
declare -gA EMOJI=(
|
||||
[SUCCESS]="✅" [INFO]="ℹ️" [WARNING]="⚠️" [DANGER]="🔴" [CRITICAL]="🚨"
|
||||
[FIRE]="🔥" [LOCK]="🔒" [GLOBE]="🌍" [CITY]="🏙️" [BUILDING]="🏢"
|
||||
[CLOCK]="🕐" [MAP]="🗺️" [SERVER]="🖥️" [USER]="👤" [IP]="🔗"
|
||||
[DOOR]="🚪" [BELL]="🔔" [SHIELD]="🛡️" [MAGNIFYING_GLASS]="🔍"
|
||||
)
|
||||
|
||||
# Флаги стран (сокращённо)
|
||||
declare -gA COUNTRY_FLAGS=(
|
||||
[RU]="🇷🇺" [US]="🇺🇸" [DE]="🇩🇪" [CN]="🇨🇳" [UA]="🇺🇦"
|
||||
[BY]="🇧🇾" [KZ]="🇰🇿" [FR]="🇫🇷" [GB]="🇬🇧" [JP]="🇯🇵"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# LOGGING
|
||||
# ============================================================================
|
||||
log() {
|
||||
local level="$1"
|
||||
local message="$2"
|
||||
local timestamp
|
||||
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
local log_file="${CONFIG[LOG_FILE]}"
|
||||
|
||||
# Создаём каталог для лога, если нужно
|
||||
[[ -n "$log_file" ]] && mkdir -p "$(dirname "$log_file")" 2>/dev/null || true
|
||||
|
||||
case "$level" in
|
||||
DEBUG)
|
||||
[[ "${CONFIG[DEBUG]}" == "true" ]] && echo "[$timestamp] [DEBUG] $message" >> "$log_file"
|
||||
;;
|
||||
INFO)
|
||||
echo "[$timestamp] [INFO] $message" >> "$log_file"
|
||||
;;
|
||||
WARN)
|
||||
echo "[$timestamp] [WARN] $message" >> "$log_file"
|
||||
;;
|
||||
ERROR)
|
||||
echo "[$timestamp] [ERROR] $message" >> "$log_file"
|
||||
echo "[$timestamp] [ERROR] $message" >&2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# UTILITY FUNCTIONS
|
||||
# ============================================================================
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
validate_ip() {
|
||||
local ip="$1"
|
||||
# IPv4
|
||||
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
local IFS='.'
|
||||
read -r -a octets <<< "$ip"
|
||||
for octet in "${octets[@]}"; do
|
||||
if (( octet < 0 || octet > 255 )); then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
# IPv6 просто проверяем формат
|
||||
if [[ "$ip" =~ ^[0-9a-fA-F:]+$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
is_private_ip() {
|
||||
local ip="$1"
|
||||
[[ "$ip" =~ ^10\. ]] && return 0
|
||||
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]] && return 0
|
||||
[[ "$ip" =~ ^192\.168\. ]] && return 0
|
||||
[[ "$ip" =~ ^127\. ]] && return 0
|
||||
[[ "$ip" =~ ^169\.254\. ]] && return 0
|
||||
[[ "$ip" =~ ^fc[0-9a-f]{2}: ]] && return 0
|
||||
[[ "$ip" =~ ^fd[0-9a-f]{2}: ]] && return 0
|
||||
[[ "$ip" =~ ^fe80: ]] && return 0
|
||||
[[ "$ip" == "::1" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
sanitize() {
|
||||
echo "$1" | tr -d '\000-\037' | cut -c1-200
|
||||
}
|
||||
|
||||
# Проверка, входит ли IP в список (поддерживаются CIDR)
|
||||
ip_in_list() {
|
||||
local ip="$1"
|
||||
local list="$2"
|
||||
[[ -z "$list" ]] && return 1
|
||||
for entry in $list; do
|
||||
if [[ "$entry" == */* ]]; then
|
||||
# CIDR
|
||||
if command_exists ipcalc; then
|
||||
if ipcalc -c "$ip" "$entry" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
# грубая проверка: если IP начинается с той же подсети
|
||||
if [[ "$ip" == "${entry%/*}"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
[[ "$ip" == "$entry" ]] && return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Rate limiting: проверяем, можно ли отправить уведомление по ключу (например, IP+user)
|
||||
check_rate_limit() {
|
||||
local key="$1"
|
||||
local limit_sec="${CONFIG[RATE_LIMIT_SEC]}"
|
||||
[[ "$limit_sec" -le 0 ]] && return 0
|
||||
local cache_file="${IP_CACHE_DIR}/rate_$(echo -n "$key" | md5sum | cut -d' ' -f1)"
|
||||
mkdir -p "$IP_CACHE_DIR"
|
||||
if [[ -f "$cache_file" ]]; then
|
||||
local last
|
||||
last="$(cat "$cache_file")"
|
||||
local now
|
||||
now="$(date +%s)"
|
||||
if (( now - last < limit_sec )); then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
date +%s > "$cache_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION LOADING
|
||||
# ============================================================================
|
||||
load_config() {
|
||||
# Сначала загружаем переменные окружения (приоритет выше)
|
||||
local env_vars=(
|
||||
TELEGRAM_TOKEN TELEGRAM_CHAT_ID TELEGRAM_TOPIC_ID
|
||||
MAX_ATTEMPTS_BEFORE_CRITICAL CRITICAL_TIME_WINDOW CLEANUP_DAYS
|
||||
IP_API_TIMEOUT ENABLE_GEOIP ENABLE_NOTIFICATIONS DEBUG
|
||||
AUTO_BLOCK_CRITICAL WHITELIST_IPS BLACKLIST_IPS RATE_LIMIT_SEC
|
||||
)
|
||||
for var in "${env_vars[@]}"; do
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
CONFIG["$var"]="${!var}"
|
||||
log "DEBUG" "Loaded from env: $var=${!var}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Затем из конфигурационного файла (переопределяет, если нет в env)
|
||||
if [[ -f "$CONFIG_FILE" ]]; then
|
||||
log "INFO" "Loading configuration from $CONFIG_FILE"
|
||||
local line key value
|
||||
while IFS='=' read -r key value; do
|
||||
key="${key// /}"
|
||||
value="${value// /}"
|
||||
[[ -z "$key" || "$key" =~ ^# ]] && continue
|
||||
if [[ -n "${CONFIG[$key]:-}" ]]; then
|
||||
CONFIG["$key"]="$value"
|
||||
log "DEBUG" "Config: $key=$value"
|
||||
fi
|
||||
done < "$CONFIG_FILE"
|
||||
else
|
||||
log "WARN" "Configuration file not found, using defaults/env"
|
||||
fi
|
||||
|
||||
# Валидация обязательных параметров
|
||||
if [[ -z "${CONFIG[TELEGRAM_TOKEN]}" || -z "${CONFIG[TELEGRAM_CHAT_ID]}" ]]; then
|
||||
log "ERROR" "TELEGRAM_TOKEN and TELEGRAM_CHAT_ID must be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Создаём необходимые каталоги
|
||||
mkdir -p "$FAILED_LOGINS_DIR" "$IP_CACHE_DIR" "$(dirname "${CONFIG[LOG_FILE]}")"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# GEOIP FUNCTIONS
|
||||
# ============================================================================
|
||||
get_ip_info() {
|
||||
local ip="$1"
|
||||
local cache_file="${IP_CACHE_DIR}/geo_$(echo -n "$ip" | md5sum | cut -d' ' -f1)"
|
||||
local cache_age=3600
|
||||
|
||||
# Проверка кэша
|
||||
if [[ -f "$cache_file" ]]; then
|
||||
local file_time now
|
||||
file_time="$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file")"
|
||||
now="$(date +%s)"
|
||||
if (( now - file_time < cache_age )); then
|
||||
cat "$cache_file"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Если GeoIP отключён
|
||||
if [[ "${CONFIG[ENABLE_GEOIP]}" != "true" ]]; then
|
||||
echo "🌐 *Страна:* отключено"
|
||||
echo "🏙️ *Город:* отключено"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Приватный IP
|
||||
if is_private_ip "$ip"; then
|
||||
{
|
||||
echo "🌐 *Страна:* частная сеть"
|
||||
echo "🏙️ *Город:* локальный адрес"
|
||||
} > "$cache_file"
|
||||
cat "$cache_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Попытка использовать mmdblookup (MaxMind GeoIP2) если установлен
|
||||
if command_exists mmdblookup && [[ -f /usr/share/GeoIP/GeoLite2-City.mmdb ]]; then
|
||||
local country city
|
||||
country="$(mmdblookup --file /usr/share/GeoIP/GeoLite2-City.mmdb --ip "$ip" country names en 2>/dev/null | head -1 | tr -d '"')"
|
||||
city="$(mmdblookup --file /usr/share/GeoIP/GeoLite2-City.mmdb --ip "$ip" city names en 2>/dev/null | head -1 | tr -d '"')"
|
||||
{
|
||||
echo "🌐 *Страна:* ${country:-Неизвестно}"
|
||||
echo "🏙️ *Город:* ${city:-Неизвестно}"
|
||||
} > "$cache_file"
|
||||
cat "$cache_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Резервный API ip-api.com
|
||||
local response
|
||||
response="$(curl -s --max-time "${CONFIG[IP_API_TIMEOUT]}" "http://ip-api.com/json/$ip?fields=status,country,city")" || true
|
||||
if [[ -n "$response" ]] && command_exists jq; then
|
||||
local status country city
|
||||
status="$(jq -r '.status' <<<"$response")"
|
||||
if [[ "$status" == "success" ]]; then
|
||||
country="$(jq -r '.country' <<<"$response")"
|
||||
city="$(jq -r '.city' <<<"$response")"
|
||||
{
|
||||
echo "🌐 *Страна:* ${country:-Неизвестно}"
|
||||
echo "🏙️ *Город:* ${city:-Неизвестно}"
|
||||
} > "$cache_file"
|
||||
cat "$cache_file"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Если ничего не сработало
|
||||
echo "🌐 *Страна:* не определена"
|
||||
echo "🏙️ *Город:* не определён"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# IP FILTERING (WHITELIST/BLACKLIST)
|
||||
# ============================================================================
|
||||
check_ip_filter() {
|
||||
local ip="$1"
|
||||
if ip_in_list "$ip" "${CONFIG[BLACKLIST_IPS]}"; then
|
||||
log "INFO" "IP $ip is blacklisted, blocking immediately"
|
||||
auto_block_ip "$ip" "blacklist"
|
||||
return 1
|
||||
fi
|
||||
if ip_in_list "$ip" "${CONFIG[WHITELIST_IPS]}"; then
|
||||
log "DEBUG" "IP $ip is whitelisted, skipping notifications"
|
||||
return 2
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# AUTO BLOCK IP
|
||||
# ============================================================================
|
||||
auto_block_ip() {
|
||||
local ip="$1"
|
||||
local reason="$2"
|
||||
if [[ "${CONFIG[AUTO_BLOCK_CRITICAL]}" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local cmd="${CONFIG[BLOCK_COMMAND]//\{ip\}/$ip}"
|
||||
log "WARN" "Auto-blocking IP $ip (reason: $reason) with command: $cmd"
|
||||
eval "$cmd" || log "ERROR" "Failed to block IP $ip"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# FAILED LOGIN TRACKING
|
||||
# ============================================================================
|
||||
track_failed_attempt() {
|
||||
local ip="$1"
|
||||
local user="$2"
|
||||
local timestamp
|
||||
timestamp="$(date +%s)"
|
||||
local date_str
|
||||
date_str="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
local safe_ip
|
||||
safe_ip="$(echo "$ip" | tr './:' '_')"
|
||||
local ip_file="$FAILED_LOGINS_DIR/$safe_ip"
|
||||
|
||||
# Читаем предыдущие данные
|
||||
local attempts=1 last_attempt=0 old_user="$user"
|
||||
if [[ -f "$ip_file" ]]; then
|
||||
IFS='|' read -r attempts last_attempt old_user _ < "$ip_file"
|
||||
attempts=$((attempts + 1))
|
||||
# Сбрасываем счётчик, если прошло много времени
|
||||
if (( timestamp - last_attempt > 600 )); then
|
||||
attempts=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Сохраняем
|
||||
echo "$attempts|$timestamp|$user|$date_str" > "$ip_file"
|
||||
log "INFO" "Failed attempt #$attempts from $ip as $user"
|
||||
|
||||
# Проверяем условия для уведомления
|
||||
local notify=false
|
||||
local alert_level="info"
|
||||
local time_window_diff=$((timestamp - last_attempt))
|
||||
|
||||
# Критическая атака
|
||||
if (( attempts >= ${CONFIG[MAX_ATTEMPTS_BEFORE_CRITICAL]} )) && \
|
||||
(( time_window_diff < ${CONFIG[CRITICAL_TIME_WINDOW]} )); then
|
||||
notify=true
|
||||
alert_level="critical"
|
||||
auto_block_ip "$ip" "critical_attack"
|
||||
elif (( attempts == 1 || attempts == 3 || attempts == 5 || attempts % 10 == 0 )); then
|
||||
notify=true
|
||||
alert_level="warning"
|
||||
fi
|
||||
|
||||
if [[ "$notify" == "true" ]]; then
|
||||
send_failed_login_alert "$ip" "$user" "$attempts" "$date_str" "$alert_level"
|
||||
fi
|
||||
|
||||
return "$attempts"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# TELEGRAM SENDER
|
||||
# ============================================================================
|
||||
send_telegram_message() {
|
||||
local message="$1"
|
||||
local disable_notification="${2:-false}"
|
||||
local parse_mode="${3:-markdown}"
|
||||
|
||||
if [[ "${CONFIG[ENABLE_NOTIFICATIONS]}" != "true" ]]; then
|
||||
log "INFO" "Notifications disabled, message not sent"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local url="https://api.telegram.org/bot${CONFIG[TELEGRAM_TOKEN]}/sendMessage"
|
||||
local response_file="/tmp/tg_response_$$.json"
|
||||
|
||||
local curl_cmd=(curl -s -X POST "$url" -F "chat_id=${CONFIG[TELEGRAM_CHAT_ID]}" -F "text=$message" -F "parse_mode=$parse_mode" -F "disable_notification=$disable_notification")
|
||||
if [[ -n "${CONFIG[TELEGRAM_TOPIC_ID]}" ]]; then
|
||||
curl_cmd+=(-F "message_thread_id=${CONFIG[TELEGRAM_TOPIC_ID]}")
|
||||
fi
|
||||
|
||||
local http_code
|
||||
http_code="$("${curl_cmd[@]}" -w "%{http_code}" -o "$response_file" 2>/dev/null || echo "000")"
|
||||
|
||||
if [[ "$http_code" -eq 200 ]]; then
|
||||
log "DEBUG" "Telegram message sent"
|
||||
else
|
||||
log "ERROR" "Telegram send failed (HTTP $http_code). Response: $(cat "$response_file" 2>/dev/null)"
|
||||
fi
|
||||
rm -f "$response_file"
|
||||
}
|
||||
|
||||
send_failed_login_alert() {
|
||||
local ip="$1"
|
||||
local user="$2"
|
||||
local attempts="$3"
|
||||
local date_str="$4"
|
||||
local alert_level="$5"
|
||||
|
||||
# Rate limiting
|
||||
local rate_key="failed_${ip}_${user}"
|
||||
check_rate_limit "$rate_key" || return 0
|
||||
|
||||
local ip_info
|
||||
ip_info="$(get_ip_info "$ip")"
|
||||
|
||||
local danger_emoji title
|
||||
case "$alert_level" in
|
||||
critical)
|
||||
danger_emoji="${EMOJI[FIRE]}${EMOJI[CRITICAL]}"
|
||||
title="*КРИТИЧЕСКАЯ АТАКА!*"
|
||||
;;
|
||||
warning)
|
||||
if (( attempts >= 10 )); then
|
||||
danger_emoji="${EMOJI[DANGER]}⚠️"
|
||||
title="*ВЫСОКАЯ АКТИВНОСТЬ*"
|
||||
else
|
||||
danger_emoji="${EMOJI[WARNING]}"
|
||||
title=""
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
danger_emoji="❌"
|
||||
title=""
|
||||
;;
|
||||
esac
|
||||
|
||||
local message
|
||||
message="$(cat << EOF
|
||||
${danger_emoji} *НЕУДАЧНАЯ ПОПЫТКА SSH* ${danger_emoji}
|
||||
${title}
|
||||
|
||||
⚠️ *Попытка:* #${attempts}
|
||||
👤 *Пользователь:* \`${user}\`
|
||||
🖥️ *Сервер:* \`${HOSTNAME:-$(hostname)}\`
|
||||
🔗 *IP атакующего:* \`${ip}\`
|
||||
|
||||
*Геоинформация:*
|
||||
${ip_info}
|
||||
|
||||
🕐 *Время:* ${date_str}
|
||||
📊 *Всего попыток с IP:* ${attempts}
|
||||
|
||||
#SSH #FailedLogin #SecurityAlert
|
||||
EOF
|
||||
)"
|
||||
send_telegram_message "$message" "false"
|
||||
}
|
||||
|
||||
send_successful_login() {
|
||||
local ip="$1"
|
||||
local user="$2"
|
||||
|
||||
local rate_key="success_${ip}_${user}"
|
||||
check_rate_limit "$rate_key" || return 0
|
||||
|
||||
local ip_info
|
||||
ip_info="$(get_ip_info "$ip")"
|
||||
|
||||
local message
|
||||
message="$(cat << EOF
|
||||
✅ *УСПЕШНЫЙ ВХОД SSH*
|
||||
|
||||
👤 *Пользователь:* \`${user}\`
|
||||
🖥️ *Сервер:* \`${HOSTNAME:-$(hostname)}\`
|
||||
🔗 *IP адрес:* \`${ip}\`
|
||||
|
||||
*Информация о подключении:*
|
||||
${ip_info}
|
||||
|
||||
🕐 *Время входа:* $(date '+%d %b %Y, %H:%M:%S')
|
||||
📡 *Сервис:* ${PAM_SERVICE:-ssh}
|
||||
|
||||
#SSH #Login #Successful
|
||||
EOF
|
||||
)"
|
||||
send_telegram_message "$message" "true"
|
||||
}
|
||||
|
||||
send_logout_notification() {
|
||||
local ip="$1"
|
||||
local user="$2"
|
||||
local session_start="${3:-}"
|
||||
|
||||
local duration="?"
|
||||
if [[ -n "$session_start" ]]; then
|
||||
local start_sec end_sec diff
|
||||
start_sec="$(date -d "$session_start" +%s 2>/dev/null || echo 0)"
|
||||
end_sec="$(date +%s)"
|
||||
if (( start_sec > 0 )); then
|
||||
diff=$((end_sec - start_sec))
|
||||
duration="$(printf '%02d:%02d:%02d' $((diff/3600)) $(((diff%3600)/60)) $((diff%60)))"
|
||||
fi
|
||||
fi
|
||||
|
||||
local message
|
||||
message="$(cat << EOF
|
||||
🚪 *ВЫХОД ИЗ SSH СЕССИИ*
|
||||
|
||||
👤 *Пользователь:* \`${user}\`
|
||||
🖥️ *Сервер:* \`${HOSTNAME:-$(hostname)}\`
|
||||
🔗 *IP адрес:* \`${ip}\`
|
||||
|
||||
🕐 *Время выхода:* $(date '+%d %b %Y, %H:%M:%S')
|
||||
⏱️ *Длительность:* $duration
|
||||
|
||||
#SSH #Logout
|
||||
EOF
|
||||
)"
|
||||
send_telegram_message "$message" "true"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# LOG MONITORING (режим демона)
|
||||
# ============================================================================
|
||||
monitor_logs() {
|
||||
log "INFO" "Starting log monitor"
|
||||
|
||||
local use_journal="${CONFIG[USE_JOURNAL]}"
|
||||
if [[ "$use_journal" == "true" ]] && command_exists journalctl; then
|
||||
journalctl -f -n0 -u ssh.service -o cat | while read -r line; do
|
||||
process_log_line "$line"
|
||||
done
|
||||
else
|
||||
local auth_logs=("/var/log/auth.log" "/var/log/secure")
|
||||
local log_file=""
|
||||
for f in "${auth_logs[@]}"; do
|
||||
if [[ -f "$f" ]]; then
|
||||
log_file="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -z "$log_file" ]]; then
|
||||
log "ERROR" "No auth log file found"
|
||||
return 1
|
||||
fi
|
||||
log "INFO" "Monitoring $log_file"
|
||||
tail -Fn0 "$log_file" | while read -r line; do
|
||||
process_log_line "$line"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
process_log_line() {
|
||||
local line="$1"
|
||||
local ip user
|
||||
|
||||
# Failed password
|
||||
if [[ "$line" =~ Failed\ password\ for\ (.+)\ from\ ([0-9\.]+) ]]; then
|
||||
user="${BASH_REMATCH[1]}"
|
||||
ip="${BASH_REMATCH[2]}"
|
||||
check_ip_filter "$ip" || return 0
|
||||
track_failed_attempt "$ip" "$user"
|
||||
# Invalid user
|
||||
elif [[ "$line" =~ Invalid\ user\ ([^\ ]+)\ from\ ([0-9\.]+) ]]; then
|
||||
user="${BASH_REMATCH[1]}"
|
||||
ip="${BASH_REMATCH[2]}"
|
||||
check_ip_filter "$ip" || return 0
|
||||
track_failed_attempt "$ip" "$user"
|
||||
# Successful login
|
||||
elif [[ "$line" =~ Accepted\ password\ for\ ([^\ ]+)\ from\ ([0-9\.]+) ]]; then
|
||||
user="${BASH_REMATCH[1]}"
|
||||
ip="${BASH_REMATCH[2]}"
|
||||
check_ip_filter "$ip" || return 0
|
||||
send_successful_login "$ip" "$user"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# PAM HANDLER (вызывается из PAM)
|
||||
# ============================================================================
|
||||
handle_pam() {
|
||||
local pam_type="${PAM_TYPE:-}"
|
||||
local pam_user="${PAM_USER:-}"
|
||||
local pam_rhost="${PAM_RHOST:-}"
|
||||
local pam_service="${PAM_SERVICE:-}"
|
||||
|
||||
[[ -z "$pam_type" || -z "$pam_user" || -z "$pam_rhost" ]] && return 0
|
||||
|
||||
# Фильтрация
|
||||
check_ip_filter "$pam_rhost" || return 0
|
||||
|
||||
log "INFO" "PAM event: $pam_type from $pam_rhost as $pam_user"
|
||||
|
||||
case "$pam_type" in
|
||||
open_session)
|
||||
send_successful_login "$pam_rhost" "$pam_user"
|
||||
;;
|
||||
close_session)
|
||||
send_logout_notification "$pam_rhost" "$pam_user"
|
||||
;;
|
||||
auth)
|
||||
# Обрабатывается через логи, игнорируем
|
||||
;;
|
||||
*)
|
||||
log "DEBUG" "Unknown PAM type: $pam_type"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# MAINTENANCE
|
||||
# ============================================================================
|
||||
cleanup() {
|
||||
log "INFO" "Cleaning up old files"
|
||||
find "$FAILED_LOGINS_DIR" -type f -mtime "+${CONFIG[CLEANUP_DAYS]}" -delete
|
||||
find "$IP_CACHE_DIR" -type f -mtime +1 -delete
|
||||
log "INFO" "Cleanup done"
|
||||
}
|
||||
|
||||
status() {
|
||||
echo "=== Telegram SSH Notifier Status ==="
|
||||
echo "Version: $SCRIPT_VERSION"
|
||||
echo "Log file: ${CONFIG[LOG_FILE]}"
|
||||
echo "Failed logins dir: $FAILED_LOGINS_DIR"
|
||||
echo "Notifications: ${CONFIG[ENABLE_NOTIFICATIONS]}"
|
||||
echo "GeoIP: ${CONFIG[ENABLE_GEOIP]}"
|
||||
echo "Auto-block: ${CONFIG[AUTO_BLOCK_CRITICAL]}"
|
||||
echo ""
|
||||
local count
|
||||
count="$(find "$FAILED_LOGINS_DIR" -type f 2>/dev/null | wc -l)"
|
||||
echo "Tracked IPs with failed attempts: $count"
|
||||
echo ""
|
||||
echo "Recent failed attempts (last 10):"
|
||||
find "$FAILED_LOGINS_DIR" -type f -exec ls -lt {} + 2>/dev/null | head -10 | while read -r line; do
|
||||
local file
|
||||
file="$(echo "$line" | awk '{print $NF}')"
|
||||
if [[ -f "$file" ]]; then
|
||||
IFS='|' read -r attempts _ user date < "$file"
|
||||
local ip_name
|
||||
ip_name="$(basename "$file" | tr '_' '.')"
|
||||
echo " $ip_name: $attempts attempts, last: $date, user: $user"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# COMMAND LINE PARSING
|
||||
# ============================================================================
|
||||
usage() {
|
||||
cat << EOF
|
||||
Telegram SSH Notifier v$SCRIPT_VERSION
|
||||
|
||||
Использование: $SCRIPT_NAME [КОМАНДА]
|
||||
|
||||
Команды:
|
||||
monitor Запустить мониторинг логов (демон)
|
||||
cleanup Очистить старые файлы и кэш
|
||||
status Показать статус и статистику
|
||||
test Отправить тестовое сообщение
|
||||
--help, -h Показать эту справку
|
||||
--version, -v Показать версию
|
||||
|
||||
Без аргументов работает в режиме PAM (для интеграции с /etc/pam.d/sshd).
|
||||
|
||||
Конфигурация: $CONFIG_FILE
|
||||
Лог: ${CONFIG[LOG_FILE]}
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
main() {
|
||||
# Предотвращаем повторный запуск в режиме monitor
|
||||
if [[ "${1:-}" == "monitor" ]]; then
|
||||
if [[ -f "$LOCK_FILE" ]]; then
|
||||
if kill -0 "$(cat "$LOCK_FILE")" 2>/dev/null; then
|
||||
log "ERROR" "Another monitor process is already running (PID $(cat "$LOCK_FILE")). Exiting."
|
||||
exit 1
|
||||
else
|
||||
rm -f "$LOCK_FILE"
|
||||
fi
|
||||
fi
|
||||
echo $$ > "$LOCK_FILE"
|
||||
trap 'rm -f "$LOCK_FILE"' EXIT
|
||||
fi
|
||||
|
||||
load_config
|
||||
|
||||
case "${1:-}" in
|
||||
monitor)
|
||||
monitor_logs
|
||||
;;
|
||||
cleanup)
|
||||
cleanup
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
test)
|
||||
send_telegram_message "*Тестовое сообщение от SSH Notifier*" "true"
|
||||
echo "Тестовое сообщение отправлено."
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
--version|-v)
|
||||
echo "$SCRIPT_VERSION"
|
||||
;;
|
||||
"")
|
||||
# PAM mode
|
||||
handle_pam
|
||||
;;
|
||||
*)
|
||||
echo "Неизвестная команда: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Защита от source
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
trap 'log "ERROR" "Script failed at line $LINENO"' ERR
|
||||
main "$@"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user