iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔐

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.

ssh-login-notify.sh
#!/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:

/etc/pam.d/sshd
 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 optional setting, 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