Realtime login monitoring with Pushover

See Part 2.

My previous article was about securing your Raspberry Pi and having it alert you via email whenever something suspicious occurred. This time I’m taking it a step further, and adding real time push notifications whenever somebody logs in via ssh.

For delivering the notifications I’m using Pushover, which is a generic push service, with clients for both iOS and Android. The app isn’t free, but once you purchase it, the service is free up to 7500 messages per app, per month, which is more than enough for this purpose.

Assuming you’ve already purchased the app, you start by registering a new application with Pushover.

I’ve created a small python script for monitoring /var/log/auth.log for whenever a user logs in, but when I wrote it I took into consideration that it should be easy to extend for monitoring more files.

Here is the script


    #!/usr/bin/env python
    import httplib
    import urllib
    import subprocess
    import re
    from threading import Thread
    from socket import gethostname
    
    try:
        from queue import Queue
    except ImportError:
        from Queue import Queue
    
    class PushoverNotification(object):
        _token = "PUSHOVER_APP_TOKEN"
        _user = "PUSHOVER_USER_TOKEN"
    
        @property
        def token(self):
            return self._token
    
        @token.setter
        def token(self,value):
            self._token = value
    
        @property
        def user(self):
            return self._user
    
        @user.setter
        def user(self, value):
            self._user = value
    
        def __init__(self):
            pass
    
        def send_notification(self,message,token=None, user=None):
            if token is None:
                token = self.token
            if user is None:
                user = self.user
            conn = httplib.HTTPSConnection("api.pushover.net:443")
            conn.request("POST", "/1/messages.json",
            urllib.urlencode({
                "token": token,
                "user": user,
                "message": message,
            }), { "Content-type": "application/x-www-form-urlencoded" })
            resp = conn.getresponse()
    
            if resp.status != 200:
                raise Exception("Error : %d " % resp.status)
    
    class LogWatcher(object):
        _patterns = list()
    
        class WatcherThread(Thread):
            def __init__(self, queue, filename, patterns, prefix=""):
                super(LogWatcher.WatcherThread,self).__init__()
                self._queue = queue
                self._filename = filename
                self._patterns = patterns
                self._prefix = prefix
    
            def run(self):
                cmd = ['tail','-F',self._filename]
                p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
                for l in iter(p.stdout.readline,''):
                    for m in self._patterns:
                        match = m.match(l)
                        if match is not None:
                            self._queue.put("%s%s" %(self._prefix,l)
                            break
    
    
        def __init__(self):
            self._queue = Queue()
    
        def monitor_file(self, filename, patterns, prefix = ""):
            monitored_patterns = list()
    
            for pat in patterns:
                if isinstance(pat,str):
                    p = re.compile(pat)
                    if p is not None:
                        monitored_patterns.append(p)
                    else:
                        raise Exception("Error compiling pattern:\"%s\"" % pat)
                else:
                    monitored_patterns.append(pat)
    
            t = LogWatcher.WatcherThread(self._queue, filename, patterns, prefix)
            t.start()
    
        def iterate_matches(self):
            r = self._queue.get()
            yield r
            self._queue.task_done()
    
    
    def main():
    
        p = PushoverNotification()
        l = LogWatcher()
    
        pattern = re.compile("^.*sshd\\[[0-9]+\\]: [^ ]+ session opened for user ([^ ]+).*$")
        l.monitor_file('/var/log/auth.log',(pattern,),"%s:" % gethostname())

        for user in l.iterate_matches():
            p.send_notification("%s" % user)
    
    if __name__ == '__main__':
        main()

In the main() function, the “rules” for monitoring is setup, and the following lines

    pattern = re.compile("^.*sshd\\[[0-9]+\\]: [^ ]+ session opened for user ([^ ]+).*$")
    l.monitor_file('/var/log/auth.log',(pattern,),"%s:" % gethostname())

setup a regular expression looking for the login pattern, and register a file for monitoring. Multiple patterns can be specified per file.

The script then starts a new thread for monitoring the file, and whenever a line matches a regular expression, the line is put on the queue, and a push notification is sent.

To use the script, fill in your Pushover application token, along with your Pushover user token and save it to /usr/local/bin/pushover.py and set execute permissions on it.

  $sudo chmod 700 /usr/local/bin/pushover.py

For a quick test, try running the script

  $sudo /usr/local/bin/pushover.py

Next, try logging in from another terminal. You should receive a push notification from Pushover. If you don’t receive a notification, go back and check that you filled in the tokens correctly.

Setting it up as a service

Next we’re gonna register the script as a service that starts up whenever the system reboots. Save the following shell script to /etc/init.d/pushover

    #!/bin/sh
    ### BEGIN INIT INFO
    # Provides: pushover
    # Required-Start: $local_fs $remote_fs
    # Required-Stop: $local_fs $remote_fs
    # Should-Start: $network
    # Should-Stop: $network
    # Default-Start: 2 3 4 5
    # Default-Stop: 0 1 6
    # Short-Description: Daemonized version of pushover
    # Description: Starts the pushover daemon for realtime login monitoring.
    ### END INIT INFO
    
    DAEMON=/usr/local/bin/pushover
    
    start() {
        echo "Starting pushover"
        start-stop-daemon -b -o -c root -S -u root -x $DAEMON --
    }
    
    stop() {
        dbpid=`pgrep -fu root $DAEMON`
        if [ ! -z "$dbpid" ]; then
            echo "Stopping pushover"
            start-stop-daemon -o -c root -K -u root -x $DAEMON
        fi
    }
    
    status() {
        dbpid=`pgrep -fu root $DAEMON`
        if [ -z "$dbpid" ]; then
            echo "pushover: not running."
        else
            echo "pushover: running (pid $dbpid)"
        fi
    }
    
    case "$1" in
        start)
            start
            ;;
        stop)
            stop
            ;;
        restart|reload|force-reload)
            stop
            start
            ;;
        status)
            status
            ;;
        *)
            echo "Usage: /etc/init.d/pushover {start|stop|reload|force-reload|restart|status}"
            exit 1
    esac
    
    exit 0

