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.
The forward log
Every forward is recorded when it closes: what it reached, when it opened, how long it lasted, how many bytes went each way, and how it ended — a clean close, the client going away, or the agent dropping. It is on the Firepuncher page under the allowlist, one log per agent.
Only the payload is counted, and only its size. Firepuncher does not inspect
the stream, so the log can say how much went to 10.0.0.9:22 and never what.
A forward still running is not in the log yet — it is in the rules table above, which shows live sessions. The log is kept for the same window as a tunnel’s access log, and is bounded by the same retention setting.
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.