So having a bastion kind of server in any environment is nice but having bots endlessly try to brute-force such machines is a pain in the butt, so hardening them is crucial!

When using SSH, I always use some kind of Swiss cheese approach: no passwords, only valid SSH keys, no root login, two-factor authentication, a limited set of users in the SSH configuration, and so on and on.

But even with all the precautions, you may never know, so another good practice is logging and alerting. On some of my bastions I also want to notify a Slack chat if a client or someone logs onto the machine.

A custom SSH port can still reduce scanner noise, but it is not an authentication control.

Store the Webhook Separately

A Slack incoming-webhook URL is a secret that can post to its channel. I keep it out of the PAM script and make it root-readable only.

sudo install -d -m 700 /etc/ssh-login-notify
sudoedit /etc/ssh-login-notify/slack.env
sudo chmod 600 /etc/ssh-login-notify/slack.env
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/REPLACE/ME

Slack documents webhook URLs as secrets and revokes leaked ones. Slack incoming webhooks

Notification Script

/opt/scripts/login-logger.sh:

#!/usr/bin/env bash
set -u

readonly LOG_FILE=/var/log/ssh-auth
readonly ALLOWLIST=/etc/ssh-login-notify/allow-users
readonly SECRETS=/etc/ssh-login-notify/slack.env

[[ ${PAM_TYPE:-} == open_session ]] || exit 0
user=${PAM_USER:-unknown}
remote=${PAM_RHOST:-unknown}
printf '[%s] open_session: %s from %s\n' "$(date --iso-8601=seconds)" "$user" "$remote" >> "$LOG_FILE"

if [[ -r $ALLOWLIST ]] && grep -Fxq -- "$user" "$ALLOWLIST"; then
  exit 0
fi

# A notification outage must never reject the PAM session.
(
  set +e
  . "$SECRETS"
  payload=$(jq -n --arg text "SSH login: $user from $remote on $(hostname)" '{text:$text}')
  curl --fail --silent --show-error \
    --connect-timeout 3 --max-time 8 \
    -H 'Content-Type: application/json' \
    --data "$payload" "$SLACK_WEBHOOK_URL" >/dev/null
) >/dev/null 2>&1 &

exit 0

jq builds valid JSON even if PAM fields contain quotes or backslashes. grep -Fxq matches one whole allowlist line instead of treating a username as a regular expression.

Prepare the log and files:

sudo install -o root -g adm -m 0640 /dev/null /var/log/ssh-auth
sudo install -o root -g root -m 0755 login-logger.sh /opt/scripts/login-logger.sh
sudo install -o root -g root -m 0644 /dev/null /etc/ssh-login-notify/allow-users

Add It to PAM

At the end of /etc/pam.d/sshd:

session optional pam_exec.so quiet /opt/scripts/login-logger.sh

I use optional, keep the script ending successfully, and give curl short timeouts so Slack cannot block or deny an SSH login. I test with a second SSH session while keeping the first root session open, then check /var/log/ssh-auth and the channel.

The notification is useful visibility, but the SSH authentication and authorization settings remain the actual protection.



Buy Me a Coffee