and set permissions on it

$sudo chmod 700 /etc/init.d/pushover

Next we’ll register the service for automatic startup

$sudo update-rc.d pushover defaults

And start the service with

$sudo service pushover start

And that’s it. You should now receive a push notification every time someone logs into your Raspberry Pi.

Extending the script

The script above is what i use for now, but as i promised the script is easy to extend. All you need is to provide a regular expression, and a file to monitor. I recommend using something like Reggy for testing your expressions.

E.g. if you’d like a notification every time fail2ban bans an IP, the following regular expression will filter that out

"^.*fail2ban.actions: WARNING \\[ssh\\] Ban [0-9]+.[0-9]+.[0-9]+.[0-9]+$"

and to add it, you add the following code

    pattern = re.compile("^.*fail2ban.actions: WARNING \\[ssh\\] Ban [0-9]+.[0-9]+.[0-9]+.[0-9]+$")
    l.monitor_file('/var/log/fail2ban.log',(pattern,),"%s:" % gethostname())

main() should look like this after adding

    def main():
    
        p = PushoverNotification()
        l = LogWatcher()
    
        pattern = re.compile("^.*sshd\\[[0-9]+\\]: [^ ]+ session opened for user ([^ ]+).*$")
        l.monitor_file('/var/log/auth.log',(pattern,),"%s:" % gethostname())

        pattern = re.compile("^.*fail2ban.actions: WARNING \\[ssh\\] Ban [0-9]+.[0-9]+.[0-9]+.[0-9]+$")
        l.monitor_file('/var/log/fail2ban.log',(pattern,),"%s:" % gethostname())

        for user in l.iterate_matches():
            p.send_notification("%s logged in" % user)

I may add a config file later on to make it easier to add rules, but for now some Python knowledge is required.


See also