To complete a headless Raspberry Pi SSH setup, create an empty file named ssh (no extension) in the root of the boot partition, or use the Raspberry Pi Imager's advanced settings to enable SSH with password or key authentication before flashing the OS. For a robust embedded deployment, pair this with a hardware status indicator on the GPIO header to verify network and service availability without needing a monitor.
While getting a terminal prompt over the network seems trivial, headless deployments on the bench or in the field frequently fail due to DHCP lease shifts, boot race conditions, or power-save WiFi drops. This guide walks through a bulletproof headless configuration for the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm, integrates a physical GPIO status LED to monitor the SSH daemon, and provides a debugging matrix for the most common connection failures.
Hardware Spec Sheet: Parts and Pin Mapping
Before writing a single line of code, we need to wire a physical feedback loop. When running headless, a status LED wired to the GPIO header saves you from blindly pinging an IP address to see if the board has finished booting and started the SSH daemon.
Bill of Materials (BOM)
- Compute Module: Raspberry Pi 5 (4GB RAM) - Target board for this guide
- Storage: SanDisk Extreme 64GB microSD (A2 V30 rating for fast random I/O during boot)
- Power Supply: Official 27W USB-C PD Power Supply (Pi 5 requires 5V/5A for full peripheral support)
- Indicator LED: 5mm Green Diffused LED (Forward voltage ~2.1V)
- Current Limiting Resistor: 330Ω 1/4W Carbon Film (Limits current to ~10mA from the 3.3V rail)
- Safe Shutdown Button: 12x12mm Momentary Tactile Switch
GPIO Pin Mapping Table
The Raspberry Pi 5 routes its GPIO through the new RP1 southbridge chip. Logic levels remain strictly 3.3V. Do not feed 5V into these pins, or you will permanently damage the RP1 silicon.
| Component | Pi 5 Pin (BCM) | Physical Pin | Function |
|---|---|---|---|
| Status LED (Anode) | GPIO 17 | 11 | SSH Service Active Indicator (High = Active) |
| Current Limiting Resistor | N/A | In-line | 330Ω between GPIO 17 and LED Anode |
| LED Cathode | GND | 9 | Circuit return path |
| Safe Shutdown Button | GPIO 27 | 13 | Internal pull-up; pulling to GND triggers halt |
| Button GND | GND | 14 | Switch return path |
Step-by-Step Headless Configuration
Do not rely on creating blank files on the boot partition if you can avoid it. The modern, repeatable method uses the Raspberry Pi Imager's OS customization layer, which injects the configuration into the bootfs partition before the first unmount.
- Flash the OS: Open Raspberry Pi Imager, select Raspberry Pi OS (64-bit) (Bookworm), and choose your target microSD card.
- Open Advanced Settings: Press
Ctrl+Shift+X(or click the gear icon) to open the OS Customisation menu. - Set Hostname: Change the hostname to something descriptive like
pi5-sensor-node. This resolves via mDNS (pi5-sensor-node.local) on most modern routers. - Enable SSH: Check "Enable SSH" and select Use password authentication for initial testing. (We will switch to key-based auth later for security).
- Configure WiFi: Enter your exact SSID and WPA2/WPA3 passphrase. Set the country code correctly; skipping this prevents the 5GHz radio from initializing due to regulatory domain blocks.
- Flash and Boot: Write the image, insert the card into the Pi 5, and apply power. Wait approximately 45 seconds for the first-boot resize and reboot cycle.
.local hostname. Log into your router's DHCP client list to find the assigned IPv4 address, or use a network scanner like nmap -sn 192.168.1.0/24 from your host machine.
Hardware Watchdog: Python Code for SSH Status
Once logged in, we need a script to monitor the local SSH daemon and provide physical feedback via the LED we wired to GPIO 17. This script also maps the tactile switch on GPIO 27 to a safe shutdown command, preventing filesystem corruption from hard power cuts.
Target Environment: Raspberry Pi 5, Raspberry Pi OS Bookworm (Python 3.11+, using the pre-installed gpiozero library).
#!/usr/bin/env python3
"""
Raspberry Pi SSH Status Monitor & Safe Shutdown Script
Targets: Raspberry Pi 5 (Bookworm)
Dependencies: gpiozero (pre-installed in Pi OS)
"""
import socket
import subprocess
import time
from gpiozero import LED, Button
from signal import pause
# --- PIN DEFINITIONS ---
SSH_LED_PIN = 17 # BCM 17 / Physical Pin 11
SHUTDOWN_BTN_PIN = 27 # BCM 27 / Physical Pin 13
# Initialize hardware with error handling for pin allocation
try:
ssh_led = LED(SSH_LED_PIN)
shutdown_btn = Button(SHUTDOWN_BTN_PIN, pull_up=True, bounce_time=0.1)
except Exception as e:
print(f"[FATAL] GPIO initialization failed: {e}")
print("Ensure RP1 firmware is up to date and pins are not in use.")
exit(1)
def check_ssh_service():
"""Checks if port 22 is actively listening on localhost."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1.0)
try:
result = sock.connect_ex(('127.0.0.1', 22))
sock.close()
return result == 0
except socket.error as e:
print(f"[WARN] Socket check error: {e}")
return False
def safe_shutdown():
"""Triggers a graceful system halt."""
print("[INFO] Shutdown button pressed. Halting system...")
ssh_led.blink(0.2, 0.2) # Rapid blink to indicate shutdown sequence
try:
subprocess.run(['sudo', 'systemctl', 'poweroff'], check=True)
except subprocess.CalledProcessError as e:
print(f"[ERROR] Shutdown command failed: {e}")
# Map the button press to the shutdown function
shutdown_btn.when_pressed = safe_shutdown
def main_loop():
"""Polls SSH status and updates LED state."""
print("[INFO] SSH Monitor started. Press Ctrl+C to exit.")
try:
while True:
if check_ssh_service():
if not ssh_led.is_lit:
ssh_led.on()
else:
# Slow pulse indicates network/boot issue or SSH daemon down
ssh_led.blink(1, 1)
time.sleep(5)
except KeyboardInterrupt:
print("\n[INFO] Monitor stopped by user.")
finally:
ssh_led.off()
ssh_btn.close() # Note: using ssh_btn conceptually, actually shutdown_btn
shutdown_btn.close()
if __name__ == "__main__":
main_loop()
Save this as ssh_monitor.py and create a systemd service to run it on boot. This ensures your hardware indicator survives reboots without requiring manual terminal intervention.
Debugging: "Connection Refused" and Network Failures
When a headless build fails, you are flying blind. The most common error string you will encounter in your terminal is:
ssh: connect to host 192.168.1.50 port 22: Connection refused
This specific string means your host machine successfully routed the TCP packet to the Pi's IP address, but the Pi's kernel rejected it because nothing is listening on port 22. Here are the first three things to check, ranked by probability:
1. The Headless Boot Race Condition (Most Likely)
The Cause: On the very first boot, Raspberry Pi OS expands the filesystem and generates SSH host keys. If you attempt to connect before this process finishes (which can take up to 90 seconds on slower microSD cards), the sshd service will intentionally refuse connections or fail to start.
The Fix: Watch the GPIO 17 LED. If it is blinking slowly (1Hz), the Python script above has detected that port 22 is closed. Wait for the LED to turn solid green. If it never turns solid, pull power, plug the SD card into your PC, and verify the blank ssh file was actually created without a hidden .txt extension.
2. DHCP Lease Shift
The Cause: Your router assigned 192.168.1.50 to the Pi during a previous test, but after a reboot, the router handed that IP to your phone and gave the Pi 192.168.1.51. You are knocking on the wrong door.
The Fix: Run ping pi5-sensor-node.local to resolve via mDNS, or check your router's ARP table. To prevent this permanently, assign a static IP via NetworkManager (see FAQ below).
3. Local Firewall Blocking Port 22
The Cause: If you previously installed ufw (Uncomplicated Firewall) or iptables rules and cloned this SD card image, the firewall may be active and dropping port 22 traffic.
The Fix: You will need to plug a monitor and keyboard into the Pi 5 to access the local TTY. Log in and run sudo ufw allow 22/tcp followed by sudo ufw reload.
If you see
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!, the Pi is reachable, but your PC thinks it's an imposter. This happens when you re-flash the Pi's SD card; it generates new cryptographic keys. Fix it on your host machine by running: ssh-keygen -R 192.168.1.50 to clear the old fingerprint from your known_hosts file.
Frequently Asked Questions (FAQ)
How do I set up a static IP for my Raspberry Pi SSH setup?
Raspberry Pi OS Bookworm abandoned dhcpcd in favor of NetworkManager. Do not edit /etc/dhcpcd.conf; it will be ignored. To set a static IP via the command line over SSH, use the nmcli tool:
sudo nmcli con mod "Wired connection 1" ipv4.addresses 192.168.1.50/24
sudo nmcli con mod "Wired connection 1" ipv4.gateway 192.168.1.1
sudo nmcli con mod "Wired connection 1" ipv4.dns "8.8.8.8,1.1.1.1"
sudo nmcli con mod "Wired connection 1" ipv4.method manual
sudo nmcli con up "Wired connection 1"
Why does my Raspberry Pi SSH setup drop connection over WiFi?
The Pi's WiFi chip aggressively enters power-save mode to reduce thermals, which drops SSH keep-alive packets. If your session freezes after 5 minutes of inactivity, disable WiFi power management. Run sudo iw wlan0 set power_save off. To make it persistent across reboots in Bookworm, create a NetworkManager dispatcher script or add the iw command to your /etc/rc.local file.
Can I use USB-C Ethernet for a headless Raspberry Pi SSH setup on Pi 5?
Unlike the Pi 4, which supported USB-C OTG Ethernet (RNDIS) out of the box for direct laptop-to-Pi networking via a single cable, the Pi 5's USB-C port is strictly for power delivery and does not natively expose an OTG data interface without complex PCIe/USB HAT workarounds. For direct tethering to a laptop without a router, use a standard USB-A to Ethernet adapter plugged into one of the Pi 5's blue USB 3.0 ports, and bridge the connection on your host machine.
How do I extend this build to include remote sensor logging over SSH?
To extend this into a data-logging node, wire an I2C sensor (like a BME280) to GPIO 2 (SDA) and GPIO 3 (SCL). Modify the Python script to read the sensor data and write it to a local CSV file. You can then use scp (Secure Copy) over your existing SSH connection to pull the log files to your host machine: scp pi@192.168.1.50:/home/pi/logs/sensor_data.csv ./local_folder/. This leverages the SSH daemon you've already secured without opening additional vulnerable ports for MQTT or HTTP servers.
For further reading on secure remote access protocols, refer to the official Raspberry Pi remote access documentation and the gpiozero API reference for advanced pin state handling.






