To SSH into a Raspberry Pi for a headless hardware build, enable the SSH daemon by placing an empty file named ssh in the boot partition or using the Raspberry Pi Imager's advanced settings, locate the device's IP via your router's DHCP table or ping raspberrypi.local, and connect via terminal using ssh your_username@ip_address. For embedded projects running on the latest Raspberry Pi OS (Bookworm), you must also configure the lgpio backend to interact with the GPIO header remotely.

Running a Pi without a monitor, keyboard, or mouse (headless) is the standard deployment for industrial sensor nodes, MQTT gateways, and automated relays. This guide walks through provisioning a Raspberry Pi 5 for a remote I2C sensor build, writing the control code, and debugging the exact SSH errors that halt jobsite deployments.

Project Spec Sheet & Hardware Requirements

Before writing a single line of code, verify your hardware. The Raspberry Pi 5 has stricter power delivery requirements than the Pi 4; running I2C sensors and WiFi simultaneously on a marginal power supply will cause silent brownouts that drop your SSH session and corrupt the SD card.

Bill of Materials (BOM)

  • Compute: Raspberry Pi 5 (8GB variant) - Required for heavy edge-computing or multi-threaded MQTT logging.
  • Power: Official Raspberry Pi 27W USB-C PD Power Supply - Do not use generic 5V/3A phone chargers; the Pi 5 negotiates 5V/5A via PD.
  • Storage: SanDisk Extreme 64GB microSD (A2 rating) - A2 ensures the random I/O performance needed for OS logging over SSH.
  • Sensor: Adafruit BME280 I2C Temperature/Humidity/Pressure breakout.
  • Indicator: Standard 5mm LED with a 330Ω current-limiting resistor.

Network & SSH Protocol Specifications

When configuring your router or firewall for a remote Pi node, use these baseline parameters for secure, modern SSH access.

Parameter Specification / Value Notes for Embedded Deployments
Protocol Version SSH-2 (OpenSSH 9.x) SSH-1 is obsolete and rejected by modern Pi OS defaults.
Default Port 22 (TCP) Change to a non-standard port (e.g., 2222) if exposed to the WAN via port forwarding to reduce botnet noise.
Key Exchange Curve25519-SHA256 Default on Bookworm; do not downgrade to RSA-1024 for legacy compatibility.
Authentication Ed25519 Public Key Disable password authentication in sshd_config after verifying key-based login works.
Encryption AES256-GCM Hardware-accelerated on the Pi 5's Cortex-A76 cores; negligible CPU overhead.

BME280 I2C Pin Mapping

The Pi 5's I2C bus layout remains compatible with previous generations, but physical pin numbering must be exact. Wire the BME280 to the primary I2C bus (Bus 1).

BME280 Pin Pi 5 GPIO Header Pin BCM GPIO Number Function
VIN / 3V3 Pin 1 N/A 3.3V Power
GND Pin 6 N/A Ground Reference
SCK / SCL Pin 5 GPIO 3 (SCL1) I2C Clock
SDI / SDA Pin 3 GPIO 2 (SDA1) I2C Data

Step-by-Step Headless SSH Configuration

Do not boot the Pi with a monitor just to enable SSH. Use the Raspberry Pi Imager's advanced settings to inject credentials and network configs before the SD card ever touches the board.

  1. Flash the OS with Advanced Settings: Open Raspberry Pi Imager, select Raspberry Pi OS (64-bit) Bookworm. Click the gear icon (or press Ctrl+Shift+X). Check Enable SSH and select Use password authentication (we will switch to keys later). Set your custom username, password, and inject your local WiFi SSID/password.
  2. Boot and Discover the IP: Insert the SD card and apply power. Wait 60 seconds for the first-boot resize and network connection. From your workstation terminal, run ping raspberrypi.local. If mDNS is blocked on your network, check your router's DHCP client list for the MAC address starting with dc:a6:32 or 2c:cf:67 (common Pi OUI blocks).
  3. Generate and Copy SSH Keys: On your host machine, generate an Ed25519 key pair: ssh-keygen -t ed25519 -C 'pi5-sensor-node'. Copy it to the Pi: ssh-copy-id -i ~/.ssh/id_ed25519.pub username@raspberrypi.local.
  4. Harden the SSH Daemon: SSH into the Pi. Edit the config via sudo nano /etc/ssh/sshd_config. Set PasswordAuthentication no and PermitRootLogin no. Restart the service with sudo systemctl restart ssh.
