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.
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
| Component | Component Pin | Pi 5 GPIO / Physical Pin | Notes |
|---|---|---|---|
| OLED | VCC | 3.3V (Pin 1) | Do not use 5V; SH1106 logic is 3.3V |
| OLED | GND | GND (Pin 6) | Common ground |
| OLED | SDA | GPIO 2 (Pin 3) | I2C1 Data |
| OLED | SCL | GPIO 3 (Pin 5) | I2C1 Clock |
| LED | Anode (+) | GPIO 17 (Pin 11) | Via 330Ω resistor |
| LED | Cathode (-) | GND (Pin 9) | Common ground |
| Switch | Pin 1 | GPIO 27 (Pin 13) | Internal pull-up enabled in code |
| Switch | Pin 2 | GND (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 Scenario | Auth Method | Pros | Cons | Verdict |
|---|---|---|---|---|
| Isolated local bench testing | Password | Zero setup friction | Vulnerable to brute-force, easily sniffed if MITM | Reject for production |
| Remote IoT / Internet-facing | Ed25519 Keys | Small key size, fast, highly secure against quantum/classic attacks | Requires key management and ssh-agent setup | DEFAULT PICK |
| Enterprise fleet (>50 nodes) | SSH Certificates | Centralized CA, automated key rotation | Requires running HashiCorp Vault or Step CA | Overkill 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
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.
- Cause 1 (Most Likely): The
sshservice is disabled or crashed. Raspberry Pi OS disables SSH by default on fresh flashes. Fix: Runsudo systemctl enable --now sshor place an empty file namedsshin the boot partition. - Cause 2: UFW or iptables is blocking the custom port. Fix: Run
sudo ufw allow 2222/tcp. - Cause 3: The
sshddaemon failed to start due to a syntax error insshd_config. Fix: Checksudo journalctl -u ssh -n 20for config parsing errors.
Error 2: Permission denied (publickey)
The TCP connection succeeded, the server accepted the connection, but your cryptographic proof failed.
- Cause 1 (Most Likely): The
~/.ssh/authorized_keysfile has permissions that are too open. OpenSSH strictly enforces file modes. Fix: Runchmod 600 ~/.ssh/authorized_keysandchmod 700 ~/.ssh. - Cause 2: Your local
ssh-agentis offering the wrong key. Fix: Explicitly pass the key:ssh -i ~/.ssh/pi-sentinel_ed25519 -p 2222 user@192.168.1.50. - Cause 3: You copied the public key to the
rootuser instead of your standardpior custom user directory. Fix: Verify the target user's home directory.
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.






