Difficulty: Intermediate | Time: 45 Minutes | Target Board: Raspberry Pi 5 (4GB/8GB) running Raspberry Pi OS Bookworm 64-bit

When you deploy a Raspberry Pi as a headless embedded node—whether it is running a Home Assistant instance, an MQTT broker, or a remote sensor array—SSH is your only lifeline. If the network stack misbehaves or the SSH daemon crashes, a headless Pi becomes a paperweight. Learning how to setup SSH on Raspberry Pi hardware properly means going beyond the basic raspi-config toggle. It requires building a hardware fallback and a software watchdog to ensure you never lose access to your deployment.

This guide walks through a robust headless setup for the Raspberry Pi 5, integrating a UART serial console fallback, a physical GPIO status LED, and a Python watchdog daemon to monitor and auto-recover the SSH service.

Headless Pi Deployment: Hardware & Software Requirements

Before we write any code, we need to establish the physical layer. The Raspberry Pi 5 introduces the RP1 southbridge chip, which changes how GPIO and UART are handled at the silicon level, but the gpiozero library abstracts this beautifully in user space. We will wire a serial debug console and an SSH status indicator.

Parts List

  • Compute: Raspberry Pi 5 (4GB or 8GB variant)
  • Power: Official 27W USB-C PD Power Supply (critical for Pi 5 peripheral stability)
  • Storage: 64GB microSD card (A2 application performance class rated)
  • Serial Adapter: USB-to-TTL serial cable (CP2102 or PL2303 chipset, 3.3V logic)
  • Indicator: 5mm Green LED + 330Ω through-hole resistor
  • Wiring: Female-to-female jumper wires, breadboard

Pin Mapping & Wiring Table

The Pi 5 UART0 is mapped to GPIO 14 and 15 by default. Warning: Never connect a 5V logic serial adapter to the Pi 5 GPIO header; it will permanently damage the RP1 chip.

Pi 5 GPIO PinFunctionConnect ToNotes
Pin 8 (GPIO 14)UART TXDSerial Adapter RXD (White/Green)Pi transmits data to your PC
Pin 10 (GPIO 15)UART RXDSerial Adapter TXD (Green/White)Pi receives data from your PC
Pin 6 (GND)GroundSerial Adapter GND (Black)Common ground is mandatory
Pin 11 (GPIO 17)LED Control330Ω Resistor → LED Anode (+)Pull high to illuminate
Pin 9 (GND)GroundLED Cathode (-)Current return path

Step-by-Step: Enable SSH and Configure UART Fallback

The Raspberry Pi OS Bookworm release changed the boot partition mount point and default security policies. Follow these exact steps to ensure SSH and UART are enabled before the Pi ever boots headless.

Bookworm Path Change: In older OS versions (Bullseye and earlier), the boot partition was mounted at /boot/. In Bookworm, it is mounted at /boot/firmware/. If you are creating files manually, ensure you are targeting the correct partition.
  1. Flash the OS with Customization: Open Raspberry Pi Imager. Select Raspberry Pi OS (64-bit). Click the gear icon (OS Customization). Check Enable SSH and select Use password authentication. Set your username and password. This is the most reliable way to setup SSH on Raspberry Pi boards headless.
  2. Manual Fallback (If already flashed): If you missed the Imager step, mount the microSD card on your PC. Navigate to the boot/firmware partition and create an empty file named exactly ssh (no .txt extension).
  3. Enable the UART Console: Open the config.txt file located in the boot/firmware partition. Add the following line to the very bottom of the file to force the hardware UART to remain active for our serial fallback:
    enable_uart=1
  4. Assemble the Hardware: Wire the CP2102 serial adapter and the GPIO 17 LED according to the pin mapping table above. Plug the serial adapter into your host PC.
  5. Boot and Verify: Power on the Pi 5. Open a terminal on your host PC (using PuTTY or screen) and connect to the COM port at 115200 baud. You should see the Linux boot sequence and a login prompt.

Python SSH Watchdog: Code, Pin Definitions, and Error Handling

Now we build the software layer. This Python daemon checks if the SSH port (22) is actively listening on the local loopback interface. If it is, the GPIO 17 LED stays solid. If the SSH daemon crashes or hangs, the LED blinks rapidly, the script logs the failure over the UART serial console, and it attempts to restart the sshd service.

Prerequisites: Run sudo apt update && sudo apt install python3-gpiozero python3-serial on your Pi before executing this script.

import socket
import time
import serial
import subprocess
from gpiozero import LED
from signal import pause

# --- Pin & Port Definitions (Target: Raspberry Pi 5 Bookworm) ---
SSH_STATUS_LED = LED(17)       # GPIO 17 physical pin 11
UART_PORT = '/dev/serial0'     # Primary UART on Pi 5
BAUD_RATE = 115200
SSH_PORT = 22
CHECK_INTERVAL = 15            # Seconds between health checks

def check_ssh_port(host='127.0.0.1', port=SSH_PORT, timeout=2.0):
    """Returns True if SSH port is open and accepting connections."""
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(timeout)
            result = s.connect_ex((host, port))
            return result == 0
    except socket.error as e:
        print(f"Socket error during check: {e}")
        return False

def restart_ssh_service():
    """Attempts to restart the SSH daemon via systemctl."""
    try:
        # Using subprocess for better error capturing over os.system
        result = subprocess.run(
            ['sudo', 'systemctl', 'restart', 'ssh'],
            capture_output=True, text=True, timeout=10
        )
        return result.returncode == 0
    except subprocess.TimeoutExpired:
        return False
    except Exception as e:
        print(f"Restart exception: {e}")
        return False

