Maker.io main logo

How To Monitor Login Attempts on a Raspberry Pi

2025-11-17 | By Maker.io Staff

Single Board Computers Raspberry Pi SBC

Small, connected, always-on devices can quickly become targets of attacks. Tracking user logins is an effective way to assess threat levels, and it serves as the basis for subsequent security measures. Read on to learn how to monitor login attempts on a Raspberry Pi and log them to the console.

Why Does Login Monitoring Matter?

Most services for accessing Linux devices remotely use a well-known port. After a device is connected to the Internet, attackers can scan for open ports and attempt to gain unauthorized access to the device and network to spread malware or to add it to a botnet.

While logging alone does not automatically prevent or stop attacks, monitoring the logs for anomalies helps detect malicious activity. Besides remote hackers, local users might try to gain unauthorized access. Finally, logs can help detect unused accounts or unusual activity, such as users logging in during unusual hours.

Common Login Sources

Most system services for managing users and logins record their events in the system journal. Prominent sources include SSH, the local shell, and VNC, with SSH remaining the most frequent target of attackers. Custom applications, like a specific REST API, can also be included, as long as they log to the system journal or another accessible location.

In older Raspberry Pi OS releases, these attempts were written to plain text files under /var/log, such as auth.log. Starting with Bookworm, the system uses the system journal to centralize all login-related events. The entries can be inspected using the journalctl command.

For example, to show SSH login attempts from the past 24 hours, run:

Copy Code
journalctl -t sshd --since "24 hours ago"

General Log Monitor Workflow

The first step when developing a login monitor is to identify the services to include. The monitor then needs to read journal entries and extract the relevant information. Each service produces slightly different messages, but they typically include a timestamp, the username, the source address if applicable, and whether the login attempt was successful. To simplify parsing, journalctl can return the results as JSON. Still, by converting this information into a standardized internal format, the monitor can treat all sources the same way.

After normalizing the data, the monitor can display or store the results. Advanced features such as filtering, searching, or sorting make it easier to work with the data. The converted contents can also be displayed on a website or stored in a database, for even more powerful data processing.

Installing Required Packages

This login monitor program mostly utilizes standard Python functions. However, it relies on the tabulate package to generate a nicely formatted output table with the results. The package can be installed in a virtual environment via pip:

Copy Code
pip install tabulate

Writing a Python-Based Login Monitor

Start by creating a class that defines the standardized format of the data from the individual log files:

Copy Code
class LogEntry:

    def __init__(self, service, user, source, success, time):
        self.service = str(service)
        self.user = str(user)
        self.source = str(source)
        self.success = "Success" if success else "Failure"
        self.time = str(time)

    def __iter__(self):
        return iter((self.service, self.user, self.source, self.success, self.time))

The constructor takes in the extracted information from the logs and stores it together in a convenient object. The __iter__ function makes the LogEntry class iterable, which is helpful when generating the output table.

Next, define a class that describes the services to extract from the log. This class identifies relevant journal entries, which is necessary because the journal stores all entries together:

Copy Code
class ServiceParameters:

    def __init__(self, name, success_string, failure_string, user_pattern):
        self.name = name
        self.failure_string = failure_string
        self.success_string = success_string
        self.user_pattern = user_pattern

The class contains the human-readable service name, the strings that identify successful and failed login attempts, and a regular expression for extracting the user information from the target service’s log messages in the system journal.

Next, create a class for the main application, and import the required libraries:

Copy Code
import subprocess, json, re

from tabulate import tabulate
from datetime import datetime
from log_entry import LogEntry
from service_parameters import ServiceParameters

Create a map with the service identifiers as the keys and the service descriptions as values:

Copy Code
SERVICE_MAP = {
    "sshd": ServiceParameters(
        name="ssh",
        success_string="accepted password",
        failure_string="failed password",
        user_pattern=re.compile(r"for .*?(\w+) from (\S+)")
    ),
    "lightdm": ServiceParameters(
        name="local",
        success_string="session opened",
        failure_string="authentication failure",
        user_pattern=re.compile(r"user[=\s](\w+)")
    )
}

For example, the sshd service entry represents the login attempts recorded by the ssh daemon. The human-readable name is ”ssh”, and successful login messages contain the string “accepted password”. In contrast, the journal lists failed attempts as “failed password” messages. Finally, the user name in the message comes between the keywords “for” and “from”.

Image of How To Monitor Login Attempts on a Raspberry Pi This screenshot shows examples of successful (green) and failed (red) login attempts as they are recorded in the system journal.

