Guide · ~10 min · SSH

SSH into your home lab
from anywhere

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.

🖥

No open ports

Nothing listens on your router. The connection dials outward from both ends.

🔑

Your keys, unchanged

SSH still does its own auth. HLE moves the bytes, it doesn't terminate your session.

One-word command

Finish with ssh rpi from any machine you've set up.

Port 22 is the wrong thing to forward

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.

How it works

Your laptop
127.0.0.1:9922
HLE Relay
Encrypted bridge
The Pi
localhost:22

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.

What you need

  1. A free HLE account Sign up here, then enable Agents in your dashboard settings.
  2. hle-client 2607.5 or newer, on both machines Firepuncher landed in 2607.5. An older agent connects and looks healthy but ignores forward requests, so your laptop just waits. Check with hle --version.
  3. An SSH server on the Pi Whatever you already use. Nothing about your sshd_config needs to change.

Create an agent

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.

An agent token is not an API key. They're separate credentials for separate jobs: the agent token enrolls a machine, your API key authenticates you. They live in different files and don't interfere with each other.

Install the agent on the Pi

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.

Running the agent in Docker? Then 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.

Open the forward from your laptop

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.

Make it ssh rpi

Typing 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.

Why 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.

More than one machine

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.

Any TCP port, same idea

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.

Troubleshooting

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.

Try it on one machine

Pick the box you most often wish you could reach. Ten minutes, two commands, and one block in your SSH config.