Firepuncher
Every tunnel described elsewhere in these docs is inbound: the internet reaches a web app on your network. Firepuncher inverts that. Your laptop dials the relay, the relay pairs it with one of your agents, and a port on your laptop becomes a port inside your network.
That makes it work for things a browser can’t reach: SSH, Postgres, Redis, RDP, VNC — anything that speaks TCP.
hle fp --agent rpi --to 22 --port 9922Forwarding 127.0.0.1:9922 → rpi → localhost:22Agent allows: localhost:*Try: ssh -p 9922 <user>@127.0.0.1Ctrl+C to stop.Then, in another terminal:
ssh -p 9922 root@localhostWhich machine is which
This is the one genuinely confusing part, so it’s worth being blunt about it.
The word localhost appears on both sides of a firepuncher session and means a
different machine each time.
| Where | Resolved by | Example |
|---|---|---|
--to | the agent | --to 22 is the agent’s own sshd |
--to | the agent | --to 192.168.1.50:5432 is a box on the agent’s LAN |
--port / --bind | your laptop | --port 9922 opens 127.0.0.1:9922 locally |
ssh … root@localhost | your laptop | connects to the port you just opened |
Read --to as “as the agent would type it”. You are naming a target from
inside your network, from a terminal outside it.
Requirements
An enrolled, running agent on the far end — see Agent — and an
API key for yourself (hle auth login). The agent needs no firepuncher-specific
setup.
Targets
--to accepts a bare port or host:port:
--to | Means |
|---|---|
22 | port 22 on the agent’s own loopback |
localhost:22 | the same thing, written out |
192.168.1.50:5432 | port 5432 on that host, as resolved by the agent |
nas:8096 | a hostname the agent can resolve |
[::1]:22 | IPv6 literals must be bracketed |
The local port
--port is optional. Left out, the local port is the target port + 9000
(so 22 → 9022, 5432 → 14432), which keeps the mapping guessable and off
privileged ports.
--bind defaults to 127.0.0.1. Widening it exposes the forward to your whole
local network, which throws away much of the point — think before changing it.
The allowlist
An agent only forwards to targets it has been told to allow. The allowlist is per agent and stored on the server — there is nothing to configure on the agent’s machine. It is pushed down the agent’s control connection when it connects and again whenever you change it.
A fresh agent with no rules configured allows its own loopback on any port. That is why SSH-ing to the agent’s own machine needs no setup at all.
Manage rules in the dashboard under Connections → Firepuncher.
A rule is a host plus an optional port. Omitting the port allows any port on that host.
| Rule | Allows |
|---|---|
localhost:22 | only SSH on the agent itself |
localhost | any port on the agent itself |
192.168.1.50:5432 | only Postgres on that host |
192.168.1.50 | any port on that host |
localhost, 127.0.0.1, ::1, and ip6-localhost are treated as the same
target, so a rule for one can’t be side-stepped by spelling it another way.
When a target isn’t allowed, the connection is refused and the CLI tells you what is allowed:
Refused: 192.168.1.50:5432 is not allowed. Allowed: localhost:*hle fp also prints the agent’s allowlist in its start-up banner, so you can
see what you can reach before you try it.
Keeping a forward up
hle fp reconnects on its own. The local port is bound once and held for the
lifetime of the command, so a relay restart or a dropped network doesn’t take
the port away — connections in flight when the link drops are closed (the far
end is genuinely gone), but the listener stays ready.
For a forward that should always be there, install it as a service:
hle service install --fp --agent-name rpi --to 22 --port 9922The API key is never written into the service file; it is read at runtime from
~/.config/hle/config.toml or HLE_API_KEY.
Using it with SSH
Add the forwarded port to ~/.ssh/config and the indirection disappears:
Host rpi HostName localhost Port 9922 User rootssh rpiSecurity
- Nothing listens on the public internet. Both ends dial out to the relay: your laptop with an API key, the agent with its agent token. There is no inbound port on either side, and nothing to firewall.
- The agent decides what is reachable, not the client. A caller who asks for a target that isn’t allowed is refused by the agent itself.
- The local listener is loopback-only by default, so other machines on your network can’t use your forward.
- Traffic is carried over the same encrypted WebSocket transport as the rest of HLE. Firepuncher does not terminate or inspect the stream — SSH’s own end-to-end encryption is unaffected.
Flags
| Flag | Default | Meaning |
|---|---|---|
--agent | required | Agent name or id to forward through |
--to | required | Target as the agent sees it: HOST:PORT, or a bare port |
--port | target port + 9000 | Local port to listen on |
--bind | 127.0.0.1 | Local address to bind |
--api-key | env / config | API key, if not already saved |
--relay-host | hle.world | Relay host |
--relay-port | 443 | Relay port |
Troubleshooting
Missing option '--agent' — both --agent and --to are required.
Refused: … is not allowed — the target isn’t in that agent’s allowlist. Add
it in Connections → Firepuncher, and remember that your first rule replaces the
loopback default.
No API key — run hle auth login, or pass --api-key.
“Waiting for it to come back…” — the agent is offline. hle fp holds the
local port and reconnects by itself once the agent returns.
could not bind 127.0.0.1:9922 — something else already has that local
port. Pick another with --port.