Your Raspberry Pi sits on your desk at home, running everything.
From the sofa, ssh pi@raspberrypi.local just works.
From a café, from the office, from a hotel in another country — nothing.
Let's fix that without opening port 22 to the internet.
Nothing listens on your router. The connection dials outward from both ends.
SSH still does its own auth. HLE moves the bytes, it doesn't terminate your session.
Finish with ssh rpi from any machine you've set up.
The obvious fix is to forward port 22 on your router and be done with it. Then you watch
/var/log/auth.log for a day and find thousands of login attempts from hosts you've never
heard of. Automated scanners find an open 22 within minutes.
You can harden it — keys only, no root, fail2ban, move to a high port. People run that setup for years without incident. But it's a permanent front door on the public internet, and you're the one responsible for it.
The other route is a VPN back home. That works well, and if you already run WireGuard you may not need anything here. It's also a whole piece of infrastructure to stand up and keep running for what started as "I want to check on the Pi."
This guide takes a third path: a private, authenticated bridge between two machines you own, with no listening port on either side.
An agent runs on the Pi and holds an outbound connection to the relay.
On your laptop, hle fp — firepuncher — opens its own outbound connection
and asks the relay to pair the two.
You get a local port on your laptop. Anything you send to it comes out of the agent on the Pi,
aimed at localhost:22. Neither machine accepts an inbound connection, and there is no
public TCP port anywhere.
SSH doesn't know any of this is happening. It sees a TCP socket, does its usual key exchange, and your host keys and authentication work exactly as they always have.
hle --version.
sshd_config needs to change.
In your dashboard, go to Agents → New Agent
and name it rpi. You'll get a token starting with hlea_.
Copy it now — it's shown once and never again. If you lose it, delete the agent and make another.
SSH in one last time the old-fashioned way, then run:
curl -fsSL https://get.hle.world | sh -s -- --agent
That installs the client, prompts for the token you just copied, and sets up a service that starts at boot. Run it as root and you get a system service; run it as your normal user and you get a per-user one.
If HLE is already installed, upgrade and enroll instead:
hle update
hle agent enroll # paste the hlea_ token
hle service install --agent
Check it took:
hle service status --agent
The agent should now show as online in your dashboard, with its version and last-seen time.
localhost means the container,
not the Pi — and the container has no SSH server. Use --network host, or point
the forward at the Pi's LAN IP and allow that target. The installer above avoids this entirely.
On the machine you want to connect from:
hle update # 2607.5 or newer
hle auth login # firepuncher uses your API key
hle fp --agent rpi --to 22 --port 9922
You'll see something like:
Forwarding 127.0.0.1:9922 → rpi → localhost:22
Agent allows: localhost:*
Try: ssh -p 9922 <user>@127.0.0.1
That second line is worth reading. The agent only dials targets on its allowlist, and it tells you
what that list is up front rather than failing later. Out of the box it's loopback only — which
is exactly what localhost:22 is, so SSH works with no configuration.
Leave that running and, in another terminal:
ssh -p 9922 pi@127.0.0.1
You're in. Same keys, same prompt, same everything — from anywhere with an internet connection.
ssh rpiTyping a port number and remembering to start a forward first gets old immediately. Two changes fix it for good.
First, make the forward permanent. Instead of running hle fp by hand,
install it as a service on your laptop:
hle service install --fp --agent-name rpi --to 22 --port 9922
Now 127.0.0.1:9922 is simply always there — through reboots, through Wi-Fi changes,
through the relay blipping. It restarts itself.
Second, teach SSH about it. Add this to ~/.ssh/config:
Host rpi
HostName 127.0.0.1
Port 9922
User pi
# The forward always lands on loopback, so the host key
# belongs to the port, not to "127.0.0.1" generally.
HostKeyAlias rpi.hle
ServerAliveInterval 30
ServerAliveCountMax 3
That's the whole trick:
ssh rpi
And because it's ordinary SSH config, everything built on SSH follows along:
scp backup.tar.gz rpi:/mnt/storage/
rsync -avz ~/photos/ rpi:/mnt/storage/photos/
sftp rpi
code --remote ssh-remote+rpi /home/pi
VS Code Remote, Ansible inventories, git remotes over SSH, your shell's tab completion —
they all just see a host called rpi.
HostKeyAlias? Without it, SSH files the Pi's host key under
[127.0.0.1]:9922. Point a second forward at a different machine on the same port later
and you'll get a host key mismatch warning that looks alarming and isn't. The alias pins the key to
the machine instead of the port.
Each forward is its own service and its own local port, so several machines coexist without stepping on each other:
hle service install --fp --agent-name rpi --to 22 --port 9922
hle service install --fp --agent-name nas --to 22 --port 9923
hle service install --fp --agent-name devbox --to 22 --port 9924
Then one block per host in ~/.ssh/config:
Host rpi
HostName 127.0.0.1
Port 9922
User pi
HostKeyAlias rpi.hle
Host nas
HostName 127.0.0.1
Port 9923
User admin
HostKeyAlias nas.hle
Host devbox
HostName 127.0.0.1
Port 9924
User ioannis
HostKeyAlias devbox.hle
Shared settings can go in a Host rpi nas devbox block above them, or a
Match block, in the normal way.
Nothing here is SSH-specific. Firepuncher forwards TCP, so the same two commands reach anything the agent can talk to:
# Postgres on the NAS, as if it were local
hle fp --agent nas --to 5432 --port 15432
psql -h 127.0.0.1 -p 15432 -U postgres
# A Redis instance
hle fp --agent devbox --to 6379 --port 16379
# The Pi's own web UI, without a public URL
hle fp --agent rpi --to 8080 --port 18080
By default the agent only dials loopback — itself. To reach something else on its network,
such as a NAS at 192.168.1.50, add that target to the agent's allowlist in the dashboard
first. The agent refuses anything it wasn't told to allow, so a forward can't quietly become a
tunnel into your whole LAN.
The forward starts but SSH hangs. Usually an old client on the Pi. Firepuncher
needs 2607.5 on both ends; an older agent connects, reports healthy, and ignores the
forward. hle update on the Pi.
"Agent is not connected." The agent isn't running or can't reach the relay.
Check hle service status --agent on the Pi, and
journalctl -u hle-agent -f for why.
"localhost:22 is not allowed." The agent has an allowlist that no longer includes
loopback — adding your first explicit rule replaces the default. Add localhost
back in the dashboard.
Connection refused, through the tunnel. The agent reached the target and got
nothing. Check sshd is actually listening on the Pi:
ss -tlnp | grep :22.
The service dies when you log out (Linux laptops). A per-user systemd service
stops at logout unless lingering is on: sudo loginctl enable-linger $USER.
Pick the box you most often wish you could reach. Ten minutes, two commands, and one block in your SSH config.