Receive SSH login notifications through Slack or Discord

I have a problem. Sometimes I’m slightly paranoid about computer security. When I’m on my university network I wonder if web requests made to websites not using HTTPS are being logged. Sometimes I wonder if a local server running on my laptop is being exposed to a public network when I’m not at home. And when it comes to my server, sometimes I wonder if I’m actually alone.

I follow secure practices both for my SSH server and for the appliations that run on it. I check the logs regularly. Yet, if someone is clever enough to break-in into my server, they may just wipe the footprints from the logs as well. Critical logs should not live on the same server you are trying to protect, otherwise the logs are not at a lower risk of being tampered.

Considering my smartphone already has a bunch of chat applications, it would be cool to receive an IM notification on Slack or Discord whenever a log in attempt is done. Let’s see how to do this using webhooks: I’ll write a program that pings an HTTP webhook, and I’ll run that program every time someone logs in via SSH through PAM.

Set up the Slack/Discord webhook

Both Slack and Discord support webhooks. Webhooks are URL endpoints associated to your server. Using an HTTP client or library, you can make a POST request to that URL endpoint providing some multimedia payload as a parameter: text, but also images, files or embedded data. Whatever you send as a payload, it will appear on your server.

Here are the instructions for setting up a webhook in Slack, and here are the instructions for Discord. Here is the Discord webhook I made:

Settings dialog for my Discord webhook.
I made a webhook named "sample hook" that is able to send messages to the #server-status channel.

The cool thing about webhooks is that they work on any program that can make HTTP requests. You can even test your webhook using curl:

λ ~/ curl -X POST \
>       -H 'Content-Type: application/json' \
>       -d '{"content": "Hello world, I am a hook!"}' \
>       https://discordapp.com/api/webhooks/...

If you place the proper webhook URL, you will see a message sent through a bot appear on your server chat. The same thing goes for Slack.

A webhook has sent the message 'Hello world, I am a hook!' to the channel.
Running the webhook results in a message being posted in the server chat by a bot.

Setting up a PAM rule

Note: while the changes I’ll make are not dangerous, you should use a VM instead of a production server if you don’t want to trust some internet guy.

PAM is a framework for many UNIX flavours, including GNU/Linux and macOS, for creating authentication modules that allows the system to perform advanced login strategies, like forcing users to log in through hardware keys or using two factor authentication.

One of these modules is pam_exec.so. It can run a program on different stages of the server lifecycle, such as after a successful login is made or after a logout. This program will receive information about the login event, including the user and the hostname, if the login is made remotely.

To add an execution rule that will run a program every time a successful log in is done through SSH, add the following information to your /etc/pam.d/sshd file — this file contains the PAM rules triggered during SSH login:

session   optional   pam_exec.so   /usr/local/sbin/sshd-login

PAM configuration files, like many other UNIX configuration files, contain whitespace-separated values. Every word in a rule is an argument for that rule. There is a comprehensive explanation about these arguments on pam.d(5), but the most important things to grasp here is that session rules will trigger before and after an user is given service. The program I’m trying to execute is /usr/local/sbin/sshd-login.

Wiring the program to the endpoint

Let’s make a program that sends information about the login event to a Discord webhook. Or to a Slack server, although the parameters may change.

When pam_exec.so executes the program, it provides some information about the login event, such as the logged in user or the remote host that made the login attempt, through environment variables. Some of these environment variables are PAM_USER and PAM_RHOST. You can find more information in the manpage for pam_exec(8).

sshd-login —my program— will wrap the values for those variables into a JSON payload and perform an HTTP request using curl to send the payload to the webhook. The script is so simple that it can be written using bash:

#!/bin/bash

WEBHOOK_URL="https://discordapp.com:443/api/webhooks/<hook URL>"

# Let's capture only open_session and close_session events (login and logout).
case "$PAM_TYPE" in
    open_session)
        PAYLOAD=" { \"content\": \"$PAM_USER logged in (remote host: $PAM_RHOST).\" }"
        ;;
    close_session)
        PAYLOAD=" { \"content\": \"$PAM_USER logged out (remote host: $PAM_RHOST).\" }"
        ;;
esac

# Let's only perform a request if there is an actual payload to send.
if [ -n "$PAYLOAD" ] ; then
    curl -X POST -H 'Content-Type: application/json' -d "$PAYLOAD" "$WEBHOOK_URL"
fi

Despite I like shell scripts, escaping JSON quotes in a shell script is hell. If you are going to make a more complex script, I strongly suggest you to use a different scripting language such as Ruby or Python, which make easier to write and send JSON payloads.

Make this file executable.

Let’s test this. When pam_exec.so executes this file during a login, $PAM_USER will contain the username; $PAM_RHOST, the remote hostname and $PAM_TYPE will equal open_session. During log out, it works the same but $PAM_TYPE will equal close_session. Run the following programs on your terminal, and check that the hooks are triggering on your chat room.

PAM_USER=foo PAM_RHOST=bar PAM_TYPE=open_session /usr/local/sbin/sshd-login
PAM_USER=foo PAM_RHOST=bar PAM_TYPE=close_session /usr/local/sbin/sshd-login
The webhook is being triggered and some login messages appear in the channel.
The result of this test.

Now open a new shell window or tab and log in to your server. You should see your real username and host appearing on the chat log. Try to log out as well.

The webhook prints my username and my (blurred) host address.
Oh my god, it knows me!

Conclusion

I made a PAM rule that runs a program whenever someone logs in or logs out via SSH into my server. The executed program takes a few environment variables provided by PAM with information about the login event and sends them to my Discord server into a private channel using a webhook.

Webhooks are extremely cool because of the multiple use cases they have. They are available both in Discord and Slack. I also made a similar shell script that triggers a Slack webhook on the server at work, so that I can be less paranoid about that server as well.

One thing to notice is that this will only track successful login events. I haven’t found a way to track unsuccessful login events yet. However, I’d rather don’t track those events. Servers exposed to the internet will eventually receive the visit of vulnerability scanners and botnets. Assuming you have a proper security police, they will fail. However, these break-in attempts will still be logged, adding noise into your channel. I only want to get notified on successful logins. This way, if I ever receive a message on this channel and I’m not in front of my computer, I can be sure that things got fu***d up.