1. Introduction

The Secure Shell (SSH) protocol is the de facto standard for secure remote access to Linux machines. So, systems usually have at least a server like OpenSSH installed and configured for inbound access. Without additional means, this can leave an environment exposed to brute-force attack attempts.

In this tutorial, we explore how to run scripts and get notified in case of a successful SSH login attempt. First, we delve into the standard way that Linux and applications within handle authentication. After that, we leverage that to create an SSH login notification mechanism. Finally, we briefly go through several steps for hardening an SSH server.

Critically, if working remotely, any SSH configuration modification steps should be performed while a backup session is open in case of a connection loss.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. It should work in most POSIX-compliant environments unless otherwise specified.

2. Pluggable Authentication Modules (PAM)

As with many other security tools in Linux, when it comes to authentication, it’s usually best to work with Pluggable Authentication Modules (PAM).

To use PAM, we need several prerequisites:

  • installed libpam
  • PAM-aware application
  • proper configuration
  • PAM modules

With these in place, we can move on to a basic example.

2.1. Structure

Let’s understand a usual scenario for PAM:

                                     +-----------+ 
                                     |/etc/pam.d/| 
                                     +-----+-----+ 
                                           |       
+--------+      +------------+      +------+------+
|        |----->|            |----->|             |
| Client |      |   Server   |      | PAM Library |
|        |<-----|            |<-----|             |
+--------+      +------------+      +-------------+
                                    |    +-------+
                                    +--- |mod1.so|
                                    |    +-------+
                       Pluggable    |
                    Authentication  |    +-------+
                        Modules     +--- |mod2.so|
                                    |    +-------+
                                    |       ...    
                                    |    +-------+
                                    +--- |modN.so|
                                         +-------+

In this example diagram, a Client makes a request to a Server. Based on the context and contents of that request, the server can turn to a specific module for further processing via libpam, the PAM Library. In a way, modules are the functions that libpam offers to PAM-aware applications. Their configuration is under /etc/pam.d/.

This dynamic is useful when we need to separate the authentication from the application that requires it. In this case, Server communicates with the PAM library.

2.2. General Configuration

To use PAM, we first have to set it up for our application of choice. This is done via a file in /etc/pam.d/, named after the process we want to configure:

$ cat /etc/pam.d/su
auth  sufficient  pam_permit.so

There are three required whitespace-separated columns in each line:

  1. activity type: auth, account, session, password
  2. control flag: required, requisite, sufficient, optional
  3. PAM name: usually ends with .so

After these, we can have parameters that the module takes.

In this case, we use the pam_permit.so module, which doesn’t take any parameters and always succeeds, to allow any execution of su without needing to authenticate:

$ su root
# whoami
root

Here, we see a passwordless transition to root, as verified by whoami.

2.3. Activity Types

Modules can work on different activities:

  • auth: verify identity by validating credentials such as password, key, token, and others
  • account: verify authorization for activity such as permissions, restrictions, and similar
  • session: allocate resources during login such as personal data, limits, and others
  • password: update credentials

Notably, the order above more or less covers the priority and frequency of the activities.

2.4. Control Flags

Modules either pass or fail, returning the appropriate result. Notably, some failures are more important than others:

  • requisite: if fail, return fail to application and stop
  • required: if fail, return fail to application and continue
  • sufficient: if pass, return pass to application and stop, unless we already failed a required module
  • optional: operational module, the result of which is ignored

Importantly, a failed required precedes a passed sufficient module. Naturally, the order of modules is very important in a configuration file.

2.5. Modules and Parameters

Many modules exist in the default libpam installation:

  • pam_unix (auth, session, password): authenticate against /etc/shadow.
  • pam_limits (session): limit system resource usage during a user session via /etc/security/limits.conf and /etc/security/limits.conf
  • pam_exec (auth, account, session, password): run external command or script
  • pam_rootok (auth): pass if root, fail if not
  • pam_cracklib (password): verify password complexity against rules (parameters)
  • pam_permit (auth, account, session, password): pass
  • pam_deny (auth, account, session, password): fail
  • pam_warn (auth, account, session, password): log message to system log
  • pam_motd (session): display message of the day (MotD)
  • pam_wheel (auth, account): allow root access only if in wheel group
  • pam_nologin (auth): prevent non-root login if /etc/nologin exists

Some of these modules support different parameters for configuring specifics such as password complexity rules, commands or scripts to execute, and others.

2.6. Full Example

Let’s check and understand the PAM configuration file for cron:

$ cat /etc/pam.d/cron
# The PAM configuration file for the cron daemon

@include common-auth

# Sets the loginuid process attribute
session    required     pam_loginuid.so

