When deploying a headless Raspberry Pi for IoT, robotics, or remote network management, you need reliable remote access. Exposing port 22 (SSH) directly to the public internet via router port forwarding is a critical security risk that guarantees your Pi will be brute-forced by botnets within hours. The direct answer for 95% of makers and field deployments is to use a WireGuard-based mesh VPN. Specifically, Tailscale is the default pick for secure, zero-config remote SSH and GPIO API access without opening a single firewall port on your router.

The Remote Access Raspberry Pi Decision Matrix

Before wiring any hardware, you must choose your remote access transport. Use this decision path to select the right tool for your deployment environment.

Method Security Profile Setup Time Public Web Access? Best Use Case
Direct SSH (Port 22 Forwarding) Poor (Requires fail2ban, key-only auth) 15 mins No (SSH only) Legacy local networks only
Cloudflare Tunnel Excellent (Zero Trust) 45 mins Yes (HTTP/HTTPS) Public-facing web dashboards
Tailscale (WireGuard Mesh) Excellent (E2E Encrypted, No open ports) 5 mins No (Private mesh only) SSH, GPIO APIs, SMB shares, VNC
Decision Path Termination: If you need to expose a public website to strangers, use Cloudflare Tunnel. If you need to SSH into your Pi from your phone, trigger GPIO pins remotely, or access Samba shares while traveling, choose Tailscale. It assigns a static 100.x.y.z IP to your Pi that is only reachable by your authenticated devices.

Hardware BOM and GPIO Pin Mapping

This build creates a remote hardware watchdog. If a remote router or 3D printer locks up, you can trigger a physical power-cycle via a relay over your Tailscale network. Safety Note: The relay specified here is for low-voltage DC (<24V) router reset lines. Do not use this exact module to switch 120V/240V mains without a properly rated contactor and enclosure.

Parts List

  • Board: Raspberry Pi 5 (8GB variant, RPI-5-8GB) - ~$80
  • OS: Raspberry Pi OS Bookworm (64-bit, Lite or Desktop)
  • Power: Official 27W USB-C PD Power Supply (PI-PS-27W)
  • Relay Module: HiLetgo 1-Channel 5V Relay Module with Optocoupler Isolation - ~$6
  • Indicator: Standard 5mm Green LED with 330Ω current-limiting resistor
  • Wiring: 22 AWG stranded hookup wire, female-to-female Dupont connectors

Pin Mapping Table (BCM Numbering)

Raspberry Pi OS Bookworm defaults to the lgpio pin factory, which strictly enforces BCM numbering. Do not use BOARD (physical) numbering in your code.

Physical Pin BCM GPIO Component Function Wire Color
2 5V Power Relay VCC 5V DC Supply Red
6 GND Relay GND Common Ground Black
11 BCM 17 Relay IN Trigger Router Reset (Active LOW) Blue
13 BCM 27 LED Anode (+) System Status Indicator Green
14 GND LED Cathode (-) LED Ground (via 330Ω resistor) Black

Headless Provisioning and Tailscale Configuration

Bookworm changed how headless setups work. You must configure SSH and WiFi before the first boot.

  1. Flash the OS: Open Raspberry Pi Imager. Select Raspberry Pi 5 and Raspberry Pi OS (64-bit) Lite.
  2. OS Customization: Click the gear icon (Edit Settings). Set your hostname (e.g., watchdog-pi), enable SSH (Use password authentication), and enter your 2.4GHz WiFi credentials. Crucial: If you skip enabling SSH here, the daemon will be masked on first boot.
  3. First Boot: Insert the microSD card, power on, and wait 90 seconds. Find the Pi's local IP via your router's DHCP table.
  4. SSH In and Update:
    ssh pi@192.168.1.x
    sudo apt update && sudo apt upgrade -y
  5. Install Tailscale: Run the official install script (source: Tailscale Linux Docs).
    curl -fsSL https://tailscale.com/install.sh | sh
    sudo tailscale up
  6. Authenticate: The terminal will output a URL. Open it on your phone/laptop to authorize the Pi to your Tailnet. Note the assigned 100.x.y.z IP address.

The Code: Remote GPIO Watchdog Server

This Python script creates a lightweight, local HTTP server. Because the Pi is on Tailscale, you can trigger the relay securely from anywhere by sending an HTTP GET request to http://100.x.y.z:8080/trigger. This code targets the Raspberry Pi 5 running Bookworm, utilizing the gpiozero library which natively hooks into the lgpio backend.

#!/usr/bin/env python3
"""
Remote GPIO Watchdog Server for Raspberry Pi 5 (Bookworm)
Requires: sudo apt install python3-gpiozero
"""

import sys
import time
import logging
from http.server import BaseHTTPRequestHandler, HTTPServer
from gpiozero import LED, OutputDevice
from signal import pause

