Tomáš Faikl
~rampaq

Tomáš Faikl ~rampaq

Linux programming electronics mathematics

My PGP key.

© 2023

Feed

E-mail notifications for sshd login

If you’re a person who owns a small public facing server, you probably use ssh to access the server. To add more security, it is a good idea to monitor who and when logs to your machine. You can overkill it with some bloat ISPConfig etc., or you can write your own little efficient shell script to send you notifications right to your mobile phone/mail when somebody or something logs in.


Firstly, one must have an email account. Wow.

Then we need to setup a mail transmitter on the Linux server. Such a program is for ex. a lightweight msmtp. As first few lines from official page say:

msmtp is an SMTP client. In the default mode, it transmits a mail to an SMTP server which takes care of further delivery. To use this program with your mail user agent (MUA), create a configuration file with your mail account(s).

To avoid confusion:

  1. This is not a mail server like Gmail, it just transmits messages to such servers.
  2. We will use bsd-mailx MUA, a very minimal one. But is it up to you.

Also, we need to figure out what process will we choose to deliver information about ssh login to our script.

At last, we need to write the script.


Setting up email

Install msmtp, many distros have it in their repos by default. Or compile it, info can be found at the official site.

Create a file in /etc/msmtprc with 600 permissions (only read&write for owner) and root:root ownership, your password to mail account will be stored in there. It is recommended to have a separate mail account just for these logging purposes. Also, see my note at the end.

# as root
touch /etc/msmtprc
chmod 600 /etc/msmtprc

(-rc stands probably for ‘run control’, it is a common name for config files.) Write this content to /etc/msmtprc and substitute for your mail account + server (there may be different procedures (2FA), check with your provider):

# Set default values for all following accounts.
defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
syslog         on

# log mail
account        <name>           # me
host           <MAIL server>    # smtp.gmail.com
port           <MAIL port>      # 587
from           <FROM address>   # me@gmail.com
user           <MAIL user>      # me@gmail.com
password       <MAIL password>  # password

# Set a default account
account default : <name>        # me
aliases        /etc/aliases

I also installed bsd-mailx, a very minimal MUA for convenience. After install, tell it to use msmtp - append this to the end of /etc/mail.rc:

set sendmail="/usr/bin/msmtp"

You can test it with echo hi | mailx -s SUBJECT <recipient's mail>.


Passing connection event information

Now we need to ensure that our future script will be envoked every time that someone logs in using sshd.

Here will be presented three ways that were tried to accomplish the task.

TL;DR: skip to #3

There are better alternatives and it is left in here just for curiosity.

1) Parsing log messages

Every system has a logger which retrieves messages from various parts of system. sshd of course logs login attempts. These are then sorted by a main program (mostly rsyslog is installed by default) into files. In /var/log/auth.log these attempts can be found.

Mar 19 22:43:44 rampaq sshd[19050]: Accepted publickey for <user> from <IP> port 47318 ssh2: <PUBKEY>
Mar 19 22:43:44 rampaq sshd[19050]: pam_unix(sshd:session): session opened for user <user> by (uid=0)

(Now, I should mention that I’m using Debian and that openssh-server package comes with disto-specific default option UsePAM yes instead of no. So the output may vary.)

What can be done is to listen to all messages and when a message contains for ex. pam_unix(sshd:session) session opened for user, then we know that somebody logged in. It is a crude way and all info (IP) cannot be retrieved easily, but we can try.

To listen to these messages, create /etc/rsyslog.d/50-mail-ssh.conf (all files in the directory are sorted by their number in front, lowest execute first) with content (a little explanation):

module(load="omprog")
if $msg contains 'pam_unix(sshd:session): session opened for user' then action(
        type="omprog"
        binary="/path/to/script.sh"
        template="RSYSLOG_TraditionalFileFormat"
        )

which executes a /path/to/script.sh with content

#!/bin/sh
SUBJECT="Subject: SSH Access to ${HOST}"
while IFS= read -r line; do
        echo "$line" | mailx -s "$SUBJECT" <mail address>
