Introduction
You have a server, client machine, Raspberry Pi or NAS at a client / at home behind a router you can't configure (locked-down ISP box, dynamic IP, carrier-grade NAT). How do you administer it remotely?
Classic solution: port forwarding on the router. Impossible when you don't have control.
Modern solution: reverse SSH tunnel. The target machine connects itself to a public VPS you control (the "bastion"). Once the reverse SSH session is established, you connect to the bastion and bounce to the target.
Architecture:
[You] ──SSH──▶ [Bastion VPS] ◀──Reverse SSH── [Target behind NAT]
(public IP) (10.0.0.42)
Prerequisites
- A public VPS with active SSH (= the bastion). A small VeryCloud VPS is enough.
- A target Linux machine with SSH client (Raspberry Pi, NAS, server behind NAT...)
- Root or sudo access on both
Step 1: Prepare the bastion
On the bastion VPS, create a dedicated tunnel user:
sudo adduser tunnel
sudo usermod -s /bin/false tunnel # No interactive shell (security)
Disable unnecessary functions in /etc/ssh/sshd_config:
Match User tunnel
PasswordAuthentication no
PermitTTY no
AllowAgentForwarding no
AllowTcpForwarding yes
X11Forwarding no
PermitOpen any
ForceCommand /bin/false
sudo systemctl reload sshd
Allow listening port opening (GatewayPorts):
sudo nano /etc/ssh/sshd_config
Add:
GatewayPorts clientspecified
sudo systemctl reload sshd
Step 2: Generate SSH key on target
On the target machine (behind NAT):
sudo ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_tunnel
Get the public key:
cat /root/.ssh/id_tunnel.pub
Step 3: Install the key on bastion
On the bastion, add the public key to tunnel user's authorized_keys:
sudo mkdir -p /home/tunnel/.ssh
sudo nano /home/tunnel/.ssh/authorized_keys
Paste the public key. Permissions:
sudo chown -R tunnel:tunnel /home/tunnel/.ssh
sudo chmod 700 /home/tunnel/.ssh
sudo chmod 600 /home/tunnel/.ssh/authorized_keys
Step 4: Test the tunnel manually
On the target:
sudo ssh -i /root/.ssh/id_tunnel \
-N -R 2222:localhost:22 \
tunnel@BASTION_IP
Decoded:
-N: no command execution-R 2222:localhost:22: opens port 2222 on bastion, redirected to port 22 (SSH) on target
From your workstation, connect to bastion then bounce:
ssh user@BASTION_IP
ssh -p 2222 target_user@localhost
Or directly in one command from your workstation (no intermediate step):
ssh -J tunnel@BASTION_IP -p 2222 target_user@localhost
If that works, you're connected to the target via the bastion.
Step 5: systemd service for persistent tunnel
Manual tunnel dies on reboot or any disconnect. We'll make it persistent.
On the target:
sudo nano /etc/systemd/system/reverse-tunnel.service
[Unit]
Description=Reverse SSH tunnel to bastion
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/bin/ssh \
-NT \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=accept-new \
-i /root/.ssh/id_tunnel \
-R 2222:localhost:22 \
tunnel@BASTION_IP
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable:
sudo systemctl daemon-reload
sudo systemctl enable --now reverse-tunnel
sudo systemctl status reverse-tunnel
Check:
sudo journalctl -u reverse-tunnel -f
On bastion:
ss -tlnp | grep 2222
# Must show listening on 127.0.0.1:2222
Step 6: With autossh (more robust than native ssh)
autossh detects disconnects and reconnects faster. Install on target:
sudo apt install -y autossh
Edit the service:
sudo nano /etc/systemd/system/reverse-tunnel.service
[Service]
ExecStart=/usr/bin/autossh \
-M 0 -NT \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-i /root/.ssh/id_tunnel \
-R 2222:localhost:22 \
tunnel@BASTION_IP
sudo systemctl daemon-reload
sudo systemctl restart reverse-tunnel
Step 7: Multiple tunnels for multiple targets
If you have 5 Raspberry Pis at 5 clients, assign a distinct port on bastion:
| Client | Bastion port | Target |
|---|---|---|
| ClientA | 2201 | rpi-a:22 |
| ClientB | 2202 | rpi-b:22 |
| ClientC | 2203 | rpi-c:22 |
On each target, adapt the port in -R 22XX:localhost:22.
To connect:
ssh -J tunnel@BASTION_IP -p 2202 pi@localhost
Step 8: Expose ports other than SSH
The reverse tunnel isn't limited to SSH. You can expose a local web service:
# On target
ssh -R 8080:localhost:80 tunnel@BASTION_IP
On bastion, port 8080 redirects to target's web server (port 80).
To make it publicly accessible (not just from bastion), use 0.0.0.0:
ssh -R 0.0.0.0:8080:localhost:80 tunnel@BASTION_IP
⚠️ Requires GatewayPorts clientspecified or yes on bastion side (done in step 1).
Step 9: Multi-port reverse tunnel
To expose multiple services in a single tunnel:
ssh -N \
-R 2222:localhost:22 \
-R 8080:localhost:80 \
-R 9090:localhost:9090 \
tunnel@BASTION_IP
Step 10: Nginx as front reverse-proxy
Instead of exposing a random port 8080, route the tunnel through Nginx with HTTPS and subdomain. On bastion:
server {
listen 443 ssl http2;
server_name rpi-clienta.verycloud.fr;
ssl_certificate /etc/letsencrypt/live/rpi-clienta.verycloud.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rpi-clienta.verycloud.fr/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080; # tunnel port
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
You now access client A's Raspberry Pi via https://rpi-clienta.verycloud.fr.
Step 11: Additional security
Restrict exposable ports
In authorized_keys of tunnel user, prefix the key with options:
restrict,port-forwarding,permitlisten="2222",no-pty ssh-ed25519 AAAAC3Nz... mathys@laptop
This client can only create tunnels on port 2222 of bastion. Useful for per-client limiting.
Firewall on bastion
Block external access to tunnel ports:
sudo ufw deny 2222:2299/tcp
So to connect, you MUST first go through SSH on bastion (port 22), no direct access from internet.
Tunnel monitoring
On target:
# Status
sudo systemctl status reverse-tunnel
# Live logs
sudo journalctl -u reverse-tunnel -f
On bastion, check active connections:
ss -tnp | grep tunnel
Step 12: Modern alternative — Tailscale
For the same problem in SaaS / zero-config mode, see the Tailscale Mesh VPN guide. Less manual config but dependency on third-party service.
Reverse SSH is:
- ✅ 100% self-hosted
- ✅ Minimalist (just SSH)
- ❌ More manual
- ❌ Requires managing ports/Nginx
Tailscale is:
- ✅ Zero-config (auth key and it works)
- ✅ Mesh (each machine sees each machine)
- ❌ Third-party dependency (coordination server)
- ❌ More software layers
Troubleshooting
Tunnel doesn't hold
Enable keepalives (already in systemd service). Also check NAT/firewall on target side which may cut idle connections. ServerAliveInterval=30 sends a packet every 30s.
"Could not request local forwarding"
Port already used on bastion or GatewayPorts misconfigured. Change port or check sshd_config.
"Connection refused" from bastion
Tunnel is not active. On target:
sudo systemctl status reverse-tunnel
sudo journalctl -u reverse-tunnel -n 30
"Host key verification failed"
First connection to bastion: SSH asks to validate fingerprint. With StrictHostKeyChecking=accept-new in the service, it's auto on first pass. If it fails, delete the bastion line in /root/.ssh/known_hosts.
Degraded performance
SSH encrypts everything. For big transfers, use fast ciphers:
ssh -c [email protected] ...
Useful commands
# See all active tunnels on bastion
ss -tlnp | grep sshd
# tunnel user connections
who | grep tunnel
# Test bastion connectivity from target
sudo -u root ssh -i /root/.ssh/id_tunnel tunnel@BASTION_IP exit
echo $? # 0 = OK
# Force reconnect
sudo systemctl restart reverse-tunnel
# List exposed ports from bastion
ss -tlnp | grep '127.0.0.1\|0.0.0.0'
# Kill a zombie tunnel (on bastion)
sudo pkill -u tunnel
Conclusion
The reverse SSH tunnel is the minimalist tool to reach any machine behind NAT:
- No port forwarding on client/box side
- 100% encrypted (SSH)
- Persistent via systemd + autossh
- Multi-tunnel possible with a single bastion
Going further:
- Combine with Nginx + Let's Encrypt to expose HTTPS
- Use Cloudflare Tunnel as SaaS alternative
- Migrate to Tailscale or WireGuard for mesh VPN
Resources
- OpenSSH documentation: https://www.openssh.com/manual.html
- autossh: https://www.harding.motd.ca/autossh/
- VeryCloud guide — WireGuard VPN:
/docs/article/wireguard - VeryCloud guide — Tailscale Mesh:
/docs/article/tailscale - VeryCloud guide — SSH hardening:
/docs/article/ssh-hardening


















