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.
OS: Raspberry Pi OS (Bookworm, 64-bit).
Components: 3x 3mm LEDs (Green, Yellow, Red), 3x 330Ω resistors, female-to-female jumper wires.
| Component | BCM GPIO Pin | Physical Pin | Function / Trigger |
|---|---|---|---|
| Green LED | GPIO 17 | 11 | Network Link Active (Ping successful) |
| Yellow LED | GPIO 27 | 13 | SSHD Port 22 Listening |
| Red LED | GPIO 22 | 15 | Auth Failure / Daemon Crash |
| Common Ground | GND | 9 | Cathode 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.
| Parameter | Default Value | Embedded Value | Engineering Rationale |
|---|---|---|---|
PermitRootLogin | prohibit-password | no | Forces all access through a standard user (e.g., pi or admin), requiring sudo for privilege escalation and leaving a clear audit trail. |
PasswordAuthentication | yes | no | Eliminates brute-force vector. Requires cryptographic key pairs. |
PubkeyAuthentication | yes | yes | Explicitly enforces key-based auth. We use Ed25519 keys for their small size and speed on ARM cores. |
ClientAliveInterval | 0 | 60 | Sends a keepalive packet every 60 seconds. Critical for keeping NAT table mappings open on cellular routers. |
ClientAliveCountMax | 3 | 5 | Allows 5 missed keepalives (5 minutes) before dropping the dead session, preventing ghost SSH sessions from exhausting Pi memory. |
MaxAuthTries | 6 | 2 | Drops the TCP connection after 2 failed key attempts, severely throttling automated scanning bots. |
AllowUsers | (none) | pi admin | Whitelists 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.
- Generate the Key Pair (Host Machine):
ssh-keygen -t ed25519 -C "pi5-node-01" -f ~/.ssh/pi5_node01_ed25519 - 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). - 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 - Lock Down the Daemon:
Apply thesshd_configtable values from above, runsudo sshd -t, and restart the service. - 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
sshservice is not running. Check withsystemctl status ssh. On fresh Raspberry Pi OS images, SSH is disabled by default unless a blanksshfile was placed in the/boot/firmwarepartition during imaging. - Cause B: A local firewall (UFW or iptables) is dropping or rejecting port 22. Run
sudo ufw statusto verify. - Cause C: The
sshddaemon crashed due to a syntax error insshd_config. Check logs viajournalctl -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 ~/.sshandchmod 600 ~/.ssh/authorized_keys. If thepiuser's home directory is writable by others, auth fails silently. - Cause B: You are offering the wrong key. Run your SSH command with
-vvvto see exactly which key files the client is presenting to the server. - Cause C:
PasswordAuthentication nois set insshd_config, but you never actually copied the public key to the Pi'sauthorized_keysfile.
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
.localmDNS hostname.
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.






