Ops

Monitoring SSH connections on your server without external services

Author Photo

Quentin Lerebours

Thumbnail

A few weeks ago, I had a doubt about potentially suspicious SSH connections on one of my servers.
In the end, there was no actual issue, but as part of a continuous improvement mindset, I thought:

What if I could receive real-time alerts whenever an SSH connection is established on one of my servers?

My constraints

As always, it’s important to look at the context to decide whether it makes sense to build this kind of system yourself.
So I defined a few constraints:

  • no external dependencies
  • quick to set up
  • be notified at any time, wherever I am

The idea

SSH relies on PAM (Pluggable Authentication Modules).
PAM allows you to execute a script every time a session is opened.

The idea is therefore simple:

  1. A bash script triggered on every SSH login
  2. It sends a message via a webhook (Discord or Slack)
  3. I get notified in real time

Setup

The SSH alert script

Create the script that will send a message via Discord
(an alternative for Slack is available further down):

sudo vim /usr/local/bin/ssh-session-alert.sh

Add the following content:

#!/bin/bash

[ "$PAM_TYPE" != "open_session" ] && exit 0

WEBHOOK_URL="https://discord.com/api/webhooks/XXX/YYY" # replace this

CONTENT="SSH login user=$PAM_USER ip=${PAM_RHOST:-local} host=$(hostname) date=$(date '+%F %T')"

JSON=$(printf '{"content":"%s"}' "$CONTENT")

/usr/bin/curl \
  --silent \
  --fail \
  -H "Content-Type: application/json" \
  -X POST \
  -d "$JSON" \
  "$WEBHOOK_URL" \
  >> /tmp/ssh-alerting-error.log 2>&1

Make sure to:

  • Replace the Discord webhook URL with your own
  • Make the script executable:
    sudo chmod +x /usr/local/bin/ssh-session-alert.sh

Hooking the script into SSH via PAM

Edit the SSH PAM file:

sudo vim /etc/pam.d/sshd

Add at the end of the file:

session optional pam_exec.so /usr/local/bin/ssh-session-alert.sh

From that point on, every new SSH session will trigger the script, resulting in something like:

SSH login user=qlerebours ip=x.y.z.a host=my-server date=2026-02-02 14:32:10


Slack version of the curl command

Slack expects a slightly different payload.
Here is the Slack version of the curl command, to use instead of the Discord one if needed:

JSON=$(printf '{"text":"%s"}' "$CONTENT")

/usr/bin/curl \
  --silent \
  --fail \
  -H "Content-Type: application/json" \
  -X POST \
  -d "$JSON" \
  "$WEBHOOK_URL" \
  >> /tmp/ssh-alerting-error.log 2>&1

The rest of the script remains exactly the same.


Debugging tips if it doesn’t work

  1. Make sure the script is actually executed

    • Add at the very top of the script:
      echo "SSH LOGIN $(date) user=$PAM_USER rhost=$PAM_RHOST" >> /tmp/ssh-test.log
    • Then try an SSH connection and check whether /tmp/ssh-test.log was written to
  2. Check curl errors
    If the script runs but nothing shows up on Discord / Slack:

    • Inspect the file: cat /tmp/ssh-alerting-error.log
  3. Test the script manually

    • /usr/local/bin/ssh-session-alert.sh

If it works when run manually but not via SSH, the issue almost always comes from the PAM execution context.


Why I like this solution

  • Easy to set up
  • Zero external dependencies
  • Effective enough to quickly detect suspicious behavior

This is obviously not a complete security system.
But as is often the case, having a simple and immediate signal is better than a perfect solution that never gets implemented.

#security#linux#ssh#monitoring#devops
Author Photo

About Quentin Lerebours

I’m an entrepreneur, but above all a developer. I’ve deliberately chosen to remain versatile so I can approach projects with a clear, cohesive overall vision. Development, sales, entrepreneurship, and project management are part of my daily life — and I wouldn’t have it any other way.