def main():
    # Initialize UART for fallback serial logging
    ser = None
    try:
        ser = serial.Serial(UART_PORT, BAUD_RATE, timeout=1)
        print("UART Fallback initialized successfully.")
    except serial.SerialException as e:
        print(f"Warning: UART Fallback Failed ({e}). Running headless-only.")

    print("SSH Watchdog Daemon Started...")
    
    try:
        while True:
            is_ssh_up = check_ssh_port()
            
            if is_ssh_up:
                # SSH is healthy: Solid LED
                SSH_STATUS_LED.on()
                msg = "[OK] SSH Port 22 is listening.\n"
            else:
                # SSH is down: Blink LED and attempt recovery
                SSH_STATUS_LED.blink(on_time=0.2, off_time=0.2, background=True)
                msg = "[CRITICAL] SSH Port 22 is DOWN. Attempting systemctl restart...\n"
                
                if restart_ssh_service():
                    msg += "[RECOVERY] ssh.service restart command issued.\n"
                else:
                    msg += "[FAILURE] ssh.service restart FAILED. Hardware reboot required.\n"
                    
            # Write status to UART serial console if connected
            if ser and ser.is_open:
                ser.write(msg.encode('utf-8'))
                ser.flush()
                
            time.sleep(CHECK_INTERVAL)
            
    except KeyboardInterrupt:
        print("\nWatchdog stopped by user.")
    finally:
        SSH_STATUS_LED.off()
        if ser and ser.is_open:
            ser.close()

if __name__ == '__main__':
    main()

How to simplify: If you do not need the serial UART fallback, simply delete the serial import and the UART initialization block. The script will still function perfectly as a local LED watchdog.

Debugging Connection Failures: Exact Errors and Ranked Causes

When your SSH client fails to connect, the terminal throws a specific error string. Do not guess; read the string. Here are the first three things to check when it fails: (1) Verify the Pi has power and is on the network via a ping test. (2) Check the router DHCP table to ensure the IP address hasn't changed. (3) Connect your UART serial adapter and run systemctl status ssh to verify the daemon state.

Error 1: Connection Refused

Exact String: ssh: connect to host 192.168.1.50 port 22: Connection refused

What it means: Your PC successfully found the Pi on the network, but the Pi actively rejected the connection on port 22.

  • Cause A (Most Likely): The ssh file was missing from the /boot/firmware/ partition on first boot, so the OS disabled the service for security.
  • Cause B: The sshd service crashed. Check the UART serial console for OOM (Out of Memory) killer logs.
  • Fix: Run sudo systemctl enable --now ssh via the serial console, or re-flash the SD card using the Imager's OS customization menu.

Error 2: Connection Timed Out

Exact String: ssh: connect to host 192.168.1.50 port 22: Connection timed out

What it means: Your PC sent packets into the void and heard nothing back. The network layer is broken.

  • Cause A (Most Likely): You are using a stale IP address. The Pi rebooted and the router assigned it a new DHCP lease.
  • Cause B: The Pi is connected to a 2.4GHz Wi-Fi network that dropped, or the Ethernet cable is unseated.
  • Fix: Stop using raw IP addresses. Use mDNS by typing ssh username@raspberrypi.local. If that fails, check your router's client list.

Error 3: Host Identification Changed

Exact String: WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

What it means: The cryptographic fingerprint of the device at that IP address does not match what your PC remembers.

  • Cause A (Most Likely): You re-flashed the Pi's SD card, generating new SSH host keys, but kept the same IP address.
  • Cause B: A different device on your network claimed the Pi's old IP address via DHCP.
  • Fix: Clear the old key from your PC's known_hosts file by running: ssh-keygen -R 192.168.1.50 (replace with your Pi's IP).

Frequently Asked Questions

How do I setup SSH on Raspberry Pi without a monitor or keyboard?

The most reliable method is using the Raspberry Pi Imager on your host PC. Before clicking 'Write', click the gear icon (OS Customization), check 'Enable SSH', and set a password. If you are deploying an image via a script or dd, mount the resulting SD card on your PC and create an empty file named ssh in the root of the boot/firmware partition. For deeper debugging without a monitor, always wire a UART serial adapter to GPIO 14/15 as shown in this guide.

Why does my Raspberry Pi SSH connection keep dropping?

Intermittent drops on a Pi 5 are almost always power or thermal related. The Pi 5 requires a 27W USB-C PD supply to maintain full peripheral power. If you use a standard 15W phone charger, the Pi will throttle the CPU and drop network interfaces under load to save power. Additionally, check your Wi-Fi power management settings. You can disable Wi-Fi power saving by running sudo iwconfig wlan0 power off, which prevents the radio from sleeping during idle SSH sessions.

Can I setup SSH on Raspberry Pi over Wi-Fi instead of Ethernet?

Yes. If using the Raspberry Pi Imager, enter your Wi-Fi SSID and password in the OS Customization menu. If doing it manually, create a file named wpa_supplicant.conf in the /boot/firmware/ partition. However, for embedded deployments where reliability is paramount, Ethernet is strongly preferred. Wi-Fi introduces latency spikes and drops that can corrupt long-running SSH tunnels or SCP file transfers.

How do I extend this build to trigger a hardware relay if SSH hangs?

To extend the Python watchdog script, add a second GPIO pin definition (e.g., RELAY_PIN = LED(27)) connected to an optocoupler or relay module. Inside the else block of the watchdog loop, if restart_ssh_service() returns False, you can trigger the relay to physically cut and restore power to a secondary sensor, or trigger a hardware watchdog timer (WDT) to hard-reboot the Pi itself. To simplify the build for basic hobby use, just remove the UART serial code and rely solely on the GPIO 17 LED for visual feedback.

References: For the latest Bookworm configuration changes, consult the official Raspberry Pi configuration documentation. For secure remote access protocols, review the Raspberry Pi remote access guidelines.