# --- PIN DEFINITIONS (BCM) ---
RELAY_PIN = 17
LED_PIN = 27

# --- HARDWARE SETUP ---
# Relay modules are typically Active LOW. 
# active_high=False ensures the pin defaults to HIGH (relay OFF) on startup.
try:
    relay = OutputDevice(RELAY_PIN, active_high=False, initial_value=False)
    status_led = LED(LED_PIN, initial_value=False)
except Exception as e:
    logging.critical(f"GPIO Initialization failed: {e}")
    sys.exit(1)

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')

class WatchdogHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/trigger':
            self._cycle_relay()
            self._send_response(200, "Relay cycled for 5 seconds.")
        elif self.path == '/status':
            state = "ON" if status_led.is_lit else "OFF"
            self._send_response(200, f"System Status: {state}")
        elif self.path == '/ping':
            self._send_response(200, "pong")
        else:
            self._send_response(404, "Invalid endpoint. Use /trigger, /status, or /ping")

    def _cycle_relay(self):
        logging.info("Triggering hardware reset sequence...")
        status_led.on()
        relay.on()  # Drops the relay contact (cuts power to target)
        time.sleep(5) # Hold for 5 seconds
        relay.off()   # Restores power
        status_led.off()
        logging.info("Reset sequence complete.")

    def _send_response(self, code, message):
        self.send_response(code)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(message.encode('utf-8'))

    def log_message(self, format, *args):
        # Suppress default noisy HTTP logs, use our logger instead
        logging.info(f"{self.client_address[0]} - {format % args}")

def run_server(port=8080):
    server_address = ('', port)
    httpd = HTTPServer(server_address, WatchdogHandler)
    logging.info(f"Watchdog server listening on port {port}")
    status_led.blink(on_time=0.5, off_time=0.5, n=3) # Boot sequence blink
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        logging.info("Server stopped by user.")
    finally:
        httpd.server_close()
        relay.close()
        status_led.close()
        logging.info("GPIO resources cleaned up.")

if __name__ == '__main__':
    run_server()
Pro-Tip: Save this as watchdog.py and run it via a systemd service so it survives reboots. Do not use rc.local or crontab @reboot on Bookworm; systemd handles service restarts and logging via journalctl much more reliably.

Debugging: "Connection Refused" and Tailscale Dropouts

When remote access fails, you need a systematic triage path. Here are the exact error strings and their ranked causes.

Error 1: ssh: connect to host 100.118.x.y port 22: Connection refused

First three things to check:

  1. SSH Daemon Status: Bookworm disables SSH by default if not configured in the Imager. Connect a monitor/keyboard or plug in Ethernet to your local LAN. Run sudo systemctl status ssh. If it says inactive (dead) or masked, run sudo systemctl enable --now ssh.
  2. Tailscale IP Mismatch: Verify you are pinging the Tailscale IP (100.x.y.z), not the local LAN IP. Run tailscale ip -4 on the Pi to confirm.
  3. Firewall Rules: If you installed ufw, ensure port 22 is allowed. Run sudo ufw allow 22/tcp. Tailscale traffic usually bypasses local iptables via the tailscale0 interface, but strict UFW configs can still drop it.

Error 2: tailscale status shows node as offline or derp-only

This means the Pi cannot establish a direct WireGuard UDP connection and is falling back to Tailscale's DERP relay servers (which are slow and drop TCP streams like SSH).

  • Cause A (NAT Traversal): Your router's firewall is blocking outbound UDP. Ensure the Pi isn't behind a CGNAT (common on cellular/starlink). If it is, enable Tailscale's DERP routing or use a subnet router.
  • Cause B (WiFi Power Saving): The Pi 5's WiFi chip aggressively sleeps. Disable power management: sudo iwconfig wlan0 power off.

Extending and Simplifying the Build

Depending on your field requirements, you may need to strip this down to bare metal or scale it up for environmental monitoring.

How to Simplify

If you don't need the HTTP API and just want raw SSH control, delete the Python script entirely. Install gpiozero and use the command-line tools provided by the gpiozero CLI. You can trigger the pin directly from your remote SSH session:

ssh pi@100.x.y.z "gpiozero-cli write 17 0 && sleep 5 && gpiozero-cli write 17 1"
This eliminates the HTTP server overhead and reduces the attack surface to just the SSH daemon.

How to Extend

To turn this watchdog into a full remote environmental node, wire a BME280 sensor to the I2C pins (Physical Pin 3 for SDA, Pin 5 for SCL). Add the adafruit-circuitpython-bme280 library via pip. Modify the /status endpoint in the Python script to read the I2C bus and return JSON containing temperature, humidity, and barometric pressure. This allows you to monitor the enclosure's internal thermals before deciding to trigger the hardware reset relay.