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 Pin | Function | Connect To | Notes |
|---|---|---|---|
| Pin 8 (GPIO 14) | UART TXD | Serial Adapter RXD (White/Green) | Pi transmits data to your PC |
| Pin 10 (GPIO 15) | UART RXD | Serial Adapter TXD (Green/White) | Pi receives data from your PC |
| Pin 6 (GND) | Ground | Serial Adapter GND (Black) | Common ground is mandatory |
| Pin 11 (GPIO 17) | LED Control | 330Ω Resistor → LED Anode (+) | Pull high to illuminate |
| Pin 9 (GND) | Ground | LED 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.
/boot/. In Bookworm, it is mounted at /boot/firmware/. If you are creating files manually, ensure you are targeting the correct partition.
- 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.
- Manual Fallback (If already flashed): If you missed the Imager step, mount the microSD card on your PC. Navigate to the
boot/firmwarepartition and create an empty file named exactlyssh(no .txt extension). - Enable the UART Console: Open the
config.txtfile located in theboot/firmwarepartition. 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 - 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.
- 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
115200baud. 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
sshfile was missing from the/boot/firmware/partition on first boot, so the OS disabled the service for security. - Cause B: The
sshdservice crashed. Check the UART serial console for OOM (Out of Memory) killer logs. - Fix: Run
sudo systemctl enable --now sshvia 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.