done

The omprog module of rsyslog sends to stdin (while keeping opened) a line with our information

Mar 19 22:43:44 rampaq sshd[19050]: pam_unix(sshd:session): session opened for user <user> by (uid=0)

and the script then processes it.

We could parse either the ‘user line’ or the ‘ip line’ but we would have to somehow remember the ip when next user line arrived at it would hard and not be elegant. At the moment, we can send only mail with the user line, which is ugly and would not tell us anything about the IP adrress.

2) sshrc

We can put our code to send an email to user’s .bashrc. But it can be easily circumvented. A better options is create a sshrc file (read login process, 8. in man sshd). We have to prevent user for creating his own (and by specs overriding the global one). This means adding to /etc/ssh/sshd_config option:

PermitUserRC no # prevents user from overwriting system-wide sshrc

Now to /etc/ssh/sshrc add

/path/to/script.sh &

to run our script on every login. Now we have more options, as $SSH connection variables are initialized. I won’t explicitly write the script in this section as it was very similiar to the one presented in #3.

But still, it has a major flaw, even bigger than the previous rsyslog approach. Upon reading more documentation on the third point in the login process above:

3) Checks /etc/nologin; if it exists, prints contents and quits (unless root).

It can be deduced, that the sshrc will NOT execute if the user’s shell is set to /bin/false, /bin/nologin, etc… as it is not a valid shell. These ‘shells’ are used to restrict user’s permission in system (via ForceCommand option in sshd_config), for example to only allow port forwarding. So this option will not log these attempts.

Also, because we set /etc/msmtprc to 600 and root:root, we can use mail only when root. Let’s move to the final and most satisfying option.

3) PAM

The best option is to use PAM. What is it? When you take a closer look on the log line produced by rsyslog,

Mar 19 22:43:44 rampaq sshd[19050]: pam_unix(sshd:session): session opened for user <user> by (uid=0)

you can notice pam_unix(sshd:session), which is a result of the UsePAM yes option in sshd_config (see above). After doing some research, it means that ssh contacts PAM to create a ‘PAM session’ and then logs user in. A very helful ‘resource’ (SE) here.

So now, we need to hook into the PAM session. After reading mapage for pam(7), there is an interesting paragraph:

session - this group of tasks cover things that should be done prior to

a service being given and after it is withdrawn. Such tasks include the maintenance of audit trails and the mounting of the user’s home directory. The session management group is important as it provides both an opening and closing hook for modules to affect the services available to a user.

and

FILES

/etc/pam.conf the configuration file

/etc/pam.d the Linux-PAM configuration directory. Generally, if this directory is present, the /etc/pam.conf file is ignored.

Take a look into /etc/pam.d directory. There are most of programs that use PAM (su, cron, login, …) and our sshd file. Open it and alter it like so (it is helpful to read the whole file):

# PAM configuration for the Secure Shell service
...
# SELinux needs to be the first session rule.  This ensures that any
# lingering context has been cleared.  Without this it is possible that a
# module could execute code in the wrong domain.
session [success=ok ignore=ignore module_unknown=ignore default=bad]        pam_selinux.so close
...

# INSERT HERE
# send mail on new session
session    optional     pam_exec.so /path/to/mail-ssh.sh

# SELinux needs to intervene at login time to ensure that the process starts
# in the proper default security context.  Only sessions which are intended
# to run in the user's context should be run after this.
session [success=ok ignore=ignore module_unknown=ignore default=bad]        pam_selinux.so open
...

Notice that we inserted our line in the part where our code can be run as root. This is not a good practise but we need to be able to access files (msmtprc - mail password and our script - we don’t want anybody else to look), which will be readable only by root! Also, see my note at the end.

Writing the final script

Now we can begin writing our final script which will harness env variables given by PAM. These are (read SE link above):

PAM_SERVICE=sshd
PAM_RHOST=<IP>
PAM_USER=<USER>
PWD=/
SHLVL=1
PAM_TYPE=open_session # or close_sesion
PAM_TTY=ssh
_=/usr/bin/env

