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)
Callout Tip: The JD-VCC Jumper
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:

  1. Key Permissions: The remote server's ~/.ssh directory must be 700 and authorized_keys must be 600. If they are too open, sshd silently ignores them.
  2. Wrong Key Path: The SSH_KEY_PATH in the script points to the public key (.pub) instead of the private key, or the path is incorrect.
  3. Key Type Mismatch: Older servers might reject Ed25519 keys. Regenerate using ssh-keygen -t rsa -b 4096 if connecting to legacy Linux boxes.

Exact Error: Connection refused or Socket timed out

The first three things to check when it fails:

  1. Is sshd running on the target? Run sudo systemctl status sshd on the remote machine. On many minimal IoT gateways, the SSH daemon is disabled by default.
  2. 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 if ufw or iptables on the remote host is blocking port 22.
  3. IP Allowlists: Check the remote host's /etc/ssh/sshd_config. If AllowUsers or Match Address directives are present, the Pi's IP address must be explicitly whitelisted.
Pro-Tip for Headless Debugging: If you are running this script as a 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.