To allow SSH on a Raspberry Pi for a headless deployment, you must either create an empty file named ssh (no extension) in the /boot/firmware partition of the microSD card before first boot, or enable it via the OS Customization menu in Raspberry Pi Imager. If you are running Raspberry Pi OS Bookworm or later, the legacy /boot directory will not work, and the default pi user no longer exists. This guide details the exact hardware, Bookworm-specific file paths, and UART serial fallbacks required to reliably provision and debug SSH on edge IoT nodes.
Decision Tree: Choosing Your Headless SSH Enablement Method
The method you choose depends entirely on your current hardware state and whether the OS is already flashed. Use this decision matrix to select the correct provisioning path.
| Current State | Scenario | Method | Concrete Pick / Action |
|---|---|---|---|
| Unflashed SD/SSD | Starting a new IoT deployment from scratch | Raspberry Pi Imager GUI | OS Customization > Services > Enable SSH (Use password auth) |
| Flashed SD/SSD (Bookworm) | OS already written, no monitor available | Boot Partition File Injection | Create empty file named ssh in /boot/firmware/ |
| Flashed SD/SSD (Bullseye) | Legacy OS already written, no monitor | Boot Partition File Injection | Create empty file named ssh in /boot/ |
| Network Locked Out | SSH enabled but IP unknown or WiFi failed | Hardware UART Serial Console | USB-to-TTL cable on GPIO 14/15 at 115200 baud |
Hardware BOM and UART Fallback Pin Mapping
When deploying headless, network failures are inevitable. A seasoned embedded engineer always builds in a serial debug fallback. The code and pinouts in this article target the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (64-bit, Bookworm).
Parts List
- Compute: Raspberry Pi 5 (8GB RAM) - ~$80 USD
- Thermal: Raspberry Pi Active Cooler - ~$5 USD (Mandatory for Pi 5 under load)
- Power: Official 27W USB-C PD Power Supply - ~$12 USD (Prevents brownout warnings on GPIO)
- Storage: SanDisk Extreme 64GB microSD (A2 rating) or NVMe via PCIe HAT
- Debug: CP2102 USB-to-TTL Serial Cable (3.3V logic) - ~$9 USD
- Actuator: 5V Relay Module (Optocoupler isolated) for GPIO switching
UART Serial Fallback Pin Mapping
If SSH fails and you cannot ping the device, connect your CP2102 USB-to-TTL cable to the Pi 5's primary UART (UART0). Do not connect the 5V/VCC wire from the USB adapter to the Pi; only connect TX, RX, and GND.
| Pi 5 GPIO Label | Physical Pin | UART Function | Connect to CP2102 Adapter |
|---|---|---|---|
| GPIO 14 | Pin 8 | TXD (Transmit) | RX (Receive) |
| GPIO 15 | Pin 10 | RXD (Receive) | TX (Transmit) |
| GND | Pin 6 | Ground | GND |
Note: To enable the serial console in Bookworm, add console=serial0,115200 to the end of /boot/firmware/cmdline.txt.
Step-by-Step: Provisioning SSH and Hardening the Daemon
Assuming you are injecting the SSH file manually into an already-flashed Bookworm SD card, follow these exact steps. The partition structure changed significantly in Bookworm, breaking legacy guides.
- Mount the SD Card: Insert the flashed microSD into your host PC. You will see a partition labeled
bootfs. - Create the Trigger File: In the root of the
bootfspartition (which mounts to/boot/firmware/on the Pi), create a completely empty file named exactlyssh.Windows Warning: Windows Explorer often hides known file extensions. If you create a text file namedssh.txtand hide the extension, the Pi will ignore it. Use the command prompt:echo. > sshinside the boot drive directory to guarantee no extension exists. - Boot and Locate: Insert the SD into the Pi 5 and apply power. Wait 60 seconds for the first-boot resize and key generation. Find the IP via your router's DHCP table or by pinging
raspberrypi.local. - Connect and Authenticate: Open your terminal and run
ssh your_custom_username@raspberrypi.local. (Remember, the defaultpiuser was deprecated in Bookworm; you must use the user you created in Pi Imager or viauserconf). - Harden the Daemon: Once logged in, immediately disable password authentication to prevent brute-force botnet scans.
Changesudo nano /etc/ssh/sshd_config#PasswordAuthentication yestoPasswordAuthentication no. Ensure you have already copied your public key to~/.ssh/authorized_keysbefore doing this, or you will lock yourself out. - Restart SSH: Apply changes with
sudo systemctl restart ssh.
Python Remote GPIO Execution Script
Once SSH is established, you will likely want to execute hardware commands remotely. The following Python script is designed to be invoked via SSH (e.g., ssh user@pi 'python3 /opt/remote_gpio_ssh.py ON'). It targets the Pi 5, uses the gpiozero library (standard in Bookworm), and includes strict error handling for hardware faults.
#!/usr/bin/env python3
"""
remote_gpio_ssh.py
Target: Raspberry Pi 5 (8GB) / Raspberry Pi OS Bookworm 64-bit
Hardware: 5V Relay Module connected to GPIO 17 (Physical Pin 11)
Usage: python3 remote_gpio_ssh.py [ON|OFF|STATUS]
"""
import sys
import logging
from gpiozero import OutputDevice
from gpiozero.exc import BadPinFactory, PinGPIOZeroError
# --- Pin Definitions ---
# GPIO 17 (Pin 11) controls the relay IN pin.
# Relay is active-low (common for optocoupler modules).
RELAY_PIN = 17
RELAY_ACTIVE_HIGH = False
# Configure logging for remote SSH execution visibility
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def initialize_relay():
try:
# active_high=False means setting .on() pulls the pin LOW
relay = OutputDevice(RELAY_PIN, active_high=RELAY_ACTIVE_HIGH, initial_value=False)
return relay
except BadPinFactory as e:
logging.critical(f'GPIO Pin Factory Error: {e}. Are you running on a Pi?')
sys.exit(2)
except PinGPIOZeroError as e:
logging.critical(f'Hardware Pin Fault on GPIO {RELAY_PIN}: {e}')
sys.exit(3)
def main():
if len(sys.argv) != 2 or sys.argv[1].upper() not in ['ON', 'OFF', 'STATUS']:
logging.error('Invalid arguments. Usage: python3 remote_gpio_ssh.py [ON|OFF|STATUS]')
sys.exit(1)
command = sys.argv[1].upper()
relay = initialize_relay()
try:
if command == 'ON':
relay.on()
logging.info(f'Relay on GPIO {RELAY_PIN} energized (Circuit CLOSED).')
elif command == 'OFF':
relay.off()
logging.info(f'Relay on GPIO {RELAY_PIN} de-energized (Circuit OPEN).')
elif command == 'STATUS':
state = 'CLOSED (ON)' if relay.is_active else 'OPEN (OFF)'
logging.info(f'Relay GPIO {RELAY_PIN} current state: {state}')
print(state)
except Exception as e:
logging.error(f'Unexpected execution fault: {e}')
sys.exit(4)
finally:
# Safely release the pin back to the system
relay.close()
if __name__ == '__main__':
main()
Troubleshooting: Exact Error Strings and Ranked Fixes
When headless SSH fails, the terminal throws specific errors. Do not guess; match the exact string to the ranked causes below.
Error 1: ssh: connect to host 10.0.0.5 port 22: Connection refused
Meaning: The IP address is reachable (ARP resolved), but no service is listening on port 22.
- Cause A (Most Likely): The
sshtrigger file was placed in the wrong directory. In Bookworm, it must be in/boot/firmware, not/boot. - Cause B: The
sshdservice crashed on boot due to missing host keys (common if the Pi lost power during first-boot key generation). Fix via UART: Runsudo dpkg-reconfigure openssh-serverto regenerate keys. - Cause C: A strict
iptablesorufwrule is dropping port 22. Fix: Flush rules via UART console.
Error 2: user@10.0.0.5: Permission denied (publickey,password).
Meaning: The SSH daemon is running, but it rejected your credentials.
- Cause A (Most Likely): You are trying to log in as
pi. Thepiuser was removed for security reasons in Bookworm. You must use the custom username you defined during imaging. - Cause B: You disabled password authentication in
sshd_configbut your client isn't offering the correct private key. Fix: Explicitly point to your key:ssh -i ~/.ssh/id_ed25519 user@10.0.0.5. - Cause C: The
~/.sshdirectory on the Pi has incorrect permissions. Fix: Runchmod 700 ~/.sshandchmod 600 ~/.ssh/authorized_keys.
- File Extension: Did Windows secretly name your trigger file
ssh.txt? Delete it and recreate it via CMD. - Username: Are you typing
ssh pi@...? Stop. Use the custom user you created in Raspberry Pi Imager. - Network Isolation: Is your PC on the same VLAN/Subnet as the Pi? If the Pi is on an IoT VLAN and your PC is on the main LAN, your router's firewall will drop the SYN packet silently. Check your router's DHCP lease table to verify the Pi actually pulled an IP.
Extending the Build: Reverse Tunnels for Off-Grid Nodes
If you are deploying this Pi 5 behind a CGNAT (Carrier-Grade NAT) cellular router or a strict corporate firewall, inbound SSH will never work. You must extend the build using a reverse SSH tunnel.
How to Extend: Install autossh on the Pi (sudo apt install autossh). Configure a systemd service that forces the Pi to connect outbound to a public VPS (like a $5/mo DigitalOcean droplet) on port 2222, mapping it back to the Pi's local port 22. This allows you to SSH into your VPS, and tunnel straight into the Pi's GPIO environment, bypassing all NAT restrictions.
How to Simplify: If reverse tunnels and systemd daemons are overkill for your skill level, simplify the build by abandoning local SSH entirely. Instead, use the Raspberry Pi Connect beta service or install tailscale. Tailscale creates a WireGuard mesh network, giving your Pi a static 100.x.y.z IP address accessible from anywhere, requiring zero port forwarding and zero boot-partition file injection after the initial install.
For permanent IoT deployments, always terminate with key-based authentication (Ed25519) and disable password logins. Relying on password auth for an internet-facing GPIO node is a guaranteed way to end up with a compromised relay controller within 48 hours.






