Sometimes it can be useful to get notified when someone logs into the system via SSH. In this post I am going to show several possible solutions.

For all solutions to work, we will need to have an utility that sends emails. I prefer mailx (from mailx package in RHEL-based distros or bsd-mailx / mailutils in Debian-based distros). Also, I assume that you use OpenSSH as a server; other SSH servers may have their own peculiarities.

Using PAM Authentication

This solution relies upon pam_exec module

Prerequisites: UsePAM yes in /etc/ssh/sshd_config.

First of all, we will need a script which sends a notification (let its name be /usr/local/bin/notify.sh). It can look something like this:

#!/bin/sh

SENDER="[email protected]"
RECIPIENT="[email protected]"

if [ "$PAM_TYPE" != "close_session" ]; then
    host="$(hostname)"
    subject="SSH Login: $PAM_USER from $PAM_RHOST on $host"
    message="$(date)
$(tail /var/log/auth.log) # /var/log/secure for RHEL-based
$(who)"
    echo "$message" | mailx -r "$SENDER" -s "$subject" "$RECIPIENT"
fi
exit 0

The script needs to be executable: chmod 0755 /usr/local/bin/notify.sh

Then, we will need to add this line to /etc/pam.d/sshd:

session    optional     pam_exec.so /usr/local/bin/notify.sh

A brief explanation as to what the script does:

  • SENDER and RECIPIENT define the sender and recipient of the email; RECIPIENT is obviously the person who is going to receive these notifications;
  • Since pam_exec is configured to be used as a session module, PAM_TYPE variable can be either open_session or close_session. Since we need to send notifications when a session is opened, we filter out close_session;
  • PAM_USER will be the name of the authenticated user, PAM_RHOST will be the name of the remote (connecting) host. The value of PAM_RHOST depends on several factors:
    • if UseDNS is set to no in sshd_config, the value will be the IP address of the remote host;
    • otherwise, if the remote IP passes the FCrDNS check, the value will match that of the PTR record of the remote host (and you can get the IP with the help of gethostbyname() or a similar function);
    • otherwise, the value will be the IP address of the remote host.
  • tail /var/log/auth.log (or tail /var/log/secure for RHEL-based distros) will show the last ten lines from the authentication log; chances are those will be the lines related to the connecting host.

Using ~/.bash_login or /etc/profile

Another possibility is to use /etc/profiles (system-wide) or ~/.bash_login (per-user).

Prerequisites: bash is set as a login shell.

Note: a similar approach may work with other login shells, this depends on what files they process during startup.

When bash is invoked as an interactive login shell, or as a non-interactive shell with the --login option, it first reads and executes commands from the file /etc/profile, if that file exists. After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable.

If we need to send notifications only when someone logs in via SSH, we can use several environment variables set by sshd:

  • SSH_CLIENT: it shows the address of the client system, the outgoing port number on the client system, and the incoming port on the server;
  • SSH_CONNECTION shows the address of the client, the outgoing port on the client, the address of the server and the incoming port on the server;
  • SSH_TTY names the pseudo-terminal device on the server used by the connection;
  • SSH_ORIGINAL_COMMAND is set when the login session can be constrained to a single program with a predetermined set of parameters (either using ForceCommand in the server configuration or Command= in the authorized_keys file.

The very basic check is to verify whether SSH_CONNECTION environment variable is set; if it is, echo "$SSH_CONNECTION" | awk '{ print $1 }' will give us the IP address of the client system.

If we want to avoid notifications when a nested login shell is started (e.g., bash -l), we can check the value of SHLVL environment variable (its value is incremented by one each time an instance of bash is started); for the very first shell the value of SHLVL is 1.

The code to send the notification will look like this:

SENDER="[email protected]"
RECIPIENT="[email protected]"

if [ -n "$SSH_CONNECTION" -a "$SHLVL" = "1" ]; then
    remote=$(echo "$SSH_CONNECTION" | awk '{ print $1 }')
    user=$(id -un)
    host="$(hostname)"
    subject="SSH Login: $user from $remote on $host"
    message="$(date)
$(who)"
    echo "$message" | mailx -r "$SENDER" -s "$subject" "$RECIPIENT"
fi

unset SENDER
unset RECIPIENT
unset remote
unset user
unset host
unset subject
unset message

It is probably not the best idea to add this code directly to /etc/profile; for example, CentOS says:

# It’s NOT a good idea to change this file unless you know what you
# are doing. It’s much better to create a custom.sh shell script in
# /etc/profile.d/ to make custom changes to your environment, as this
# will prevent the need for merging in future updates.

Debian also seems to support /etc/profile.d/:

if [ -d /etc/profile.d ]; then
  for i in /etc/profile.d/*.sh; do
    if [ -r $i ]; then
      . $i
    fi
  done
  unset i
fi

However, in this case you probably need to be aware of some distro-specific things:

  • In Debian-based distros, /etc/bash.bashrc is sourced by /etc/profile; /etc/bash.bashrc by default does not use scripts from /etc/profile.d/.
  • In RHEL-based distros, both /etc/bashrc and /etc/profile source scripts from /etc/profile.d/. However, /etc/bashrc uses only /etc/profile.d/*.sh, and /etc/profile uses /etc/profile.d/*.sh and /etc/profile.d/sh.local.

If you need a universal script which runs both in RHEL and Debian-based environments, you probably need to use check shopt -q login_shell (it returns zero if the current shell is a login shell and non-zero otherwise).

If you want to use the notification script on a per-user basis, and put it into users’ home directories, you need to be aware of a few things:

  • execution order: bash looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable. Thus if ~/.bash_profile exists and is readable, ~/.bash_login will not be processed.
  • you will need to protect ~/.bash_profile (or ~/.bash_login or whichever file you use) so that the user is unable to remove the notification code from it (for that it is not necessary to edit the file; it is enough to rename or delete it).

Using /etc/ssh/sshrc or ~/.ssh/sshrc

This approach is similar to the previous one but it relies upon OpenSSH functionality rather than the login shell.

If the file ~/.ssh/rc exists, sh(1) runs it after reading the environment files but before starting the user’s shell or command. It must not produce any output on stdout; stderr must be used instead.

If this file does not exist, /etc/ssh/sshrc is run

Also note that the execution of ~/.ssh/rc can be disabled if PermitUserRC is set to no in the SSH system configuration file.

The notification script will be almost the same as for the previous case; the only exception is that we do not need to check whether this shell is a login shell, as the script is invoked by SSH (well, it is still possible that the user runs this script manually though).

( \
    SENDER="[email protected]" \
    RECIPIENT="[email protected]" \
\
    remote=$(echo "$SSH_CONNECTION" | awk '{ print $1 }') \
    user=$(id -un) \
    host="$(hostname)" \
    subject="SSH Login: $user from $remote on $host"
message="$(date)
$(who)" \
    echo "$message" | mailx -r "$SENDER" -s "$subject" "$RECIPIENT" \
) > /dev/null

Things you need to be aware of:

  • if ~/.ssh/rc exists and PermitUserRC=yes, then /etc/ssh/sshrc is not run;
  • if you rely upon if ~/.ssh/rc, you need to make sure that the user is unable to delete or rename that file or edit it to remove the notification code.
How to Log Successful SSH Login Attempts
Tagged on:             

Leave a Reply

Your email address will not be published. Required fields are marked *