SSH login notifications
An unnoticed SSH login as root is the worst thing that can happen to a server. Let’s make it so that every successful login (and optionally — attempts) is sent to you in Notifly with the user, IP and time.
The simplest way — via PAM
Section titled “The simplest way — via PAM”PAM can call an arbitrary script after successful authentication.
Notifier script
Section titled “Notifier script”Save as /usr/local/bin/notifly-ssh-login:
#!/usr/bin/env bashset -euset -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}"
# Only react to sshd session openings[ "$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" || trueMake it executable:
sudo chmod +x /usr/local/bin/notifly-ssh-loginHooking into sshd
Section titled “Hooking into sshd”Open /etc/pam.d/sshd and add at the very end:
session optional pam_exec.so /usr/local/bin/notifly-ssh-loginDone. No need to restart sshd — PAM config is re-read on every connection.
From another machine:
ssh user@your-serverNotifly will receive a message with priority 5 (or 9 for root).
Failed-attempt notifications
Section titled “Failed-attempt notifications”pam_exec only triggers on success. To track failed attempts it’s easier to read journalctl via systemd. Create a service watcher:
/usr/local/bin/notifly-ssh-failed:
#!/usr/bin/env bashset -euset -a; source /etc/notifly.env; set +aHOST=$(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 fidone/etc/systemd/system/notifly-ssh-failed.service:
[Unit]Description=Notify about failed SSH attemptsAfter=ssh.service network-online.target
[Service]ExecStart=/usr/local/bin/notifly-ssh-failedRestart=alwaysRestartSec=10s
[Install]WantedBy=multi-user.targetsudo chmod +x /usr/local/bin/notifly-ssh-failedsudo systemctl daemon-reloadsudo systemctl enable --now notifly-ssh-failedWindows: RDP / WinRM / SSH via the Security Event Log
Section titled “Windows: RDP / WinRM / SSH via the Security Event Log”The Windows-server equivalent. All logons — interactive, RDP, WinRM, OpenSSH — are recorded in the Security Event Log. Relevant codes:
- 4624 — successful logon (
LogonType=10— RDP,5— service,3— network,2— local). - 4625 — failed logon.
Notifier script
Section titled “Notifier script”param([int]$EventId = 4624). C:\scripts\Notifly.ps1
# Get the latest event from the Security Log$ev = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=$EventId} -MaxEvents 1if (-not $ev) { return }
# Parse 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
# Ignore service logons and anonymousif ($user -in @("SYSTEM", "ANONYMOUS LOGON", "$env:COMPUTERNAME$")) { return }if ($user -like "*$") { return } # computer accounts
$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 $prioEvent subscription
Section titled “Event subscription”# Event 4624 — all successful logons$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)
# Failed attempts (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)Benefits
Section titled “Benefits”- Every successful logon confirms: it was you. If you get a notification about someone’s logon at 3 AM from another country — that’s a signal.
- Root always has priority 9 — a separate loud alert on your phone, even if you’re sleeping.
- Failed attempts become a live indicator of bruteforce: if the stream runs non-stop — it’s time to set up Fail2ban or tighten
MaxAuthTries.
What to improve next
Section titled “What to improve next”- Add IP geolocation via
geoiplookup— header «🌍 Logon from RU/Moscow». - Do not send notifications for an allowlist of IPs (your office/VPN).
- Integrate with Fail2ban — receive ban events as well.