Bench Tip: If you are deploying multiple identical Pi nodes, do not copy the same SD card image. The SSH host keys and machine-id will be identical, causing severe routing and security conflicts on your network. Always flash individually or use a post-boot script to regenerate /etc/machine-id and /etc/ssh/ssh_host_* keys.

Remote GPIO Control Code (Python)

The code below targets the Raspberry Pi 5 (8GB) running Bookworm (Python 3.11+). Because the legacy RPi.GPIO library is incompatible with the Pi 5's new RP1 southbridge chip, this script uses gpiozero (which defaults to the lgpio backend on Bookworm) for the LED, and smbus2 for raw I2C communication with the BME280.

Install dependencies via your SSH session before running: sudo apt update && sudo apt install python3-gpiozero python3-smbus2 i2c-tools.

import time
import sys
from gpiozero import LED
from smbus2 import SMBus

# --- Pin & Address Definitions ---
# BCM GPIO 17 is Physical Pin 11 on the Pi 5 header
STATUS_LED_PIN = 17  
# Default BME280 I2C address (SDO tied to GND)
BME280_I2C_ADDR = 0x76 
I2C_BUS_ID = 1

# Initialize GPIO via gpiozero (uses lgpio backend on Pi 5)
status_led = LED(STATUS_LED_PIN)

def read_bme280_temp(bus):
    """Reads uncompensated temperature from BME280 register 0xFA."""
    try:
        # Read 3 bytes from the temp register
        data = bus.read_i2c_block_data(BME280_I2C_ADDR, 0xFA, 3)
        adc_t = ((data[0] << 16) | (data[1] << 8) | data[2]) >> 4
        # Simplified conversion for demonstration (real-world requires calibration registers)
        temp_c = (adc_t / 16384.0) * 25.0 
        return round(temp_c, 2)
    except OSError as e:
        print(f'I2C Bus Error: {e}. Check wiring and pull-ups.', file=sys.stderr)
        return None

def main():
    print(f'Starting sensor node on I2C Bus {I2C_BUS_ID}...')
    status_led.blink(on_time=0.5, off_time=0.5, n=2) # Visual boot confirmation
    
    try:
        with SMBus(I2C_BUS_ID) as bus:
            # Wake up BME280 (Write 0x00 to ctrl_meas register 0xF4 for sleep, then normal)
            bus.write_byte_data(BME280_I2C_ADDR, 0xF4, 0x27)
            time.sleep(0.1) # Allow sensor startup
            
            while True:
                temp = read_bme280_temp(bus)
                if temp is not None:
                    print(f'Node Temp: {temp} C')
                    status_led.on()
                else:
                    status_led.blink(on_time=0.1, off_time=0.1) # Fast blink on error
                time.sleep(5)
                
    except KeyboardInterrupt:
        print('\nSSH session terminated by user. Cleaning up GPIO.')
    except FileNotFoundError:
        print(f'Fatal: I2C Bus {I2C_BUS_ID} not found. Run "sudo raspi-config" to enable I2C.', file=sys.stderr)
    finally:
        status_led.off()
        status_led.close()

if __name__ == '__main__':
    main()

Debugging SSH Connection Failures

When a headless Pi drops off the network or refuses your connection, you are flying blind. Before pulling the SD card or plugging in a monitor, evaluate the exact error string returned by your SSH client. Here are the first three things to check when it fails, mapped to the specific terminal output.

1. Error: ssh: connect to host 192.168.1.50 port 22: Connection refused

