For any headless Raspberry Pi deployment in 2026, the optimal SSH baseline is non-negotiable: disable password authentication entirely, enforce Ed25519 key pairs, change the default port to 2222, and disable root login. Relying on default credentials or password-based SSH on a network-exposed Pi is the primary vector for brute-force botnets. This guide moves past basic raspi-config toggles to build a physical SSH Sentinel monitor using a Raspberry Pi 5 and an I2C OLED, while providing a definitive decision framework for hardening your sshd_config.

Parts List and Pin Mapping for the SSH Sentinel

This build targets the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (Bookworm or Trixie 64-bit). The Pi 5's updated RP1 I/O chip handles I2C and GPIO polling with lower latency than the Pi 4, making it ideal for real-time log monitoring without CPU spiking. Ensure you are using the official 27W USB-C PD power supply; the Pi 5 will throttle PCIe and GPIO current limits if it detects a brownout or an inadequate 5V/3A brick.

Pro Tip: When wiring I2C on the Pi 5, keep your SDA/SCL lines under 30cm and avoid running them parallel to the PWM fan lines to prevent display flicker caused by electromagnetic interference.

Bill of Materials

  • Compute: Raspberry Pi 5 (8GB) with Official Active Cooler
  • Display: Waveshare 1.3" I2C OLED (SH1106 controller, 128x64 resolution)
  • Indicator: 5mm Green LED with 330Ω current-limiting resistor
  • Control: 12x12mm Momentary Tactile Switch (hardware network kill)
  • Wiring: 24 AWG solid core silicone wire, female-to-female Dupont jumpers

GPIO Pin Mapping Table

ComponentComponent PinPi 5 GPIO / Physical PinNotes
OLEDVCC3.3V (Pin 1)Do not use 5V; SH1106 logic is 3.3V
OLEDGNDGND (Pin 6)Common ground
OLEDSDAGPIO 2 (Pin 3)I2C1 Data
OLEDSCLGPIO 3 (Pin 5)I2C1 Clock
LEDAnode (+)GPIO 17 (Pin 11)Via 330Ω resistor
LEDCathode (-)GND (Pin 9)Common ground
SwitchPin 1GPIO 27 (Pin 13)Internal pull-up enabled in code
SwitchPin 2GND (Pin 14)Grounds pin to trigger interrupt

Hardening sshd_config: The Authentication Decision Tree

Choosing the right authentication method depends on your deployment environment. The Mozilla OpenSSH Guidelines provide the industry standard for cryptographic baselines. Use the decision matrix below to select your configuration, terminating at the recommended default for embedded IoT.

Deployment ScenarioAuth MethodProsConsVerdict
Isolated local bench testingPasswordZero setup frictionVulnerable to brute-force, easily sniffed if MITMReject for production
Remote IoT / Internet-facingEd25519 KeysSmall key size, fast, highly secure against quantum/classic attacksRequires key management and ssh-agent setupDEFAULT PICK
Enterprise fleet (>50 nodes)SSH CertificatesCentralized CA, automated key rotationRequires running HashiCorp Vault or Step CAOverkill for single Pi

The Concrete Pick: For 99% of maker, industrial, and home-lab Raspberry Pi projects, Ed25519 Key-Based Authentication is the definitive choice. Generate your keypair on your host machine using ssh-keygen -t ed25519 -C 'pi-sentinel-2026', copy it via ssh-copy-id, and then edit /etc/ssh/sshd_config to include:

Port 2222
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
MaxAuthTries 3
PubkeyAuthentication yes
Lockout Warning: Never restart the ssh service after changing sshd_config without first testing the new configuration in a separate terminal window. Run sudo sshd -t to validate syntax. If you lock yourself out on a headless Pi 5, you will need to pull the SD card and mount it on another Linux machine to fix the config.

Python SSH Monitor and GPIO Control Script

This Python 3 script targets the Raspberry Pi 5's systemd journal to monitor SSH login attempts in real-time. It updates the Waveshare OLED with the last three log entries and illuminates the green LED when an active SSH session is detected. It also includes a hardware kill-switch on GPIO 27 to instantly block port 2222 via ufw if physical tampering is suspected.

Prerequisites: Install dependencies via sudo apt install python3-gpiozero python3-luma.oled ufw.


import time
import subprocess
import os
from gpiozero import LED, Button
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import sh1106

# --- Pin Definitions ---
SSH_STATUS_LED = LED(17)
NET_KILL_SWITCH = Button(27, pull_up=True, bounce_time=0.05)

# --- I2C Display Setup ---
# Waveshare 1.3" SH1106 typically sits at 0x3C
serial = i2c(port=1, address=0x3C)
device = sh1106(serial)

def get_ssh_logs():
    """Fetches the last 3 lines of the SSH systemd journal."""
    try:
        result = subprocess.run(
            ['journalctl', '-u', 'ssh', '-n', '3', '--no-pager', '-o', 'cat'],
            capture_output=True, text=True, check=True
        )
        logs = result.stdout.strip().split('\n')
        return logs if logs else ['No recent SSH activity']
    except subprocess.CalledProcessError:
        return ['Error: journalctl failed']
    except Exception as e:
        return [f'System error: {str(e)[:20]}']

