The most reliable method for enabling SSH in Raspberry Pi OS (Bookworm and newer) is using the Raspberry Pi Imager's OS Customization menu to inject your public key and set a username before flashing. If you are flashing via a third-party tool like balenaEtcher, you must place an empty file named exactly ssh (with no file extension) in the root directory of the boot partition before the first boot.

Running a headless Raspberry Pi 5 as an IoT sensor node is a staple of embedded prototyping. Without a monitor or keyboard, SSH is your only lifeline for deploying code, reading sensor data, and debugging network drops. This guide walks through a complete headless deployment: enabling SSH, wiring a BME280 environmental sensor to the Pi 5's I2C bus, and deploying robust Python code with hardware-level error handling.

Project Spec Sheet & Parts List

Difficulty: Intermediate | Time: 45 Minutes | Target Board: Raspberry Pi 5 (8GB variant recommended for edge-compute headless nodes)
ComponentExact Variant / ModelNotes
MicrocontrollerRaspberry Pi 5 (8GB RAM)Requires 27W USB-C PD power supply for full peripheral current limit.
StorageSamsung EVO Plus 64GB microSDA2 speed rating required for OS Bookworm responsiveness.
SensorAdafruit BME280 I2C BreakoutMeasures temp, humidity, pressure. Default I2C address 0x77.
EnclosureArgon ONE V3 Pi 5 CaseProvides passive cooling and routes GPIO to a rear-facing connector.
Wiring28 AWG Silicone Jumper WiresFemale-to-female for direct breakout-to-GPIO header connection.

Step-by-Step: Enabling SSH in Raspberry Pi Headless

Modern Raspberry Pi OS disables SSH by default and no longer uses the default pi username. Follow these exact steps to ensure you aren't locked out on first boot.

  1. Download Raspberry Pi Imager: Install the latest version from the official Raspberry Pi software page.
  2. Select OS and Storage: Choose 'Raspberry Pi 5' as the device, select 'Raspberry Pi OS (64-bit)', and pick your microSD card.
  3. Open OS Customization: Click 'Next', then click 'Edit Settings' when prompted to apply OS customization.
  4. Set Username and Hostname: Create a specific user (e.g., sensor_admin) and set a strong password. Set the hostname to something identifiable like iot-node-01.local.
  5. Enable SSH: Navigate to the 'Services' tab. Select 'Enable SSH'. For production or exposed nodes, select 'Use password authentication'. For secure local networks, select 'Allow public-key authentication only' and paste your ~/.ssh/id_rsa.pub key.
  6. Flash and Inject (Alternative Method): If you bypassed Imager and used a raw image flasher, mount the newly flashed SD card on your PC. Navigate to the bootfs partition. Create a new file named ssh. Ensure your OS hasn't secretly named it ssh.txt. Eject safely.
  7. Boot and Connect: Insert the SD card into the Pi 5, apply power, and wait 60 seconds. Connect via terminal: ssh sensor_admin@iot-node-01.local.
Pro Tip: If you are using the ssh blank file method, you still need to know the default username. On Bookworm and newer, if you didn't pre-configure a user via Imager or userconf file, the Pi will halt the boot process waiting for a monitor to create one. Always use the Imager's user creation step for true headless setups.

Hardware Wiring: BME280 I2C Pin Mapping

Once SSH is active, we need to wire the sensor. The Raspberry Pi 5 retains the standard 40-pin header layout, but its I2C bus performance and pull-up resistor configurations are optimized for faster clock speeds. We will use the primary I2C1 bus.

BME280 Breakout PinRaspberry Pi 5 GPIO HeaderPhysical Pin #Function
VIN / VCC3V3 PowerPin 13.3V logic and power supply
GNDGroundPin 6Common ground reference
SCK / SCLGPIO 3 (SCL1)Pin 5I2C Serial Clock Line
SDI / SDAGPIO 2 (SDA1)Pin 3I2C Serial Data Line

Note: The Adafruit BME280 breakout includes onboard 3.3V regulation and 10kΩ I2C pull-up resistors. If you are using a raw, bare-bones BME280 chip from a bulk pack, you must add 4.7kΩ pull-up resistors between SDA/SCL and 3.3V, or the Pi 5's internal pull-ups may not suffice for stable readings at 400kHz.

Python Sensor Code with Error Handling

Headless nodes fail silently if code isn't written to catch hardware faults. The following Python script uses the lightweight smbus2 and bme280 libraries. It includes explicit error handling for I2C bus lockups, missing devices, and permission errors.

Prerequisites: Run sudo apt update && sudo apt install python3-smbus python3-pip -y and pip3 install RPi.bme280 smbus2 via your SSH session. Ensure I2C is enabled via sudo raspi-config (Interface Options -> I2C).

import smbus2
import bme280
import time
import sys

# Pin definitions and I2C configuration
I2C_BUS = 1          # Physical I2C1 bus on Pi 5 (GPIO 2/3)
BME280_ADDR = 0x77   # Default Adafruit address (0x76 for generic clones)
LOG_FILE = '/var/log/bme280_node.log'

def initialize_sensor():
    try:
        bus = smbus2.SMBus(I2C_BUS)
        # Load calibration parameters from sensor ROM
        calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
        print(f'Successfully initialized BME280 at address 0x{BME280_ADDR:02X}')
        return bus, calibration_params
    except FileNotFoundError:
        print('FATAL: I2C device file not found. Is I2C enabled in raspi-config?')
        sys.exit(1)
    except OSError as e:
        print(f'FATAL: Cannot access I2C bus or device. Check wiring and address. Error: {e}')
        sys.exit(1)

