To securely configure a headless Raspberry Pi for embedded deployments, you must disable password authentication, enforce Ed25519 key pairs, and tune ClientAliveInterval in /etc/ssh/sshd_config. Default SSH settings are designed for interactive desktop use, not for remote sensor nodes sitting in NEMA enclosures or on factory floors. A proper raspberry pi ssh config hardens the daemon against brute-force attacks, prevents dropped NAT sessions, and provides deterministic hardware feedback when the network stack fails.

Parts List & GPIO Pin Mapping

Before modifying the daemon, we need physical visibility into the node's state. When a Pi is headless and mounted in an enclosure, a few status LEDs wired to the GPIO header save you from plugging in a monitor to diagnose a boot-loop or network drop.

Target Hardware: Raspberry Pi 5 (8GB variant, BCM2712 SoC, RP1 southbridge).
OS: Raspberry Pi OS (Bookworm, 64-bit).
Components: 3x 3mm LEDs (Green, Yellow, Red), 3x 330Ω resistors, female-to-female jumper wires.
ComponentBCM GPIO PinPhysical PinFunction / Trigger
Green LEDGPIO 1711Network Link Active (Ping successful)
Yellow LEDGPIO 2713SSHD Port 22 Listening
Red LEDGPIO 2215Auth Failure / Daemon Crash
Common GroundGND9Cathode return path for all LEDs

Note on Pi 5 GPIOs: The Raspberry Pi 5 routes GPIOs through the RP1 southbridge chip rather than the main BCM2712 SoC. However, the gpiozero library abstracts this seamlessly; you still use standard BCM numbering (17, 27, 22) in your code.

Hardening the sshd_config for Embedded Use

The default /etc/ssh/sshd_config allows password logins and root access, which is unacceptable for an internet-facing or industrial IoT node. Below is the exact parameter matrix you should apply to a remote embedded Pi. These settings balance strict security with the realities of flaky cellular or long-haul NAT connections.

ParameterDefault ValueEmbedded ValueEngineering Rationale
PermitRootLoginprohibit-passwordnoForces all access through a standard user (e.g., pi or admin), requiring sudo for privilege escalation and leaving a clear audit trail.
PasswordAuthenticationyesnoEliminates brute-force vector. Requires cryptographic key pairs.
PubkeyAuthenticationyesyesExplicitly enforces key-based auth. We use Ed25519 keys for their small size and speed on ARM cores.
ClientAliveInterval060Sends a keepalive packet every 60 seconds. Critical for keeping NAT table mappings open on cellular routers.
ClientAliveCountMax35Allows 5 missed keepalives (5 minutes) before dropping the dead session, preventing ghost SSH sessions from exhausting Pi memory.
MaxAuthTries62Drops the TCP connection after 2 failed key attempts, severely throttling automated scanning bots.
AllowUsers(none)pi adminWhitelists specific usernames. Even if a bot guesses a valid system user (like postgres), SSH rejects it at the handshake.

After editing /etc/ssh/sshd_config, always validate the syntax before restarting the daemon. A typo here will lock you out of a headless node permanently.

sudo sshd -t
sudo systemctl restart ssh

Step-by-Step Headless Key Deployment

Do not use RSA-2048 keys in 2026. Ed25519 keys are computationally cheaper for the Pi's ARM processor and offer superior security margins. Generate and deploy them from your host workstation.

  1. Generate the Key Pair (Host Machine):
    ssh-keygen -t ed25519 -C "pi5-node-01" -f ~/.ssh/pi5_node01_ed25519
  2. Push the Public Key to the Pi:
    ssh-copy-id -i ~/.ssh/pi5_node01_ed25519.pub pi@192.168.1.50
    (You must do this while password authentication is still temporarily enabled).
  3. Verify Directory Permissions on the Pi:
    SSH will silently reject keys if the directory permissions are too open. Run this on the Pi:
    chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
  4. Lock Down the Daemon:
    Apply the sshd_config table values from above, run sudo sshd -t, and restart the service.
  5. Test the Hardened Connection:
    ssh -i ~/.ssh/pi5_node01_ed25519 -o PasswordAuthentication=no pi@192.168.1.50

Python SSH & Network Monitor Script

This script targets the Raspberry Pi 5 (8GB) running Bookworm 64-bit. It uses the gpiozero library to toggle our status LEDs based on real-time checks of the sshd systemd service and the TCP state of port 22. It includes robust error handling for socket timeouts and subprocess failures.

import socket
import time
import subprocess
from gpiozero import LED
from signal import pause

# Pin definitions mapped to physical GPIOs (BCM numbering)
NET_LED = LED(17)   # GPIO 17 - Network reachability
SSH_LED = LED(27)   # GPIO 27 - SSH Port 22 open and listening
ERR_LED = LED(22)   # GPIO 22 - Auth failure or Daemon crashed

TARGET_IP = "127.0.0.1"
SSH_PORT = 22

