The Direct Answer: Secure Remote Access Without Port Forwarding
To establish a secure remote connection to a Raspberry Pi deployed in the field without exposing ports to the public internet, use a Tailscale mesh network combined with a local Mosquitto MQTT broker. This configuration gives you zero-config SSH access from anywhere, encrypted sensor telemetry, and complete protection against port-scanning bots. You avoid the latency and security pitfalls of traditional port forwarding or reverse SSH tunnels.
Difficulty: Intermediate (Requires basic Linux CLI and Arduino IDE experience)
Time to Complete: 90 minutes
Estimated Cost: $95 - $115 USD (excluding power supply and enclosure)
Target Board Variant: Raspberry Pi 5 (8GB) running Raspberry Pi OS (64-bit, Bookworm) & ESP32-WROOM-32 DevKit V1
Remote Connection Methods Compared
Before wiring the hardware, it is critical to choose the right transport layer. When engineers ask how to optimize a remote connection to a Raspberry Pi, they are usually weighing NAT traversal against security. Here is how the top methods stack up for headless IoT deployments in 2026.
| Method | Security Posture | Monthly Cost | Latency Overhead | NAT Traversal |
|---|---|---|---|---|
| Port Forwarding | Poor (Exposes ports to public internet) | $0 | None (Direct) | Manual (Requires router access) |
| Ngrok (Free Tier) | Good (TLS encrypted tunnel) | $0 | High (Routes via regional relay) | Automatic |
| Cloudflare Tunnels | Excellent (Zero Trust) | $0 - $5 | Medium (Edge routing) | Automatic |
| Tailscale (Mesh) | Excellent (WireGuard P2P encrypted) | $0 (up to 100 nodes) | Low (Direct P2P or DERP fallback) | Automatic (Hole punching) |
Verdict: Tailscale wins for embedded projects because it creates a virtual LAN. Your ESP32 nodes can talk directly to the Pi's virtual IP without needing complex DNS or public certificates.
Hardware Bill of Materials & Pin Mapping
This build uses a Raspberry Pi 5 as the central broker and an ESP32 as a remote environmental sensor node. We are using I2C for the sensor to keep the wiring simple and robust over short distances inside an enclosure.
Parts List
- Gateway: Raspberry Pi 5 (8GB RAM) with Active Cooler and 27W USB-C PD Power Supply
- Storage: 32GB SanDisk High Endurance microSD card (rated for continuous logging)
- Sensor Node: ESP32-WROOM-32 DevKit V1 (30-pin variant)
- Sensor: Adafruit BME280 I2C/SPI Temperature, Humidity, and Pressure breakout
- Wiring: 26 AWG silicone stranded wire, 4-pin JST-SH connector
ESP32 to BME280 Pin Mapping
| ESP32-WROOM-32 Pin | BME280 Breakout Pin | Function | Notes |
|---|---|---|---|
| GPIO 21 | SDI (SDA) | I2C Data | Default I2C SDA on ESP32 Arduino core |
| GPIO 22 | SCK (SCL) | I2C Clock | Default I2C SCL on ESP32 Arduino core |
| 3V3 | VIN | Power (3.3V) | Do NOT use 5V; BME280 is strictly 3.3V logic |
| GND | GND | Common Ground | Ensure shared ground with Pi if powered via Pi USB |
Step-by-Step: Pi 5 Broker & Tailscale Configuration
Boot your Raspberry Pi 5 headless. Ensure SSH is enabled via the Raspberry Pi Imager settings before writing the OS to the SD card.
- Install Tailscale: SSH into your Pi on your local network and run the official install script:
curl -fsSL https://tailscale.com/install.sh | sh
Follow the prompt to authenticate via the provided URL. Once connected, note the Pi's Tailscale IP (usually starts with100.x.y.z). You can now unplug the Pi from your local router, connect it to a cellular LTE router or remote site, and you will still be able to SSH into it using this 100.x IP. - Install Mosquitto MQTT Broker:
sudo apt update && sudo apt install mosquitto mosquitto-clients -y - Configure Mosquitto to Bind to Tailscale: By default, Mosquitto on Bookworm only listens on localhost. We need it to listen on the Tailscale interface so remote ESP32 nodes (which are also on your Tailscale network) can connect. Edit the config file:
sudo nano /etc/mosquitto/mosquitto.conf
Add the following lines at the bottom:
Replacelistener 1883 100.x.y.z allow_anonymous true100.x.y.zwith your Pi's actual Tailscale IP. For production, replaceallow_anonymous truewith password file authentication, but we use anonymous here for initial bench testing. - Restart and Verify:
sudo systemctl restart mosquitto
Verify it is listening on the correct interface:
sudo ss -tulpn | grep 1883
ESP32 Sensor Node Firmware (Complete Code)
This C++ code targets the ESP32 DevKit V1 board in the Arduino IDE. It connects to WiFi, establishes a tunnel to the Tailscale network (assuming your router is bridged to Tailscale, or the ESP32 is connecting to a local network that has a Tailscale subnet router), and publishes BME280 data to the Pi 5.
Required Libraries: Install PubSubClient by Nick O'Leary and Adafruit BME280 Library via the Arduino Library Manager.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
// --- PIN DEFINITIONS ---
#define I2C_SDA 21
#define I2C_SCL 22
// --- NETWORK & MQTT CONFIG ---
const char* ssid = "YourNetworkSSID";
const char* password = "YourNetworkPassword";
// Use the Pi's Tailscale IP here
const char* mqtt_server = "100.105.42.12";
const int mqtt_port = 1883;
WiFiClient espClient;
PubSubClient client(espClient);
Adafruit_BME280 bme;
unsigned long lastMsg = 0;
const long interval = 10000; // Publish every 10 seconds
void setup_wifi() {
delay(10);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
delay(500);
attempts++;
}
if (WiFi.status() != WL_CONNECTED) {
ESP.restart(); // Hard reset if WiFi fails after 20s
}
}
void reconnect() {
int retries = 0;
while (!client.connected() && retries < 5) {
String clientId = "ESP32-Sensor-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
// Connection successful
} else {
retries++;
delay(5000); // Wait 5s before retrying
}
}
}
void setup() {
Serial.begin(115200);
// Initialize I2C with explicit pins
Wire.begin(I2C_SDA, I2C_SCL);
if (!bme.begin(0x77)) { // Default BME280 I2C address is 0x77 or 0x76
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1); // Halt execution
}
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
unsigned long now = millis();
if (now - lastMsg > interval) {
lastMsg = now;
float temp = bme.readTemperature();
float hum = bme.readHumidity();
char tempStr[8];
char humStr[8];
dtostrf(temp, 1, 2, tempStr);
dtostrf(hum, 1, 2, humStr);
client.publish("pi5/sensors/temperature", tempStr);
client.publish("pi5/sensors/humidity", humStr);
Serial.printf("Published: Temp=%sC, Hum=%s%%\n", tempStr, humStr);
}
}
Debugging: Exact Error Strings & Ranked Causes
When building remote IoT gateways, network layers fail silently. Here is how to diagnose the most common failure modes based on exact terminal and serial monitor outputs.
The First Three Things to Check When It Fails
- Verify Tailscale Node Status: Run
tailscale statuson the Pi. If the node shows as logged out or offline, the daemon crashed or the auth key expired. Runsudo systemctl restart tailscaled. - Verify Mosquitto Binding: Run
sudo ss -tulpn | grep 1883. If it shows127.0.0.1:1883, your config file edit failed and it's only listening locally. It must show your100.x.y.zIP. - Check ESP32 RSSI and State: Look at the ESP32 serial monitor. If WiFi connects but MQTT fails, check the
client.state()return code to isolate the exact rejection reason.
Ranked Error Strings and Fixes
ssh: connect to host 100.105.42.12 port 22: Connection timed outCause: The Pi is powered on, but the Tailscale daemon is not running, or the remote device you are SSHing from is not logged into the same Tailscale account.
Fix: Ensure your laptop/phone is connected to Tailscale. If the Pi is accessible via local LAN, SSH in using the local IP (e.g., 192.168.1.x) and run
sudo tailscale up.
Error connecting to MQTT broker, rc=-2 (in ESP32 Serial Monitor)Cause:
rc=-2 means "Network Unreachable" in the PubSubClient library. The ESP32 cannot route traffic to the 100.x Tailscale subnet.Fix: Your local WiFi router does not know how to route Tailscale IPs. You must either install Tailscale on the router itself (if supported), run a Tailscale subnet router on a local PC, or have the ESP32 publish to a local IP while the Pi acts as a bridge.
Connection refused (when testing via mosquitto_pub on another machine)Cause: The TCP handshake reached the Pi, but Mosquitto rejected it. This happens if
allow_anonymous false is set without providing credentials, or if the listener IP in mosquitto.conf has a typo.Fix: Check the Mosquitto log:
sudo journalctl -u mosquitto -f. It will explicitly state "Connection Refused from [IP] due to anonymous access disabled".
Extending and Simplifying the Build
Once your baseline remote connection to the Raspberry Pi is stable, you will inevitably need to adapt the architecture for production or cost-reduction.
How to Extend (Production Hardening)
- Enable MQTT over TLS (Port 8883): Generate Let's Encrypt certificates on the Pi using Certbot. Update Mosquitto to listen on 8883 with
certfileandkeyfiledirectives. This prevents packet sniffing on the local WiFi segment before it hits the encrypted Tailscale tunnel. - Add a Grafana Dashboard: Install InfluxDB and Grafana via Docker on the Pi 5. Use Telegraf to subscribe to the MQTT topics and write the time-series data to InfluxDB. You can then access the Grafana dashboard securely via the Pi's Tailscale IP on port 3000.
- Implement Watchdog Timers: Use the ESP32's hardware watchdog (
esp_task_wdt_init) to force a reboot if the WiFi stack hangs, a common issue in high-interference industrial environments.
How to Simplify (Prototyping & Cost Reduction)
- Drop MQTT for HTTP POST: If you only have one sensor and don't need real-time bidirectional communication, strip out the PubSubClient library. Use the ESP32's native
HTTPClient.hto send a JSON payload via a POST request to a lightweight Python Flask or FastAPI script running on the Pi. - Use Raspberry Pi Pico W: If you don't need the processing power of the Pi 5 for local analytics, replace the entire setup with a single Raspberry Pi Pico W running MicroPython, connecting directly to a cloud MQTT broker like HiveMQ, bypassing the need for a local gateway entirely.
By leveraging a mesh VPN for your remote connection to a Raspberry Pi, you eliminate the largest attack vector in DIY IoT: the open router port. Keep your Tailscale auth keys secure, monitor your Mosquitto logs, and your off-grid sensor network will run indefinitely.






