feat: мониторинг systemd-logind (journalctl), конфиг и антидубль SSH
- Опрос journalctl -u systemd-logind, LAST_LOGIND_CHECK_FILE, LOGIND_* - README: параметры logind, ротация LAST_*, journalctl Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# ssh-monitor
|
||||
|
||||
Bash-скрипт мониторинга SSH/SUDO с уведомлениями в Telegram, опциональным резервным webhook, авто-блокировкой IP (IPv4 через `iptables`, IPv6 через `ip6tables` при наличии) и ежедневным отчётом.
|
||||
Bash-скрипт мониторинга **SSH**, **SUDO** и событий **`systemd-logind`** (локальные/графические сессии и др.) с уведомлениями в Telegram, опциональным резервным webhook, авто-блокировкой IP (IPv4 через `iptables`, IPv6 через `ip6tables` при наличии) и ежедневным отчётом.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
@@ -18,7 +18,8 @@ Bash-скрипт мониторинга SSH/SUDO с уведомлениями
|
||||
|
||||
- `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`
|
||||
- `BACKUP_WEBHOOK_URL` — резервная доставка JSON `{"text":"..."}` (например Slack Incoming Webhook), если Telegram недоступен или вернул ошибку
|
||||
- `LOG_FILE`, `LAST_HEARTBEAT_FILE`, `LAST_REPORT_FILE`, `LAST_SSH_CHECK_FILE`, `LAST_SUDO_CHECK_FILE`, `LAST_SECURITY_EVENTS_FILE`, `BAN_LIST_FILE`
|
||||
- `LOG_FILE`, `LAST_HEARTBEAT_FILE`, `LAST_REPORT_FILE`, `LAST_SSH_CHECK_FILE`, `LAST_SUDO_CHECK_FILE`, `LAST_SECURITY_EVENTS_FILE`, `LAST_LOGIND_CHECK_FILE`, `BAN_LIST_FILE`
|
||||
- `ENABLE_LOGIND_MONITOR`, `LOGIND_NOTIFY_NEW`, `LOGIND_NOTIFY_REMOVED`, `LOGIND_NOTIFY_FAILED`, `LOGIND_SKIP_REMOTE` — см. раздел «Мониторинг systemd-logind» ниже
|
||||
- `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_TZ` (опционально), `DAILY_REPORT_TOP_IPS`
|
||||
- `BRUTE_WINDOW_SEC`, `BRUTE_MIN_FAILS`, `BRUTE_NOTIFY_COOLDOWN_SEC`
|
||||
- `PROMETHEUS_TEXTFILE_DIR` — каталог для `ssh_monitor.prom` (совместимость с node_exporter textfile collector)
|
||||
@@ -40,7 +41,13 @@ Bash-скрипт мониторинга SSH/SUDO с уведомлениями
|
||||
- `LAST_SSH_CHECK_FILE` — файл с меткой времени последней проверки SSH-событий.
|
||||
- `LAST_SUDO_CHECK_FILE` — файл с меткой времени последней проверки sudo-событий.
|
||||
- `LAST_SECURITY_EVENTS_FILE` — метка последней проверки «тяжёлых» событий безопасности в журнале.
|
||||
- `LAST_LOGIND_CHECK_FILE` — метка последней обработки журнала `systemd-logind` (unix-время); при первом запуске подтягивается около 30 минут истории.
|
||||
- `BAN_LIST_FILE` — файл состояния банов (IP, время окончания бана и метаданные).
|
||||
- `ENABLE_LOGIND_MONITOR` — `1` включает опрос `journalctl -u systemd-logind`, `0` полностью отключает этот блок.
|
||||
- `LOGIND_NOTIFY_NEW` — `1` отправлять Telegram при появлении новой сессии в logind (строки вида *New session … of user …*).
|
||||
- `LOGIND_NOTIFY_REMOVED` — `1` отправлять Telegram при завершении сессии (*Removed session …*); по умолчанию `0` (только запись в `LOG_FILE`, чтобы не заспамить канал).
|
||||
- `LOGIND_NOTIFY_FAILED` — `1` отправлять Telegram по строкам logind, содержащим *failed* (широкий фильтр; при необходимости отключите).
|
||||
- `LOGIND_SKIP_REMOTE` — `1` (по умолчанию): для новой сессии, если доступен `loginctl`, запрашивается тип сессии; при `Type=ssh` **второе** уведомление в Telegram не отправляется (успешный SSH уже покрывает `monitor_ssh`), в лог пишется пояснение. Поставьте `0`, если нужны отдельные алерты logind и для SSH-сессий.
|
||||
- `DAILY_REPORT_HOUR` — час (0..23), после наступления которого в **текущих календарных сутках** (в выбранной ниже зоне) отправляется не более одного ежедневного отчёта.
|
||||
- `DAILY_REPORT_TZ` — необязательная **IANA**-зона (`Europe/Moscow`, `Asia/Yekaterinburg`, …). Если **пусто**, для отчёта используется та же зона, что и у команды `date` у процесса монитора (как правило, совпадает с `timedatectl` / `/etc/localtime` на сервере). Если сервис запускается с `TZ=UTC` в unit-файле, без `DAILY_REPORT_TZ` отчёт ориентируется на **UTC** — тогда задайте явную зону в конфиге.
|
||||
- `DAILY_REPORT_TOP_IPS` — сколько IP показывать в топе неудачных попыток за 24 часа.
|
||||
@@ -60,6 +67,15 @@ Bash-скрипт мониторинга SSH/SUDO с уведомлениями
|
||||
- `WATCHDOG_SERVICE_NAME` — имя systemd-сервиса, который контролирует watchdog (по умолчанию `ssh-monitor.service`).
|
||||
- `WATCHDOG_NOTIFY_ON_RECOVERY` — `1` включает служебные сообщения watchdog при штатном состоянии, `0` отключает.
|
||||
|
||||
### Мониторинг systemd-logind
|
||||
|
||||
- Собираются сообщения юнита **`systemd-logind`** через **`journalctl`** (`-o cat`): новые и завершённые сессии, а также строки с подстрокой *failed* (если включено).
|
||||
- Без **`journalctl`** на хосте этот блок **не работает** (как и часть других функций, завязанных на journal).
|
||||
- Для разбора «новой сессии» используются типичные англоязычные форматы (`New session … of user …`). При несовпадении формата строка всё равно попадёт в лог с пометкой о неудачном разборе.
|
||||
- **`loginctl`** нужен только для **`LOGIND_SKIP_REMOTE`**: определяется `Type` сессии; для удалённого SSH обычно `ssh`, чтобы не дублировать уведомление с блоком мониторинга SSH.
|
||||
|
||||
Команда **`ssh-monitor --check-config`** выводит актуальные значения, в том числе параметры logind.
|
||||
|
||||
## Режимы запуска
|
||||
|
||||
Обычный запуск (root):
|
||||
@@ -129,12 +145,14 @@ sudo journalctl -u ssh-monitor -f
|
||||
|
||||
## Ротация логов
|
||||
|
||||
Пример для `logrotate` лежит в репозитории: `contrib/logrotate.d/ssh-monitor`. Скопируйте файл в `/etc/logrotate.d/` и при необходимости поправьте пути под ваши `LOG_FILE` / `WATCHDOG_LOG_FILE`.
|
||||
Пример для `logrotate` лежит в репозитории: `contrib/logrotate.d/ssh-monitor`. Скопируйте файл в `/etc/logrotate.d/` и при необходимости поправьте пути под ваши `LOG_FILE` / `WATCHDOG_LOG_FILE` и другие файлы состояния в `/var/log/` (в т.ч. `LAST_*`, если решите их ротировать отдельно).
|
||||
|
||||
## Статистика без systemd (`journalctl`)
|
||||
|
||||
Для корректного подсчёта событий за последние 24 часа по файлам `/var/log/auth.log*` рекомендуется наличие **Python 3** на сервере. При его отсутствии счётчики в ежедневном отчёте для «классического» syslog могут быть занижены (используется безопасный fallback).
|
||||
|
||||
**Мониторинг `systemd-logind`** и другие части, использующие **`journalctl`**, на таких системах **не выполняются** или дают неполные данные — ориентируйтесь на окружение с **systemd** и доступным журналом.
|
||||
|
||||
## Watchdog (автоматическое восстановление)
|
||||
|
||||
Watchdog проверяет:
|
||||
|
||||
+147
-6
@@ -26,6 +26,15 @@ DAILY_REPORT_TZ=""
|
||||
LAST_SSH_CHECK_FILE="/var/log/last_ssh_check"
|
||||
LAST_SUDO_CHECK_FILE="/var/log/last_sudo_check"
|
||||
LAST_SECURITY_EVENTS_FILE="/var/log/last_security_events_check"
|
||||
LAST_LOGIND_CHECK_FILE="/var/log/last_logind_check"
|
||||
|
||||
# systemd-logind (консоль, графика, seat; SSH тоже создаёт сессии — см. LOGIND_SKIP_REMOTE)
|
||||
ENABLE_LOGIND_MONITOR=1
|
||||
LOGIND_NOTIFY_NEW=1
|
||||
LOGIND_NOTIFY_REMOVED=0
|
||||
LOGIND_NOTIFY_FAILED=1
|
||||
# 1 = не слать Telegram для новых сессий с типом ssh (уведомление уже идёт из monitor_ssh)
|
||||
LOGIND_SKIP_REMOTE=1
|
||||
|
||||
# Эксплуатация / health
|
||||
PROMETHEUS_TEXTFILE_DIR=""
|
||||
@@ -125,6 +134,12 @@ load_config() {
|
||||
local cfg_last_ssh_check_file="${LAST_SSH_CHECK_FILE}"
|
||||
local cfg_last_sudo_check_file="${LAST_SUDO_CHECK_FILE}"
|
||||
local cfg_last_security_events_file="${LAST_SECURITY_EVENTS_FILE}"
|
||||
local cfg_last_logind_check_file="${LAST_LOGIND_CHECK_FILE}"
|
||||
local cfg_enable_logind_monitor="${ENABLE_LOGIND_MONITOR}"
|
||||
local cfg_logind_notify_new="${LOGIND_NOTIFY_NEW}"
|
||||
local cfg_logind_notify_removed="${LOGIND_NOTIFY_REMOVED}"
|
||||
local cfg_logind_notify_failed="${LOGIND_NOTIFY_FAILED}"
|
||||
local cfg_logind_skip_remote="${LOGIND_SKIP_REMOTE}"
|
||||
local cfg_prometheus_textfile_dir="${PROMETHEUS_TEXTFILE_DIR}"
|
||||
local cfg_healthcheck_status_file="${HEALTHCHECK_STATUS_FILE}"
|
||||
local cfg_brute_window_sec="${BRUTE_WINDOW_SEC}"
|
||||
@@ -154,6 +169,12 @@ load_config() {
|
||||
LAST_SSH_CHECK_FILE="${LAST_SSH_CHECK_FILE:-$cfg_last_ssh_check_file}"
|
||||
LAST_SUDO_CHECK_FILE="${LAST_SUDO_CHECK_FILE:-$cfg_last_sudo_check_file}"
|
||||
LAST_SECURITY_EVENTS_FILE="${LAST_SECURITY_EVENTS_FILE:-$cfg_last_security_events_file}"
|
||||
LAST_LOGIND_CHECK_FILE="${LAST_LOGIND_CHECK_FILE:-$cfg_last_logind_check_file}"
|
||||
ENABLE_LOGIND_MONITOR="${ENABLE_LOGIND_MONITOR:-$cfg_enable_logind_monitor}"
|
||||
LOGIND_NOTIFY_NEW="${LOGIND_NOTIFY_NEW:-$cfg_logind_notify_new}"
|
||||
LOGIND_NOTIFY_REMOVED="${LOGIND_NOTIFY_REMOVED:-$cfg_logind_notify_removed}"
|
||||
LOGIND_NOTIFY_FAILED="${LOGIND_NOTIFY_FAILED:-$cfg_logind_notify_failed}"
|
||||
LOGIND_SKIP_REMOTE="${LOGIND_SKIP_REMOTE:-$cfg_logind_skip_remote}"
|
||||
PROMETHEUS_TEXTFILE_DIR="${PROMETHEUS_TEXTFILE_DIR:-$cfg_prometheus_textfile_dir}"
|
||||
HEALTHCHECK_STATUS_FILE="${HEALTHCHECK_STATUS_FILE:-$cfg_healthcheck_status_file}"
|
||||
BAN_LIST_FILE="${BAN_LIST_FILE:-$cfg_ban_list_file}"
|
||||
@@ -232,17 +253,15 @@ accepted = failed = sudo_cmd = 0
|
||||
failed_by_ip = collections.Counter()
|
||||
|
||||
line_re = re.compile(
|
||||
r"^(?P<mon>[A-Za-z]{3})\s+(?P<day>\d{1,2})\s+(?P<hms>\d{2}:\d{2}:\d{2})\s+"
|
||||
r"(?P<rest>.+)$"
|
||||
r"^(?P [A-Za-z]{3})\s+(?P \d{1,2})\s+(?P \d{2}:\d{2}:\d{2})\s+"
|
||||
r"(?P.+)$"
|
||||
)
|
||||
|
||||
|
||||
def open_any(path):
|
||||
if str(path).endswith(".gz"):
|
||||
return gzip.open(path, "rt", errors="ignore")
|
||||
return path.open("r", errors="ignore")
|
||||
|
||||
|
||||
def parse_ts(mon: str, day: int, hms: str):
|
||||
m = MONTHS.get(mon)
|
||||
if not m:
|
||||
@@ -259,7 +278,6 @@ def parse_ts(mon: str, day: int, hms: str):
|
||||
dt = dt.replace(year=year + 1)
|
||||
return dt
|
||||
|
||||
|
||||
ip_from_failed = re.compile(r" from ([^ ]+) port ")
|
||||
ip_from_accepted = re.compile(r" from ([^ ]+) port ")
|
||||
|
||||
@@ -927,6 +945,125 @@ monitor_security_events() {
|
||||
echo "$now" >"$LAST_SECURITY_EVENTS_FILE"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Локальные сессии (systemd-logind, journalctl)
|
||||
# ============================================
|
||||
|
||||
logind_session_type() {
|
||||
local sid="$1"
|
||||
[ -z "$sid" ] && echo "" && return 0
|
||||
if ! command -v loginctl >/dev/null 2>&1; then
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
loginctl show-session "$sid" -p Type 2>/dev/null | sed -n 's/^Type=//p' | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
parse_logind_new_session() {
|
||||
local line="$1"
|
||||
local -n out_sid="$2"
|
||||
local -n out_user="$3"
|
||||
out_sid=""
|
||||
out_user=""
|
||||
if [[ "$line" =~ [Nn]ew\ session[[:space:]]+([^[:space:]]+)[[:space:]]+of\ user\ \'([^\']+)\' ]]; then
|
||||
out_sid="${BASH_REMATCH[1]}"
|
||||
out_user="${BASH_REMATCH[2]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$line" =~ [Nn]ew\ session[[:space:]]+([^[:space:]]+)[[:space:]]+of\ user\ ([^[:space:].]+)\.? ]]; then
|
||||
out_sid="${BASH_REMATCH[1]}"
|
||||
out_user="${BASH_REMATCH[2]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
monitor_logind() {
|
||||
if [ "${ENABLE_LOGIND_MONITOR:-1}" != "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! command -v journalctl >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local last_check="0"
|
||||
if [ -f "$LAST_LOGIND_CHECK_FILE" ]; then
|
||||
last_check="$(<"$LAST_LOGIND_CHECK_FILE")"
|
||||
fi
|
||||
if ! [[ "$last_check" =~ ^[0-9]+$ ]]; then
|
||||
last_check=0
|
||||
fi
|
||||
|
||||
local since_journal
|
||||
if [ "$last_check" -eq 0 ]; then
|
||||
since_journal="--since=-30m"
|
||||
else
|
||||
since_journal="--since=@${last_check}"
|
||||
fi
|
||||
|
||||
local current_time
|
||||
current_time=$(date '+%s')
|
||||
|
||||
local logind_lines
|
||||
logind_lines=$(journalctl -u systemd-logind $since_journal --until="now" -o cat --no-pager 2>/dev/null || true)
|
||||
|
||||
if [ -n "$logind_lines" ]; then
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
local low
|
||||
low=$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
if [[ "$low" == *"new session"* ]]; then
|
||||
if [ "${LOGIND_NOTIFY_NEW:-1}" = "1" ]; then
|
||||
local sid="" user=""
|
||||
if parse_logind_new_session "$line" sid user; then
|
||||
if [ "${LOGIND_SKIP_REMOTE:-1}" = "1" ] && [ -n "$sid" ]; then
|
||||
local st
|
||||
st=$(logind_session_type "$sid")
|
||||
if [ "$st" = "ssh" ]; then
|
||||
write_log "LOGIND: новая сессия (тип ssh, уведомление от ssh-monitor): ${line}"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
local msg=" 🖥️ НОВАЯ СЕССИЯ (systemd-logind) "$'\n'
|
||||
msg="${msg}👤 Пользователь: ${user:-неизвестно}"$'\n'
|
||||
msg="${msg}🆔 Сессия: ${sid:-?}"$'\n'
|
||||
msg="${msg}🕐 Время: $(date '+%d.%m.%Y %H:%M:%S')"
|
||||
notify_send "$msg"
|
||||
write_log "LOGIND: новая сессия: ${line}"
|
||||
else
|
||||
write_log "LOGIND: новая сессия (разбор user/sid не удался): ${line}"
|
||||
fi
|
||||
fi
|
||||
|
||||
elif [[ "$low" == *"removed session"* ]]; then
|
||||
write_log "LOGIND: сессия завершена: $line"
|
||||
if [ "${LOGIND_NOTIFY_REMOVED:-0}" = "1" ]; then
|
||||
local sess_r=""
|
||||
if [[ "$line" =~ [Rr]emoved\ session[[:space:]]+([^[:space:].]+) ]]; then
|
||||
sess_r="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
local msg_r=" 🚪 СЕССИЯ ЗАКРЫТА (systemd-logind) "$'\n'
|
||||
msg_r="${msg_r}🆔 Сессия: ${sess_r:-?}"$'\n'
|
||||
msg_r="${msg_r}🕐 Время: $(date '+%d.%m.%Y %H:%M:%S')"
|
||||
notify_send "$msg_r"
|
||||
fi
|
||||
|
||||
elif [[ "$low" == *"failed"* ]]; then
|
||||
if [ "${LOGIND_NOTIFY_FAILED:-1}" = "1" ]; then
|
||||
local msg_f=" ❌ СБОЙ (systemd-logind) "$'\n'
|
||||
msg_f="${msg_f}📝 ${line}"$'\n'
|
||||
msg_f="${msg_f}🕐 Время: $(date '+%d.%m.%Y %H:%M:%S')"
|
||||
notify_send "$msg_f"
|
||||
write_log "LOGIND: сбой: $line"
|
||||
fi
|
||||
fi
|
||||
done <<<"$logind_lines"
|
||||
fi
|
||||
|
||||
echo "$current_time" >"$LAST_LOGIND_CHECK_FILE"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Ежедневный отчёт
|
||||
# ============================================
|
||||
@@ -1140,7 +1277,7 @@ send_startup_notification() {
|
||||
local message=" ✅ СКРИПТ МОНИТОРИНГА ЗАПУЩЕН "$'\n'
|
||||
message="${message}🖥️ Сервер: $(hostname)"$'\n'
|
||||
message="${message}🕐 Время запуска: ${timestamp}"$'\n'
|
||||
message="${message}📊 Мониторинг SSH и SUDO активирован"$'\n'
|
||||
message="${message}📊 Мониторинг SSH, SUDO и systemd-logind активирован"$'\n'
|
||||
message="${message}🚫 Автоблокировка IP после ${MAX_ATTEMPTS} попыток на ${BAN_TIME} секунд"$'\n'
|
||||
message="${message}⚪ Белый список: ${whitelist_count} IP/подсетей"$'\n'
|
||||
message="${message}❤️ Heartbeat будет отправляться каждые 12 часов"
|
||||
@@ -1180,6 +1317,7 @@ main() {
|
||||
monitor_ssh
|
||||
monitor_sudo
|
||||
monitor_security_events
|
||||
monitor_logind
|
||||
check_daily_report
|
||||
check_heartbeat
|
||||
write_health_and_metrics
|
||||
@@ -1208,6 +1346,9 @@ run_check_config() {
|
||||
printf 'BRUTE_WINDOW_SEC=%s BRUTE_MIN_FAILS=%s\n' "$BRUTE_WINDOW_SEC" "$BRUTE_MIN_FAILS"
|
||||
printf 'PROMETHEUS_TEXTFILE_DIR=%s\n' "${PROMETHEUS_TEXTFILE_DIR:-}"
|
||||
printf 'HEALTHCHECK_STATUS_FILE=%s\n' "${HEALTHCHECK_STATUS_FILE:-}"
|
||||
printf 'ENABLE_LOGIND_MONITOR=%s LAST_LOGIND_CHECK_FILE=%s\n' "${ENABLE_LOGIND_MONITOR:-}" "${LAST_LOGIND_CHECK_FILE:-}"
|
||||
printf 'LOGIND_NOTIFY_NEW=%s LOGIND_NOTIFY_REMOVED=%s LOGIND_NOTIFY_FAILED=%s LOGIND_SKIP_REMOTE=%s\n' \
|
||||
"${LOGIND_NOTIFY_NEW:-}" "${LOGIND_NOTIFY_REMOVED:-}" "${LOGIND_NOTIFY_FAILED:-}" "${LOGIND_SKIP_REMOTE:-}"
|
||||
if bash -n "$0" 2>/dev/null; then
|
||||
printf 'bash -n: OK\n'
|
||||
else
|
||||
|
||||
@@ -10,8 +10,17 @@ LAST_REPORT_FILE="/var/log/last_daily_report.txt"
|
||||
LAST_SSH_CHECK_FILE="/var/log/last_ssh_check"
|
||||
LAST_SUDO_CHECK_FILE="/var/log/last_sudo_check"
|
||||
LAST_SECURITY_EVENTS_FILE="/var/log/last_security_events_check"
|
||||
LAST_LOGIND_CHECK_FILE="/var/log/last_logind_check"
|
||||
BAN_LIST_FILE="/var/log/ssh_bans.txt"
|
||||
|
||||
# systemd-logind: 1 = включено, 0 = выключено
|
||||
ENABLE_LOGIND_MONITOR="1"
|
||||
LOGIND_NOTIFY_NEW="1"
|
||||
LOGIND_NOTIFY_REMOVED="0"
|
||||
LOGIND_NOTIFY_FAILED="1"
|
||||
# 1 = не дублировать Telegram для новых сессий с типом ssh (уведомление уже есть из SSH-монитора)
|
||||
LOGIND_SKIP_REMOTE="1"
|
||||
|
||||
DAILY_REPORT_HOUR="9"
|
||||
# IANA-зона для «9:00» и календарной даты отчёта; пусто = как у команды date (обычно локаль сервера, см. timedatectl)
|
||||
DAILY_REPORT_TZ=""
|
||||
|
||||
Reference in New Issue
Block a user