def check_active_sessions():
    """Returns True if at least one SSH session is active."""
    try:
        result = subprocess.run(['who'], capture_output=True, text=True, check=True)
        return 'pts/' in result.stdout
    except Exception:
        return False

def trigger_network_kill():
    """Hardware interrupt to block SSH port via UFW."""
    print('Kill switch pressed! Blocking port 2222...')
    subprocess.run(['sudo', 'ufw', 'deny', '2222/tcp'], capture_output=True)
    SSH_STATUS_LED.blink(on_time=0.2, off_time=0.2)

def main():
    print('SSH Sentinel Online. Target: Raspberry Pi 5 8GB')
    NET_KILL_SWITCH.when_pressed = trigger_network_kill

    try:
        while True:
            logs = get_ssh_logs()
            is_active = check_active_sessions()
            
            if is_active:
                SSH_STATUS_LED.on()
            else:
                # Only turn off if not in kill-switch blink mode
                if not SSH_STATUS_LED.is_active or not hasattr(SSH_STATUS_LED, '_blink_thread'):
                    SSH_STATUS_LED.off()

            with canvas(device) as draw:
                draw.text((0, 0), 'SSH SENTINEL', fill='white')
                draw.text((0, 12), '-------------', fill='white')
                y_offset = 24
                for line in logs:
                    # Truncate long log lines for 128px width
                    draw.text((0, y_offset), line[:21], fill='white')
                    y_offset += 12
                    if y_offset > 52:
                        break
            
            time.sleep(2)
            
    except KeyboardInterrupt:
        print('Sentinel shutting down gracefully.')
    except Exception as e:
        print(f'Fatal I2C or GPIO error: {e}')
    finally:
        SSH_STATUS_LED.off()
        device.cleanup()

if __name__ == '__main__':
    main()

Debugging: Exact Error Strings and Ranked Causes

When your headless Pi drops off the network or rejects your key, guessing wastes hours. Below are the exact error strings thrown by the OpenSSH client, ranked by probability, along with the definitive fixes.

Error 1: ssh: connect to host 192.168.1.50 port 2222: Connection refused

This means the TCP handshake reached the Pi, but the OS actively rejected it. The Pi is online, but SSH is not listening.

  1. Cause 1 (Most Likely): The ssh service is disabled or crashed. Raspberry Pi OS disables SSH by default on fresh flashes. Fix: Run sudo systemctl enable --now ssh or place an empty file named ssh in the boot partition.
  2. Cause 2: UFW or iptables is blocking the custom port. Fix: Run sudo ufw allow 2222/tcp.
  3. Cause 3: The sshd daemon failed to start due to a syntax error in sshd_config. Fix: Check sudo journalctl -u ssh -n 20 for config parsing errors.

Error 2: Permission denied (publickey)

The TCP connection succeeded, the server accepted the connection, but your cryptographic proof failed.

  1. Cause 1 (Most Likely): The ~/.ssh/authorized_keys file has permissions that are too open. OpenSSH strictly enforces file modes. Fix: Run chmod 600 ~/.ssh/authorized_keys and chmod 700 ~/.ssh.
  2. Cause 2: Your local ssh-agent is offering the wrong key. Fix: Explicitly pass the key: ssh -i ~/.ssh/pi-sentinel_ed25519 -p 2222 user@192.168.1.50.
  3. Cause 3: You copied the public key to the root user instead of your standard pi or custom user directory. Fix: Verify the target user's home directory.
The First 3 Things to Check When It Fails:
1. Ping the IP: ping 192.168.1.50 (Verifies Layer 3 network connectivity and ARP resolution).
2. Check Service Status: systemctl status ssh (Verifies the daemon is active and not masked).
3. Verify Key Permissions: ls -la ~/.ssh (Ensures no group/world read/write bits are set).

Extending and Simplifying the Build

Not every deployment requires an OLED screen, and some require deeper integration. Here is how to scale this project up or down based on your physical constraints and budget.

How to Simplify (The Minimalist Headless Node)

If you are deploying a Pi 5 inside a sealed DIN-rail enclosure where visual feedback is impossible, drop the Waveshare OLED and the tactile switch entirely. Rely solely on the Raspberry Pi remote access documentation standards. Replace the Python script with a simple systemd service that blinks the Pi 5's onboard user LED (controlled via /sys/class/leds/PWR/) when an SSH session is active. This reduces component cost by $18 and eliminates I2C bus contention.

How to Extend (The Fleet Security Upgrade)

To scale this from a single workbench monitor to a fleet security node, integrate the fail2ban package and a Telegram Bot API webhook. Modify the Python script to parse Failed password strings from the journal. When three failed attempts occur within 60 seconds, the script triggers a POST request to your Telegram bot, alerting you of the brute-force attempt and automatically executing ufw deny [OFFENDING_IP]. For enterprise fleets, replace local Ed25519 keys with an SSH Certificate Authority (CA) using HashiCorp Vault, allowing you to revoke access globally without touching the Pi's local authorized_keys file.

By enforcing Ed25519 keys, isolating the SSH port, and utilizing the Pi 5's GPIO for physical network interlocks, you transform a vulnerable SBC into a hardened, observable embedded node.