Perfect. We can use $PAM_TYPE to determine when a session is opened (or both opened/closed)\ if you want). And $PAM_RHOST with $PAM_USER to determine who and from where logged in.

We can also determine some more information about the IP, like geographical info. For that, ipinfo.io API can be used. Try it yourself, ipinfo.io/IP in browser. This API determines the form of output based on your User-Agent. So when we curl ipinfo.io/IP, you can see nice JSON output:

{
  "ip": "IP",
  "hostname": "HOSTNAME",
  "city": "CITY",
  "region": "REGION",
  "country": "COUNTRY_CODE",
  "loc": "GPS",
  "org": "ISP",
  "postal": "POSTAL_CODE",
  "timezone": "TIMEZONE",
  "readme": "https://ipinfo.io/missingauth"
}

And finally, the script is going to send detailed information about the SSH connection together with its destination and origin. We can harness the authorized pubkey for additional identifying information located at the end of it. Now create 700 root:root file /path/to/mail-ssh.sh with following content.

#!/bin/sh
# mail-ssh.sh: send mail on user ssh connection

# send mail only on connect, exit on disconnect
[ "$PAM_TYPE" != "open_session" ] && exit 0

main () {
        # get target user's home to get information from pubkey
        user_home="$(getent passwd "$PAM_USER" | cut -d: -f6)"
        pubkey="$(echo "$SSH_AUTH_INFO_0" | cut -d' ' -f3)"

        # get name from the authorized pubkey, works also with additional command="..." structure in the authorized_keys file
        ssh_pubkey_user="$(grep "$pubkey" "$user_home/.ssh/authorized_keys" | rev | cut -d' ' -f1 | rev)"

        # IP address of the user who connected to the server
        user_ip=$PAM_RHOST # PAM session
        ip_info=$(curl -s "ipinfo.io/${user_ip}" | python3 -c "import sys, json; l=json.load(sys.stdin); print(l['timezone'], l['country'], l['region'], l['city'], sep=';')")

        # load input to variables
        IFS=";" read -r timezone country region city <<< "$ip_info"
        datetime=$(date "+%d/%m/%Y %H:%M")

        subject="$ssh_pubkey_user logged in via ssh"
        html="<html><head>
        <style> .user { color: red; }</style>
        </head><body><meta charset=\"UTF-8\">
        <p>Date: ${datetime}</p>
        <h1>$ssh_pubkey_user</h1>
        <p>logged in as a local user <<span style=\"font-style: monospace;\">${PAM_USER}@$HOSTNAME</span>> from</p>
        <h3>IP ${user_ip}</h3>
        <h3>GeoIP location:</h3>
        <ul><li>${timezone}, ${country}</li><li>Region: ${region}</li><li>City: ${city}</li></ul>
        </html>"

        # sender is the mail account we configured earlier; receiver and sender can be the same address
        echo "$html" | mailx -a 'Content-Type: text/html; charset="UTF-8"' -s "${subject}" -r "Log Mailer <sender@example.com>" "Me <receiver@example.com>"
}

# do not block, there is no way to validate login at the moment anyway
main &

where we used mailx to send a HTML message with UTF-8 encoding. If something is not clear, try explainshell.com. Try to login to your server and you’re done. If no mail came, try to see the logs and grep for sshd:session.

Mobile delivery

I would also like to mention an excellent Android open-source mail app K9 Mail. It is lightweight, you can setup multiple account and most importantly, you can setup PUSH notifications. That means that instead of polling in intervals, the mail server notifies the app instantly when some email from the account with enabled PUSH comes.


Final NOTE

In this post, there is mentioned a way to setup mstmtp only for a root user. But it is a good idea to create some logger user and in its home directory create restricted mail config .msmtprc. And subsequently, somehow rewrite pam config such that our final script would run as this low priviliged logger user. It is not wise to run scripts as root when it is not mandatory. I hope to rewrite it someday, I was just too tired.


Update [05/09/2020]

Updated code for mail.sh to not block the login process,


That’s all.