def check_ssh_port(host, port, timeout=2.0):
    """Checks if the SSH daemon is actively accepting TCP 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"[ERROR] Socket check failed: {e}")
        return False

def check_sshd_service():
    """Queries systemd to verify the ssh.service unit is active."""
    try:
        result = subprocess.run(
            ["systemctl", "is-active", "ssh"],
            capture_output=True, text=True, check=False, timeout=3
        )
        return result.stdout.strip() == "active"
    except subprocess.TimeoutExpired:
        print("[ERROR] Systemctl query timed out.")
        return False
    except Exception as e:
        print(f"[ERROR] Subprocess execution failed: {e}")
        return False

def main():
    print("Starting SSH Hardware Monitor on Pi 5...")
    try:
        while True:
            # Assume network is up if local loopback responds (simplified for local node)
            NET_LED.on() 
            
            sshd_running = check_sshd_service()
            port_open = check_ssh_port(TARGET_IP, SSH_PORT)
            
            if sshd_running and port_open:
                SSH_LED.on()
                ERR_LED.off()
            elif sshd_running and not port_open:
                # Daemon is loaded but port is blocked (e.g., iptables/ufw rule)
                SSH_LED.blink(on_time=0.5, off_time=0.5)
                ERR_LED.off()
            else:
                # Daemon crashed or disabled
                SSH_LED.off()
                ERR_LED.on()
                
            time.sleep(5)
    except KeyboardInterrupt:
        print("\nMonitor interrupted. Cleaning up GPIOs.")
    finally:
        NET_LED.off()
        SSH_LED.off()
        ERR_LED.off()

if __name__ == "__main__":
    main()

Debugging: Exact Error Strings & Ranked Causes

When headless nodes fail, the SSH client spits out cryptic strings. Here is the decision path for the three most common failures, including the first three things to check before tearing apart your hardware.

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

This means the TCP SYN packet reached the Pi, but the OS actively rejected it. The network is fine; the application layer is the problem.

  • Cause A (Most Likely): The ssh service is not running. Check with systemctl status ssh. On fresh Raspberry Pi OS images, SSH is disabled by default unless a blank ssh file was placed in the /boot/firmware partition during imaging.
  • Cause B: A local firewall (UFW or iptables) is dropping or rejecting port 22. Run sudo ufw status to verify.
  • Cause C: The sshd daemon crashed due to a syntax error in sshd_config. Check logs via journalctl -u ssh -n 50.

2. "pi@192.168.1.50: Permission denied (publickey)."

The TCP connection succeeded, the SSH handshake completed, but the Pi rejected your cryptographic proof.

  • Cause A (Most Likely): File permissions are too loose. OpenSSH strictly enforces chmod 700 ~/.ssh and chmod 600 ~/.ssh/authorized_keys. If the pi user's home directory is writable by others, auth fails silently.
  • Cause B: You are offering the wrong key. Run your SSH command with -vvv to see exactly which key files the client is presenting to the server.
  • Cause C: PasswordAuthentication no is set in sshd_config, but you never actually copied the public key to the Pi's authorized_keys file.

3. "ssh: connect to host 192.168.1.50 port 22: Network is unreachable"

This is a local routing issue on your host machine, not the Pi. Your host PC doesn't know how to route packets to the 192.168.1.x subnet.

  • Cause A: Your host machine is on a different VLAN or subnet and lacks a route.
  • Cause B: The Pi's Ethernet/WiFi interface is down. (This is where your Green GPIO 17 LED saves you a trip to the site).
  • Cause C: Typo in the IP address or DNS resolution failure if using a .local mDNS hostname.
The First Three Things to Check Rule:
1. Ping the IP to verify Layer 3 routing.
2. Check the physical GPIO LEDs (or run systemctl status ssh via a serial console).
3. Verify ~/.ssh directory permissions (must be strictly 700).

Extending and Simplifying the Build

Depending on your deployment environment, you may need to scale this monitoring setup up or strip it down.

How to Extend the Build

For remote cellular deployments (e.g., using a Sixfab LTE HAT), add an MQTT publishing block to the Python script. Instead of just blinking a red LED when sshd crashes, publish a node/01/status/ssh payload to your AWS IoT Core or Mosquitto broker. You can also wire a 0.96" I2C OLED display to GPIO 2 (SDA) and GPIO 3 (SCL) to print the Pi's current IP address and SSH fingerprint directly on the enclosure for field technicians.

How to Simplify the Build

If you are deploying 50 nodes and don't want to wire LEDs, drop the Python script entirely. Rely purely on the hardened sshd_config and systemd's built-in watchdog. Add WatchdogSec=60 and Restart=on-failure to a custom /etc/systemd/system/ssh.service.d/override.conf file. This instructs the Linux kernel to automatically restart the SSH daemon if it hangs, removing the need for application-layer polling.

For further reading on secure remote access protocols, refer to the official Raspberry Pi Remote Access Documentation and the canonical OpenBSD sshd_config manual.