# Read environment variables from pam_env's default files, /etc/environment
# and /etc/security/pam_env.conf.
session       required   pam_env.so

# In addition, read system locale information
session       required   pam_env.so envfile=/etc/default/locale

@include common-account
@include common-session-noninteractive

# Sets up user limits, please define limits for cron tasks
# through /etc/security/limits.conf
session    required   pam_limits.so

Here, we first include common-auth configuration, which includes basic pam_unix requirements and requisites. After that, we ensure the user ID is set via pam_loginuid and prepare the shell environment and locale with pam_env. Next, we ensure authorization and a non-interactive session via common-account and common-session-noninteractive respectively. Finally, we set the required pam_limits.

3. SSH Notifications

As we saw, the pam_exec module enables the execution of commands and scripts during the PAM chain.

In addition, it exports several environment variables to the context of the running command or script:

  • PAM_RHOST: remote host
  • PAM_RUSER: remote user
  • PAM_SERVICE: name of the application that uses PAM
  • PAM_TTY: current TTY
  • PAM_USER: current user
  • PAM_TYPE: account, auth, password, open_session, or close_session

So, let’s apply that to create an SSH notification setup:

$ cat /etc/pam.d/sshd
[...]
# Standard Un*x session setup and teardown.
@include common-session

# Custom SSH notifications
session optional pam_exec.so seteuid /root/ssh-login-notify.sh [email protected] [email protected]
[...]

Here, we add a line to /etc/pam.d/sshd, the PAM configuration file of the SSH daemon, just below the session setup via common-session. Also, we indicate the addition via a comment that describes it.

The added line uses pam_exec as an optional activity for the current session. In particular, we run the /root/ssh-login-notify.sh script with the effective user ID (seteuid) and two arguments: the sender and recipient e-mail addresses. Thus, we ensure the script executes after the session is established.

Now, let’s see the contents of /root/ssh-login-notify.sh:

$ cat /root/ssh-login-notify.sh
#!/bin/sh

sender="$1"
recipient="$2"

if [ "$PAM_TYPE" != "close_session" ]; then
  host="$(hostname)"
  subject="SSH Login: $PAM_USER from $PAM_RHOST on $host"
  message="$(env)"
  echo "$message" | mail --subject="$subject" --return-address="$sender" "$recipient"
fi

In this basic sh script, we initialize the sender and recipient variables via the first and second script arguments respectively. After that, we check whether the special $PAM_TYPE variable indicates any activity but the end of a session. If so, we compile a $message and $subject. In our case, the message contains the current environment as returned by env, while the subject states SSH Login as the event and includes the user, as well as the local and remote hosts.

After having all the data in place, we can use any way to send the actual mail. In this case, we employ the mail application. However, we can also use a curl script to do the same:

$ cat xsendmail.sh
#!/usr/bin/env bash

SERVER='smtp.example.com'
PORT='465'
USER="$1"
PASS='PASSWORD'
SENDER_ADDRESS="$USER"
SENDER_NAME='server'
RECIPIENT_NAME='notify'
RECIPIENT_ADDRESS="$2"
SUBJECT="$3"
MESSAGE="$(cat)"

curl --ssl-reqd --url "smtps://$SERVER:$PORT" \
    --user "$USER:$PASS" \
    --mail-from "$SENDER_ADDRESS" \
    --mail-rcpt "$RECIPIENT_ADDRESS" \
    --header "Subject: $SUBJECT" \
    --header "From: $SENDER_NAME <$SENDER_ADDRESS>" \
    --header "To: $RECIPIENT_NAME <$RECIPIENT_ADDRESS>" \
    --form '=(;type=multipart/mixed' --form "=$MESSAGE;type=text/plain" --form '=)'

Of course, we have to make all involved scripts executable:

$ chmod +x /root/ssh-login-notify.sh

Importantly, we should also ensure that the UsePAM yes directive is in the sshd_config file:

$ cat /etc/ssh/sshd_config
[...]
# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication and
# PasswordAuthentication. Depending on your PAM configuration,
# PAM authentication via ChallengeResponseAuthentication may bypass
# the setting of "PermitRootLogin without-password".
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and ChallengeResponseAuthentication to 'no'.
UsePAM yes
[...]

At this point, logging in via SSH should generate a notification by e-mail.

4. SSH Server Security

Alerts don’t actively protect the system. Thus, it’s almost always a good idea to tighten up security in addition to setting up alarms:

Only by taking these further measures would monitoring the SSH logs and sending notifications be an adequate additional step.

5. Summary

In this article, we saw a robust way to execute scripts and send notifications at each SSH login.

In conclusion, unlike other solutions such as using the $HOME/sshrc file, using PAM only expects a raw SSH session setup to work reliably.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.