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 nano /etc/telegram-ssh-notify.conf
|
||||||
> sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm<br />
|
|
||||||
> sudo yum install jq
|
|
||||||
|
|
||||||
Установка :
|
|
||||||
Копируем скрипт в /usr/local/bin/
|
|
||||||
> wget -P /usr/local/bin/ https://raw.githubusercontent.com/unixhostpro/ssh-login-notification/master/ssh_login_info.sh
|
|
||||||
|
|
||||||
Устанавливаем права на запуск
|
TELEGRAM_TOKEN=
|
||||||
> chmod +x /usr/local/bin/ssh_login_info.sh
|
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/sshd в редакторе (например, nano или vim):
|
||||||
В файл /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
|
|
||||||
|
|
||||||
CentOS
|
sudo nano /etc/pam.d/sshd
|
||||||
В файл /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
|
|
||||||
|
|
||||||
|
Добавьте в конец файла строку:
|
||||||
|
|
||||||
|
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
|
#!/usr/bin/env bash
|
||||||
# Telegram notification
|
# Telegram SSH Notifications Script
|
||||||
# Send msg when your server load to high
|
# Version: 3.0.0
|
||||||
token="123456:AasdE8asdaKNiradb1wRZT87pwErerc6biTsVcPE" # put your token here
|
# Author: System Administrator
|
||||||
chat_id="1234567" # your chat_id for sending notification
|
# Description: Уведомления о SSH-входах и попытках взлома через Telegram
|
||||||
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
|
set -euo pipefail
|
||||||
date="$(date "+%d-%b-%Y-%H:%M")"
|
IFS=$'\n\t'
|
||||||
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
|
# CONSTANTS
|
||||||
country=$(cat $caption_file | jq '.country' | sed 's/"//g')
|
# ============================================================================
|
||||||
city=$(cat $caption_file | jq '.city' | sed 's/"//g')
|
readonly SCRIPT_NAME="$(basename "${0}")"
|
||||||
org=$(cat $caption_file | jq '.as' | sed 's/"//g')
|
readonly SCRIPT_VERSION="3.0.0"
|
||||||
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
|
readonly CONFIG_FILE="/etc/telegram-ssh-notify.conf"
|
||||||
#curl -d text=$message -d chat_id=$chat_id $sendmsg
|
readonly DEFAULT_LOG_FILE="/var/log/telegram-ssh-notify.log"
|
||||||
curl $sendmsg -d chat_id=$chat_id -d text="$(<$msg)"
|
readonly FAILED_LOGINS_DIR="/var/log/ssh_failed_logins"
|
||||||
rm /tmp/ssh_caption_file.txt
|
readonly IP_CACHE_DIR="/tmp/telegram-ssh-notify-cache"
|
||||||
rm /tmp/ssh_msg_info.txt
|
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