Перейти к содержимому

Уведомления о SSH-входах

Незамеченный вход по SSH под root — худшее, что может случиться с сервером. Сделаем так, чтобы каждый успешный вход (и опционально — попытки) приходил вам в Notifly с указанием пользователя, IP и времени.

PAM умеет вызывать произвольный скрипт после успешной аутентификации.

Сохраните как /usr/local/bin/notifly-ssh-login:

#!/usr/bin/env bash
set -eu
set -a; source /etc/notifly.env; set +a
# PAM передаёт переменные окружения PAM_*
USER="${PAM_USER:-unknown}"
RHOST="${PAM_RHOST:-unknown}"
SERVICE="${PAM_SERVICE:-unknown}"
TYPE="${PAM_TYPE:-unknown}"
# Реагируем только на открытие сессии sshd
[ "$SERVICE" = "sshd" ] || exit 0
[ "$TYPE" = "open_session" ] || exit 0
HOST=$(hostname -s)
PRIO=5
[ "$USER" = "root" ] && PRIO=9
/usr/local/bin/notifly-send \
"🔐 SSH-вход: $USER@$HOST" \
"Пользователь: $USER
Источник: $RHOST
Сервер: $HOST
Время: $(date '+%Y-%m-%d %H:%M:%S %Z')" \
"$PRIO" || true

Сделайте исполняемым:

Окно терминала
sudo chmod +x /usr/local/bin/notifly-ssh-login

Откройте /etc/pam.d/sshd и добавьте в самом конце:

session optional pam_exec.so /usr/local/bin/notifly-ssh-login

Готово. Перезапускать sshd не нужно — PAM-конфиг перечитывается при каждом подключении.

С другой машины:

Окно терминала
ssh user@your-server

В Notifly прилетит сообщение с приоритетом 5 (или 9 для root).

pam_exec срабатывает только при успехе. Для отслеживания неудачных попыток проще читать journalctl через systemd. Создайте сервис-watcher:

/usr/local/bin/notifly-ssh-failed:

#!/usr/bin/env bash
set -eu
set -a; source /etc/notifly.env; set +a
HOST=$(hostname -s)
journalctl -u ssh -u sshd -f -o cat --since now | \
while read -r line; do
if echo "$line" | grep -q "Failed password"; then
USER=$(echo "$line" | grep -oP 'for \K\S+' | head -1)
IP=$(echo "$line" | grep -oP 'from \K\S+' | head -1)
/usr/local/bin/notifly-send \
"⚠️ Неудачный SSH: $USER@$HOST" \
"IP: $IP
Пользователь: $USER" 4 || true
fi
done

/etc/systemd/system/notifly-ssh-failed.service:

[Unit]
Description=Notify about failed SSH attempts
After=ssh.service network-online.target
[Service]
ExecStart=/usr/local/bin/notifly-ssh-failed
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
Окно терминала
sudo chmod +x /usr/local/bin/notifly-ssh-failed
sudo systemctl daemon-reload
sudo systemctl enable --now notifly-ssh-failed

Аналог для Windows-серверов. Все входы — интерактивные, RDP, WinRM, OpenSSH — пишутся в Security Event Log. Нужные коды:

  • 4624 — успешный вход (LogonType=10 — RDP, 5 — сервис, 3 — сеть, 2 — локальный).
  • 4625 — неудачный вход.
C:\scripts\Notifly-Logon.ps1
param([int]$EventId = 4624)
. C:\scripts\Notifly.ps1
# Берём последнее событие из Security Log
$ev = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=$EventId} -MaxEvents 1
if (-not $ev) { return }
# Разбираем EventData
$xml = [xml]$ev.ToXml()
$data = @{}
$xml.Event.EventData.Data | ForEach-Object { $data[$_.Name] = $_.'#text' }
$user = $data.TargetUserName
$logonType = $data.LogonType
$ip = $data.IpAddress
$workstation = $data.WorkstationName
# Игнорируем сервисные входы и анонимных
if ($user -in @("SYSTEM", "ANONYMOUS LOGON", "$env:COMPUTERNAME$")) { return }
if ($user -like "*$") { return } # компьютерные учётки
$logonName = switch ($logonType) {
"2" { "Локальный" }
"3" { "Сеть" }
"7" { "Разблокировка" }
"10" { "RDP" }
"11" { "Кэшированный" }
default { "Type=$logonType" }
}
$prio = if ($EventId -eq 4625) { 5 } else { 5 }
if ($user -in @("Administrator", "Администратор")) { $prio = 9 }
$icon = if ($EventId -eq 4625) { "⚠️ Неудачный" } else { "🔐 Вход" }
Send-Notifly `
-Title "$icon $($logonName): $user@$env:COMPUTERNAME" `
-Message "Пользователь: $user`nИсточник: $ip $workstation`nСервер: $env:COMPUTERNAME`nВремя: $($ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))" `
-Priority $prio
Окно терминала
# Событие 4624 — все успешные входы
$xml = @"
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">
*[System[EventID=4624 and (EventData/Data[@Name='LogonType']='10' or EventData/Data[@Name='LogonType']='2' or EventData/Data[@Name='LogonType']='7')]]
</Select>
</Query>
</QueryList>
"@
$Trigger = New-ScheduledTaskTrigger -AtStartup
$Trigger.Subscription = $xml
$Action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Logon.ps1 -EventId 4624"
Register-ScheduledTask -TaskName "Notifly Logon Watch" -Trigger $Trigger -Action $Action `
-Principal (New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest)
# Неудачные попытки (Event ID 4625)
$xml2 = $xml -replace '4624', '4625' -replace "\(EventData.*\)", "true"
$T2 = New-ScheduledTaskTrigger -AtStartup
$T2.Subscription = $xml2
$A2 = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Logon.ps1 -EventId 4625"
Register-ScheduledTask -TaskName "Notifly Failed Logon Watch" -Trigger $T2 -Action $A2 `
-Principal (New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest)
  • Каждый успешный вход подтверждает: это были вы. Если пришло уведомление про чей-то вход в 3 ночи из чужой страны — это сигнал.
  • Root всегда с приоритетом 9 — отдельный громкий сигнал на телефоне, даже если вы спите.
  • Неудачные попытки превращаются в живой индикатор bruteforce: если поток идёт нон-стоп — пора ставить Fail2ban или ужесточать MaxAuthTries.
  • Добавить геолокацию IP через geoiplookup — заголовок «🌍 Вход из RU/Москва».
  • Не присылать уведомление для разрешённого списка IP (свой офис/VPN).
  • Соединить с Fail2ban — получать ещё и события банов.