The main program logic runs the journalctl command and stores the results in a local variable. It then processes each line of the JSON output:

Copy Code
def parse_journal():
    entries = []
    for line in subprocess.Popen(["journalctl", "--output", "json"], stdout=subprocess.PIPE, text=True).stdout:
        try:
            identifier, timestamp, message = extract_line_data(line)
            service_parameters = SERVICE_MAP.get(identifier)
            # Skip unwanted services
            if not service_parameters:
                continue
            # Only keep login success & failed messages
            if not service_parameters.success_string in message and not service_parameters.failure_string in message:
                continue
            success = service_parameters.success_string in message
            user, source = extract_user(service_parameters, message)
            # Hide system "login" attempts (e.g., when going back to the user-select screen in the UI)
            if not user or user == identifier:
                continue
            entries.append(LogEntry(service_parameters.name, user, source, success, timestamp))
        except Exception as e:
            print(f"Error: {e}")
    return entries

The entries array holds the relevant log entries, converted into the program’s internal format. The for loop iterates all log entries returned by journalctl. For each line, it extracts the service identifier, the timestamp, and the recorded message with the help of a short utility function. The program then checks whether the current log entry references one of the target services, defined in the SERVICE_MAP at the start of the program. It then checks whether the log message contains the required keywords that characterize login attempts. Lastly, it converts the extracted data into its internal format and appends the new entry to the result list.

The extract_line_data helper function looks as follows:

Copy Code
def extract_line_data(line):
    line_data = json.loads(line)
    message = str(line_data.get("MESSAGE", "")).lower().strip()
    identifier = str(line_data.get("SYSLOG_IDENTIFIER", "")).lower().strip()
    timestamp = line_data.get("__REALTIME_TIMESTAMP", None)
    if timestamp:
        timestamp = str(datetime.fromtimestamp(int(timestamp) / 1_000_000))
    else:
        timestamp = "N/A"
    return identifier, timestamp, message

It takes the JSON line, extracted from the system log, and extracts the variables and their values using Python’s standard JSON library. It then attempts to fetch the message, identifier, and timestamp, defaulting to empty strings or “N/A” if the requested value does not appear in a log entry.

The extract_user function performs a regular expression search in the extracted log message using the target service’s success and failure patterns, defined in the SERVICE_MAP:

Copy Code
def extract_user(service_parameters, message):
    user, source = "N/A", "N/A"
    user_data = service_parameters.user_pattern.search(message)
    if user_data:
        user = user_data.group(1)
        if user_data.lastindex == 2:
            source = user_data.group(2)
    return user, source

Lastly, the program’s main function calls the parse_journal method and outputs the result as a neatly formatted table, generated using tabulate:

Copy Code
if __name__ == "__main__":
    results = parse_journal()
    if not results:
        print("No journal entries found")
    else:
        table_headers = ["Service", "User", "Source", "Status", "Time"]
        print(tabulate(results, headers=table_headers, tablefmt="pretty"))

Executing the program with Python should result in output similar to this:

Image of How To Monitor Login Attempts on a Raspberry Pi This screenshot demonstrates the output generated by the login attempt monitor program.

Summary

Monitoring login attempts on a Raspberry Pi helps detect unauthorized access and unusual activity, whether from remote attackers or local users. The main sources for user logins are SSH, the local console, and VNC. Each of these tracks login events in the system journal that can be analyzed. Note that logins via VNC appear as local login attempts in the logs.

The process of writing a login attempt monitor starts with deciding which services to include. Most services add log messages using a custom format, so the next step is to convert each relevant line into a standardized structure. Finally, the collected entries can be displayed, filtered, or stored for further analysis.

제조업체 부품 번호 SC1176
SBC 1.0GHZ 4 CORE 512MB RAM
Raspberry Pi
₩22,324
더 보기 Details
제조업체 부품 번호 SC1237
RASPBERRY PI 500+ - US (UNIT ONL
Raspberry Pi
₩318,781
더 보기 Details
제조업체 부품 번호 SC1113
SBC 2.4GHZ 4 CORE 16GB RAM
Raspberry Pi
₩178,589
더 보기 Details
제조업체 부품 번호 SC0373
SBC 1.8GHZ 4 CORE 4GB RAM
Raspberry Pi
₩117,199
더 보기 Details
제조업체 부품 번호 SC0195(9)
SBC 1.5GHZ 4 CORE 8GB RAM
Raspberry Pi
₩111,618
더 보기 Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.