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
andRECIPIENT
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 eitheropen_session
orclose_session
. Since we need to send notifications when a session is opened, we filter outclose_session
; PAM_USER
will be the name of the authenticated user,PAM_RHOST
will be the name of the remote (connecting) host. The value ofPAM_RHOST
depends on several factors:- if
UseDNS
is set tono
insshd_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.
- if
tail /var/log/auth.log
(ortail /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 usingForceCommand
in the server configuration orCommand=
in theauthorized_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
. Thus if~/.bash_profile
,~/.bash_login
, and~/.profile
, in that order, and reads and executes commands from the first one that exists and is readable~/.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 andPermitUserRC=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.