Most makers search for an SSH client for Raspberry Pi when they simply need to connect to their board from a desktop PC. If you are on Windows 11, MobaXterm or the built-in Windows Terminal (OpenSSH) are the top choices. On macOS and Linux, the native ssh command in Terminal is all you need. For cross-platform mobile and desktop syncing, Termius remains the premium standard.
But what if your Raspberry Pi needs to act as the SSH client to pull data from a remote server and trigger physical hardware? Connecting to your Pi is just step one. This guide covers the best SSH clients to access your Pi, followed by a complete embedded project where the Pi uses Python's paramiko library to SSH into a remote host, read a state file, and toggle a 4-channel GPIO relay.
Project Spec Sheet & Parts List
| Parameter | Specification |
|---|---|
| Target Board Variant | Raspberry Pi 4 Model B (4GB RAM, Rev 1.5) |
| Operating System | Raspberry Pi OS (Bookworm, 64-bit, Lite or Desktop) |
| Difficulty Rating | Intermediate (Requires SSH key generation & Linux permissions) |
| Estimated Build Time | 45 Minutes |
| Core Python Libraries | paramiko (SSHv2), gpiozero (Hardware control) |
Required Hardware
- Microcontroller: Raspberry Pi 4 Model B (4GB) with official 27W USB-C power supply.
- Relay Module: Songle SRD-05VDC-SL-C 4-Channel Relay Module (Opto-isolated, active-low logic).
- Load (for testing): 12V LED strip segment and a Mean Well LRS-35-12 (12V 3A) power supply.
- Wiring: 22 AWG solid core jumper wires (Dupont female-to-female for Pi GPIO).
Wiring the Opto-Isolated Relay Module
The standard blue 4-channel relay modules use PC817 optocouplers. This provides galvanic isolation between the Pi's sensitive 3.3V logic and the relay's 5V coil, protecting your board from inductive kickback. Note that these modules are active-low, meaning the GPIO pin must be pulled to GND (0V) to energize the relay.
Pin Mapping Table
| Raspberry Pi 4 Pin (BCM) | Physical Pin # | Relay Module Pin | Function |
|---|---|---|---|
| 5V (Power) | 2 or 4 | VCC | Logic power (3.3V/5V tolerant on Pi 4) |
| GND | 6 | GND | Common ground reference |
| GPIO 17 | 11 | IN1 | Relay 1 Control (Active-Low) |
| GPIO 27 | 13 | IN2 | Relay 2 Control (Active-Low) |
| GPIO 22 | 15 | IN3 | Relay 3 Control (Active-Low) |
| GPIO 23 | 16 | IN4 | Relay 4 Control (Active-Low) |
Your relay module likely has a jumper cap connecting
VCC and JD-VCC. For basic use powered directly from the Pi's 5V pin, leave this jumper ON. If you are switching heavy inductive loads (like AC motors) and want true optical isolation, remove the jumper, connect JD-VCC to a separate external 5V supply, and connect the Pi's 5V to VCC.
The Python SSH Client Code (Paramiko)
This script uses paramiko, the standard SSHv2 library for Python (Paramiko Documentation). It connects to a remote server, reads a comma-separated string of 1s and 0s from a text file, and applies those states to the local GPIO relays.
Prerequisites: Install the library via terminal: sudo apt install python3-paramiko python3-gpiozero. Ensure you have generated an Ed25519 SSH key on the Pi (ssh-keygen -t ed25519) and copied it to the remote server (ssh-copy-id user@remote_host).
import paramiko
from gpiozero import OutputDevice
from time import sleep
import sys
import logging
# Configure logging for debugging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Pin definitions for active-low relay module (BCM numbering)
RELAY_PINS = [17, 27, 22, 23]
# active_high=False handles the active-low logic of the optocouplers
relays = [OutputDevice(pin, active_high=False, initial_value=False) for pin in RELAY_PINS]
# SSH Client Configuration
REMOTE_HOST = "192.168.1.100"
REMOTE_USER = "admin"
SSH_KEY_PATH = "/home/pi/.ssh/id_ed25519"
REMOTE_FILE = "/var/www/html/relay_states.txt"
POLL_INTERVAL = 10 # Seconds between SSH checks
def fetch_remote_states():
"""Connects via SSH and reads the relay state file."""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(
hostname=REMOTE_HOST,
username=REMOTE_USER,
key_filename=SSH_KEY_PATH,
timeout=5.0,
allow_agent=False,
look_for_keys=False
)
stdin, stdout, stderr = client.exec_command(f"cat {REMOTE_FILE}")
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0:
logging.error(f"Remote command failed: {stderr.read().decode('utf-8')}")
return None
states_str = stdout.read().decode('utf-8').strip()
# Expecting format: "1,0,1,0"
return [int(s) for s in states_str.split(',')]
except paramiko.ssh_exception.AuthenticationException as e:
logging.critical(f"AUTH ERROR: {e}")
return None
except TimeoutError:
logging.error("CONN ERROR: Socket timed out after 5.0s")
return None
except Exception as e:
logging.error(f"UNEXPECTED ERROR: {e}")
return None
finally:
client.close()
def apply_states(states):
"""Applies the fetched states to the GPIO relays."""
if len(states) != len(relays):
logging.warning(f"State mismatch: expected {len(relays)}, got {len(states)}")
return
for relay, state in zip(relays, states):
if state == 1:
relay.on()
else:
relay.off()
logging.info(f"Relays updated to: {states}")
if __name__ == "__main__":
logging.info("Starting SSH GPIO Relay Client...")
try:
while True:
states = fetch_remote_states()
if states is not None:
apply_states(states)
else:
logging.warning("Failed to fetch states. Keeping previous relay state.")
sleep(POLL_INTERVAL)
except KeyboardInterrupt:
logging.info("Shutting down and turning off all relays.")
for relay in relays:
relay.off()
sys.exit(0)
Debugging: Authentication and Connection Errors
When automating SSH via Python, you lose the interactive prompts of a standard terminal. If your script fails silently or loops endlessly, check the console output for these exact error strings.
Exact Error: paramiko.ssh_exception.AuthenticationException: Authentication failed.
This means the remote server rejected the Pi's credentials. Ranked causes:
- Key Permissions: The remote server's
~/.sshdirectory must be700andauthorized_keysmust be600. If they are too open,sshdsilently ignores them. - Wrong Key Path: The
SSH_KEY_PATHin the script points to the public key (.pub) instead of the private key, or the path is incorrect. - Key Type Mismatch: Older servers might reject Ed25519 keys. Regenerate using
ssh-keygen -t rsa -b 4096if connecting to legacy Linux boxes.
Exact Error: Connection refused or Socket timed out
The first three things to check when it fails:
- Is
sshdrunning on the target? Runsudo systemctl status sshdon the remote machine. On many minimal IoT gateways, the SSH daemon is disabled by default. - Network Routing & Firewalls: Can the Pi actually ping the remote host? Run
ping -c 4 192.168.1.100. If it pings but SSH fails, check ifufworiptableson the remote host is blocking port 22. - IP Allowlists: Check the remote host's
/etc/ssh/sshd_config. IfAllowUsersorMatch Addressdirectives are present, the Pi's IP address must be explicitly whitelisted.
systemd service, standard print() statements won't show in your terminal. Use journalctl -u your-service-name.service -f to tail the live logs and catch authentication errors in real-time.
Extending and Simplifying the Build
How to simplify: If setting up SSH keys and managing sshd permissions feels like overkill for a simple state toggle, drop SSH entirely. Replace the paramiko block with Python's requests library to fetch a JSON payload from a lightweight HTTP endpoint (like a Node-RED dashboard or a basic Flask API). HTTP is stateless, requires no key management, and is much easier to debug via a standard web browser.
How to extend: For bidirectional communication where the remote server pushes commands to the Pi instantly (rather than the Pi polling every 10 seconds), implement an MQTT broker (like Mosquitto). The Pi subscribes to a topic, and the relay toggles the millisecond a payload arrives. Alternatively, if you must stick to SSH, look into the Fabric library, which wraps Paramiko in a higher-level API designed specifically for executing remote shell commands and managing sudo privileges.
Frequently Asked Questions
What is the best free SSH client for Raspberry Pi on Windows?
For Windows 10 and 11, the built-in OpenSSH client accessed via Windows Terminal or PowerShell is the best choice for 90% of users. It requires no installation and supports Ed25519 keys natively. If you need a GUI-based SFTP file manager alongside your terminal, MobaXterm (free tier) is the undisputed king, offering split-screen terminal and drag-and-drop file transfers.
How do I enable the SSH server on my Raspberry Pi headless?
If you are flashing Raspberry Pi OS using the official Raspberry Pi Imager, click the gear icon (Advanced Options) before writing the image. Check "Enable SSH" and select "Use password authentication" or "Allow public-key". If you've already flashed the SD card, mount it on your PC, open the bootfs partition, and create an empty file named exactly ssh (no file extension). The Pi will enable the daemon on first boot and delete the file. See the official Raspberry Pi remote access documentation for more details.
Why does my SSH client say 'Connection refused' on port 22?
"Connection refused" means your client successfully reached the Pi's IP address, but the Pi's operating system actively rejected the TCP handshake on port 22. This almost always means the sshd service is not running. Run sudo raspi-config, navigate to Interface Options > SSH, and enable it. If it is already enabled, check if you have a firewall rule blocking it via sudo ufw status.