def read_and_log(bus, params):
    try:
        data = bme280.sample(bus, BME280_ADDR, params)
        temp_c = data.temperature
        humidity = data.humidity
        pressure_hpa = data.pressure
        
        log_entry = f'{time.strftime("%Y-%m-%d %H:%M:%S")} | Temp: {temp_c:.2f}C | Hum: {humidity:.1f}% | Press: {pressure_hpa:.2f}hPa'
        print(log_entry)
        
        with open(LOG_FILE, 'a') as f:
            f.write(log_entry + '\n')
            
    except OSError as e:
        print(f'WARNING: I2C read timeout or NACK received. Bus may be locked. Error: {e}')
    except Exception as e:
        print(f'ERROR: Unexpected failure during read: {e}')

if __name__ == '__main__':
    bus, params = initialize_sensor()
    try:
        while True:
            read_and_log(bus, params)
            time.sleep(10) # 10-second polling interval
    except KeyboardInterrupt:
        print('\nPolling stopped by user.')
        bus.close()

Debugging: Exact Error Strings and Ranked Causes

When a headless node goes dark, you need a systematic triage process. Before tearing apart the hardware, check the first three things:
1. Network Presence: Check your router's DHCP client list. Is the Pi actually pulling an IP address, or did it fail to connect to WiFi/Ethernet?
2. File Extension Trap: If using the blank file method, verify the file is named ssh and not ssh.txt (Windows hides extensions by default).
3. Username Mismatch: Are you trying to log in as pi? Modern OS versions require the custom username you created in Imager.

Error: 'ssh: connect to host 192.168.1.50 port 22: Connection refused'

This means your computer can ping the Pi's IP address, but the Pi is actively rejecting the SSH handshake.

  • Cause 1 (Most Likely): The SSH daemon (sshd) is not running. The ssh trigger file was missing, misnamed, or placed in the wrong partition (it must be in bootfs, not rootfs).
  • Cause 2: The Pi is still booting. The Pi 5 boots fast, but if you are running filesystem checks on a large SD card, sshd might not have started yet. Wait 90 seconds.
  • Cause 3: A local firewall (like ufw) was enabled on a previous image and blocked port 22.

Fix: Pull the SD card, mount it on your PC, verify the ssh file in the root of the boot partition, and re-insert.

Error: 'user@192.168.1.50: Permission denied (publickey).'

This means SSH is running, the user exists, but the cryptographic handshake failed.

  • Cause 1 (Most Likely): You selected 'Allow public-key authentication only' in Imager, but pasted the wrong key, or your local machine's SSH agent is offering a different key than the one on the Pi.
  • Cause 2: File permissions on the Pi's ~/.ssh/authorized_keys file are too open. SSH rejects keys if the file is writable by group/others.
  • Cause 3: You are trying to log in as root, which is disabled by default in Raspberry Pi OS.

Fix: Force password auth temporarily by editing /boot/firmware/ssh configs if accessible, or re-flash using the Imager with 'Use password authentication' selected to regain access. Read the official remote access documentation for key generation details.

Extending and Simplifying the Build

To Simplify: If you don't need historical logging, strip the LOG_FILE write operations from the Python script and rely entirely on systemd-journald to capture the print() stdout when you wrap the script in a systemd service. This saves SD card write cycles, extending the life of cheap microSD cards in 24/7 headless deployments.

To Extend: Add a cellular fallback. The Raspberry Pi 5's PCIe Gen 2 lane allows you to connect an M.2 HAT with a 4G/LTE modem (like the Quectel RM500Q). You can write a watchdog script that pings your router; if the local network drops, it triggers the LTE modem to push the BME280 data to an MQTT broker via TLS, ensuring zero data loss for remote environmental monitoring.

FAQ: Enabling SSH in Raspberry Pi

How to enable SSH in Raspberry Pi without a monitor?

The standard method is to place an empty, extension-less file named ssh in the root directory of the bootfs partition of the SD card before inserting it into the Pi. Alternatively, use the Raspberry Pi Imager's advanced settings (gear icon or Ctrl+Shift+X) to check the 'Enable SSH' box and configure your username and password before flashing the OS.

Why is my Raspberry Pi SSH connection refused on first boot?

A 'Connection refused' error on a fresh headless boot almost always means the SSH daemon never started. This happens if the ssh trigger file was saved with a hidden .txt extension by Windows, or if it was placed in the rootfs partition instead of the bootfs partition. It can also occur if the Pi is hanging on a first-boot filesystem resize; wait two minutes and try again.

How do I find my headless Raspberry Pi's IP address on the network?

If you set a hostname in the Imager (e.g., iot-node-01), you can usually connect using mDNS by typing ssh user@iot-node-01.local. If mDNS fails, log into your router's admin panel and check the DHCP client list for the Pi's MAC address (which starts with b8:27:eb, dc:a6:32, or 2c:cf:67 for Pi 5 boards). You can also use a network scanner app like Fing on your smartphone.

Can I enable SSH in Raspberry Pi OS after it has already booted headless?

If you are already locked out of a headless Pi that booted without SSH enabled, you cannot enable it remotely. You must power down the Pi, remove the microSD card, insert it into a PC, and use the blank ssh file method on the boot partition. Upon the next boot, the OS will detect the file, enable the sshd service, and delete the trigger file.