If someone logs into your server, you want to know immediately, especially if it wasn’t you. A small script hooked into PAM can send an alert on every SSH login with the user, source IP and time, by email, Telegram, Slack, Discord or any webhook. It takes ten minutes to set up and gives early warning of stolen keys or passwords.
How it works
PAM (Pluggable Authentication Modules) runs for every login. The pam_exec module can run a script when a session opens, after successful authentication. The script receives details in environment variables:
| Variable | Contains |
|---|---|
PAM_USER |
Username |
PAM_RHOST |
Remote IP |
PAM_TYPE |
open_session / close_session |
PAM_SERVICE |
sshd |
PAM_TTY |
Terminal |
1. Write the notification script
Telegram
Create a bot with @BotFather, and get your chat ID. Then:
sudo tee /usr/local/bin/ssh-login-notify > /dev/null <<'EOF'
#!/bin/sh
[ "$PAM_TYPE" = "open_session" ] || exit 0
BOT_TOKEN="123456:ABC-your-bot-token"
CHAT_ID="123456789"
HOST="$(hostname)"
TEXT="SSH login on ${HOST}: user ${PAM_USER} from ${PAM_RHOST} at $(date '+%F %T %Z')"
curl -fsS -m 5 "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${CHAT_ID}" --data-urlencode "text=${TEXT}" > /dev/null 2>&1 &
exit 0
EOF
sudo chmod 700 /usr/local/bin/ssh-login-notify
Slack, Discord or a generic webhook
Replace the curl line:
curl -fsS -m 5 -H 'Content-Type: application/json' \
-d "{\"text\":\"${TEXT}\"}" "https://hooks.slack.com/services/XXX/YYY/ZZZ" > /dev/null 2>&1 &
Discord uses {"content": "…"}. ntfy.sh and Gotify are simple self-hostable options. See Telegram bot on a VPS.
With a configured MTA (or msmtp):
printf 'Subject: SSH login on %s\n\n%s\n' "$HOST" "$TEXT" | /usr/sbin/sendmail [email protected] &
Important details
- Run the request in the background (
&) with a timeout (-m 5) so a slow or unreachable API never delays or blocks logins. - Always
exit 0. Withoptionalin PAM a failure won’t block logins anyway, but don’t rely on that. chmod 700, owned by root. The script contains your bot token.
2. Hook it into PAM
Add one line at the end of /etc/pam.d/sshd:
session optional pam_exec.so /usr/local/bin/ssh-login-notify
optional means a failing script never prevents a login. No sshd restart is needed. PAM reads the file on each login.
Test it from a new terminal (keep your current session open): ssh user@server. The alert should arrive within a second or two.
To alert on sudo use too, add the same line to /etc/pam.d/sudo. PAM_RHOST is empty there, but PAM_USER and PAM_RUSER (the invoking user) are set.
3. Avoid alert fatigue
If you log in many times a day, or automation logs in constantly, alerts become noise and get ignored. Options:
- Skip trusted sources (your office IP, the backup server):
case "$PAM_RHOST" in
203.0.113.50|10.0.0.*) exit 0 ;;
esac
- Skip service users (
[ "$PAM_USER" = "backup" ] && exit 0). - Add geolocation or reverse DNS to the message, so unusual sources stand out.
- Send to a channel you actually watch.
Alternatives and complements
/etc/ssh/sshrc— runs for every SSH session too (unless a user’s~/.ssh/rcoverrides it). It’s simpler, but users can bypass it, so the PAM method is more robust.- Log-based alerting — ship
auth.log/journal to central logging and alert there. It’s harder to tamper with than a local script. See rsyslog. - Fail2Ban for failed attempts. Login notifications cover successful ones. See Fail2Ban.
- Review logins regularly:
last -a | head,lastb(failed), andjournalctl -u ssh --since today.
If you get an alert you don’t recognise
- Check active sessions:
who,w. Kill a suspicious session withpkill -KILL -t pts/1. - Check
~/.ssh/authorized_keysfor every user, plus new users in/etc/passwd. - Rotate keys and passwords, and block the source IP.
- Investigate how they got in. If root was compromised, rebuild the server. See rkhunter and Linux rescue mode.
Quick reference
| Step | What |
|---|---|
| Script | /usr/local/bin/ssh-login-notify (root, 700) |
| PAM hook | session optional pam_exec.so /usr/local/bin/ssh-login-notify in /etc/pam.d/sshd |
| Variables | PAM_USER, PAM_RHOST, PAM_TYPE |
| Don’t block logins | background &, -m 5 timeout, optional |
| Recent logins | last -a, lastb |
Related: key-only SSH and SSH 2FA.