What it means: Your workstation reached the IP address, but the Pi's OS actively rejected the TCP connection on port 22.

  • Cause A (Most Likely): The SSH daemon is not running or was never enabled. If you forgot the ssh file in the boot partition, the Pi OS disables SSH by default for security.
  • Cause B: The Pi has crashed or is caught in a boot loop due to a power brownout. The IP is still leased in your router's ARP table, but the OS isn't up.
  • Fix: Ping the IP. If it replies, SSH is disabled. Power down, mount the SD card on your PC, and place an empty file named exactly ssh (no extension) in the root of the bootfs partition.

2. Error: Permission denied (publickey).

What it means: The SSH handshake succeeded, but the Pi rejected your authentication credentials.

  • Cause A: You disabled password authentication in sshd_config, but your host machine is trying to use a password or the wrong key pair.
  • Cause B: File permissions on the Pi are too open. OpenSSH strictly enforces that ~/.ssh/authorized_keys must be 600 and the ~/.ssh directory must be 700.
  • Fix: Force key-based auth explicitly from your host: ssh -i ~/.ssh/id_ed25519 username@192.168.1.50. If that fails, you must connect a physical keyboard/monitor to the Pi to fix the chmod permissions on the .ssh directory.

3. Error: WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

What it means: The cryptographic fingerprint of the server at this IP address does not match what your workstation has saved in ~/.ssh/known_hosts.

  • Cause A (Benign): You re-flashed the Pi's SD card with a fresh OS, generating new host keys, but the router assigned it the same DHCP IP address.
  • Cause B (Malicious): An active Man-In-The-Middle (MITM) attack is intercepting your traffic (highly unlikely on a local home LAN, but possible in hostile corporate environments).
  • Fix: If you know you re-flashed the Pi, clear the old key from your host machine by running: ssh-keygen -R 192.168.1.50. Reconnect and accept the new fingerprint.
Safety Note: Never blindly accept a changed host identification fingerprint if you are connecting to a Pi over the public internet or an untrusted WiFi network. Always verify the fingerprint via an out-of-band method (like a physical console) to prevent credential theft.

Extending and Simplifying the Build

Once your SSH session is stable and the Python script is polling data, you need to transition from a manual terminal session to a resilient embedded node.

How to Extend the Build

  • Add MQTT Telemetry: Install paho-mqtt via pip. Modify the Python script to publish the temp_c variable to a Mosquitto broker topic like sensors/pi5/bme280/temp. This allows Node-RED or Home Assistant to ingest the data without maintaining a persistent SSH tunnel.
  • Deploy as a Systemd Service: Do not rely on tmux or screen to keep your script running after you close the SSH session. Create a .service file in /etc/systemd/system/ to run the Python script on boot, complete with automatic restart directives (Restart=on-failure) if the I2C bus throws an unhandled exception.
  • Watchdog Timer: Enable the Pi 5's hardware watchdog. If the OS freezes (common with flaky USB-C power), the hardware watchdog will force a hard reboot, bringing your SSH and sensor services back online automatically.

How to Simplify the Build

  • Assign a Static IP: DHCP lease expirations change your Pi's IP, breaking your automated SSH scripts and cron jobs. Edit /etc/NetworkManager/system-connections/ or use raspi-config to lock the Pi to a static IP outside your router's DHCP pool.
  • Enable Serial Console: If WiFi fails and you don't have an Ethernet drop nearby, SSH is useless. Enable the serial console in raspi-config (Interface Options -> Serial Port). This allows you to plug a $10 USB-to-TTL serial cable (like the CP2102) directly into GPIO 14 (TXD) and 15 (RXD) to access a root shell without a network.

For deeper reference on OpenSSH hardening parameters specific to Debian-based embedded systems, consult the official Raspberry Pi remote access documentation. For Python GPIO pin factory configurations on the Pi 5, review the gpiozero library documentation to ensure your scripts leverage the correct lgpio backend.