iTranslated by AI
Automating Slack Notifications for SSH, SCP, and SFTP Logins Using PAM
Introduction
While managing EC2 servers, I had a thought:
"If someone logs in via SSH right now, I wouldn't even notice."
I have restricted IP addresses via Security Groups and set up key-based authentication. However, I can't know when, who, or from where someone logged in unless I manually check the logs. And it's not just SSH; I want to be aware of connections via SCP and SFTP as well.
After some research, I found that by using Linux PAM (Pluggable Authentication Modules), you can send notifications to Slack for all SSH/SCP/SFTP logins. You only need to create three files. It takes less than 10 minutes to set up.
How it works
The key here is that SSH, SCP, and SFTP all pass through sshd. Furthermore, sshd authentication is managed by PAM.
SSH Connection ─┐
SCP Transfer ─┼─→ sshd ─→ PAM Auth ─→ pam_exec.so ─→ Notification Script ─→ Slack
SFTP Connection ─┘
In short, by adding just one line to /etc/pam.d/sshd, you can cover all three protocols.
Why use PAM?
I considered other methods as well.
| Method | SSH | SCP | SFTP | Convenience |
|---|---|---|---|---|
| PAM (pam_exec) | ○ | ○ | ○ | 1 line added |
| /etc/ssh/sshrc | ○ | × | × | Insufficient coverage |
| ForceCommand | ○ | △ | △ | Side effects occur |
| journald monitoring | ○ | ○ | ○ | Daemon needs to be running |
sshrc only triggers for interactive SSH, so it cannot catch SCP/SFTP. With PAM, you can catch them all, and by using the optional flag, you can ensure a safe design where the SSH session itself is not blocked even if the notification script fails.
What to create
There are only three files.
/usr/local/bin/ssh-login-notify.sh # Notification script (755, root:root)
/usr/local/etc/ssh-notify.conf # Webhook URL config (600, root:root)
/etc/pam.d/sshd # Add one line to the end
Steps
1. Get Slack Webhook URL
Enable "Incoming Webhooks" on the Slack App settings page, select the destination channel, and retrieve the URL.
2. Create the configuration file
Create a file to store the Webhook URL.
sudo tee /usr/local/etc/ssh-notify.conf > /dev/null << 'EOF'
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX
EOF
sudo chmod 600 /usr/local/etc/ssh-notify.conf
sudo chown root:root /usr/local/etc/ssh-notify.conf
3. Create the notification script
This is the actual shell script called by PAM.
#!/bin/bash
# ==============================================
# SSH/SCP/SFTP Login Slack Notification Script
# Triggered by PAM via pam_exec.so
# ==============================================
# Only run on session open (not close)
[ "$PAM_TYPE" != "open_session" ] && exit 0
# Load config
CONFIG="/usr/local/etc/ssh-notify.conf"
[ ! -f "$CONFIG" ] && exit 0
source "$CONFIG"
[ -z "$SLACK_WEBHOOK_URL" ] && exit 0
# Detect connection type
CONNECTION_TYPE="SSH (interactive)"
if [ "$PAM_TTY" = "ssh" ] || [ -z "$PAM_TTY" ]; then
# No TTY assigned — SCP or SFTP
if ps -ef 2>/dev/null | grep -q "[s]ftp-server"; then
CONNECTION_TYPE="SFTP"
else
CONNECTION_TYPE="SCP"
fi
elif [[ "$PAM_TTY" == /dev/pts/* ]]; then
CONNECTION_TYPE="SSH (interactive)"
fi
# Gather info
HOSTNAME=$(hostname)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S %Z')
USER="${PAM_USER:-unknown}"
REMOTE="${PAM_RHOST:-unknown}"
# Build message
MESSAGE=$(cat << MSG
:lock: *SSH Login Alert*
━━━━━━━━━━━━━━━━━━
*Server:* ${HOSTNAME}
*User:* ${USER}
*From:* ${REMOTE}
*Type:* ${CONNECTION_TYPE}
*Time:* ${TIMESTAMP}
MSG
)
# Send to Slack in background (don't delay login)
(curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"text\": \"${MESSAGE}\"}" \
"$SLACK_WEBHOOK_URL" > /dev/null 2>&1) &
exit 0
sudo cp ssh-login-notify.sh /usr/local/bin/
sudo chmod 755 /usr/local/bin/ssh-login-notify.sh
sudo chown root:root /usr/local/bin/ssh-login-notify.sh
4. Add hook to PAM
Add one line to the end of /etc/pam.d/sshd. That's it.
echo 'session optional pam_exec.so seteuid /usr/local/bin/ssh-login-notify.sh' | sudo tee -a /etc/pam.d/sshd
After adding, it should look like this:
session include password-auth
session include postlogin
+session optional pam_exec.so seteuid /usr/local/bin/ssh-login-notify.sh
Here is the meaning of each option:
| Option | Meaning |
|---|---|
session |
Executes on session start/end |
optional |
Does not block login even if the script fails |
seteuid |
Executes with root privileges (required to read config file) |
Script explanation
Environment variables set by PAM
The script called via pam_exec.so receives the following environment variables:
| Variable | Content | Example |
|---|---|---|
PAM_TYPE |
Event type |
open_session, close_session
|
PAM_USER |
Connected username | ec2-user |
PAM_RHOST |
Connected IP | 203.0.113.50 |
PAM_TTY |
Assigned terminal |
/dev/pts/0, ssh
|
PAM_SERVICE |
Service name | sshd |
Discrimination logic for SSH/SCP/SFTP
This is the most interesting part of this approach.
All three protocols go through sshd, so PAM_SERVICE is always sshd. You can't distinguish them that way. The trick is to check PAM_TTY.
PAM_TTY = "/dev/pts/0" → SSH (Interactive session. PTY is assigned)
PAM_TTY = "ssh" or "" → SCP or SFTP (No PTY)
Interactive SSH is assigned a PTY (pseudo-terminal), whereas SCP and SFTP do not have a PTY because they are only for file transfer. We utilize this difference.
Furthermore, SCP and SFTP are distinguished via the process tree. SFTP launches sftp-server internally, which can be found via ps.
if ps -ef 2>/dev/null | grep -q "[s]ftp-server"; then
CONNECTION_TYPE="SFTP"
else
CONNECTION_TYPE="SCP"
fi
About the grep [s] technique
grep "[s]ftp-server" has the same meaning as grep sftp-server, but the grep process itself will not match. While [s] is equivalent to s in regex, the command line argument [s]ftp-server does not match the string sftp-server, thus excluding self-matching. It is slightly smarter than grep -v grep.
Zero login delay with background sending
(curl -s -X POST ... "$SLACK_WEBHOOK_URL" > /dev/null 2>&1) &
We are running curl in a subshell in the background. While the Webhook API typically returns within 200ms, it can occasionally take several seconds due to network latency or Slack outages. We don't want to wait every time we log in, so we decouple it with &.
Notification received in Slack
It arrives looking like this:
🔐 SSH Login Alert
━━━━━━━━━━━━━━━━━━
Server: ip-172-31-xx-xx
User: ec2-user
From: 203.0.113.50
Type: SSH (interactive)
Time: 2026-04-03 15:30:00 UTC
For SCP or SFTP, the Type changes so you can see at a glance which protocol was used to connect.
Operation Test
Script unit test
First, let's manually set the PAM environment variables and run it.
# SSH simulation
sudo PAM_TYPE="open_session" PAM_USER="test-user" \
PAM_RHOST="192.168.1.100" PAM_TTY="/dev/pts/0" \
/usr/local/bin/ssh-login-notify.sh
# SCP simulation
sudo PAM_TYPE="open_session" PAM_USER="test-user" \
PAM_RHOST="192.168.1.100" PAM_TTY="ssh" \
/usr/local/bin/ssh-login-notify.sh
If the notification arrives in Slack, it's successful.
Test by actually connecting
Connect via SSH/SCP/SFTP from another terminal.
ssh ec2-user@your-server
scp localfile.txt ec2-user@your-server:/tmp/
sftp ec2-user@your-server
Verify that notifications reach Slack for each.
Safety valve test (This is important)
Verify that the SSH connection is not affected if the script breaks.
# Strip execute permission from the script
sudo chmod 000 /usr/local/bin/ssh-login-notify.sh
# SSH from another terminal → Should be able to login without issues
# Restore permissions once confirmed
sudo chmod 755 /usr/local/bin/ssh-login-notify.sh
Thanks to optional, SSH connections still go through even if the script is unexecutable. Please remember this difference in case it had been required.
Operational Considerations
Protection of Webhook URL
If the Webhook URL leaks, anyone can send fake notifications. It's safe to check the configuration file permissions periodically.
ls -la /usr/local/etc/ssh-notify.conf
# -rw------- 1 root root ... ssh-notify.conf ← Must be 600
Use alongside logs
This mechanism is for "awareness" and does not replace audit logs. It is recommended to use it alongside /var/log/secure.
sudo grep "Accepted" /var/log/secure
Risk of silent notification failure
Since curl is backgrounded and optional is used, no one will notice if sending fails. If you are worried, adding a cron job for periodic test notifications is recommended.
Example of cron for periodic testing
# Send a test notification at 9 AM every day
0 9 * * * /usr/local/bin/ssh-login-notify-test.sh
Summary
- You can detect all SSH/SCP/SFTP logins using PAM's
pam_exec.so - With
optionalsetting, SSH connections won't be blocked even if notifications fail - Zero login delay with background curl execution
- Three files to create, can be introduced in 10 minutes
It is a mechanism that can be easily introduced as a first step toward "getting noticed." If full-scale intrusion detection is needed, consider dedicated tools like OSSEC.
Discussion