pfSense
pfSense is a FreeBSD-based firewall and router. The HLE agent runs directly on it, so you can reach the pfSense web UI — and any service behind the firewall — from the internet without a port forward, a VPN, or a public IP.
The agent runs as an rc.d service, so it starts at boot and restarts if it exits. Tunnels
are added and removed from the dashboard under Connections;
once installed, the firewall needs no further attention.
Install
SSH into pfSense as root, or use Diagnostics → Command Prompt.
1. Check Python is there:
python3 --versionAnything 3.11 or newer works. You almost certainly already have it: pfSense 2.7 carries
Python 3.11 as a dependency of unbound, Suricata and pfBlockerNG, and OPNsense ships 3.13.
It comes with the system rather than being something you add, so if that prints a version,
skip to step 2.
Only if it’s genuinely missing:
pkg install python3Don’t ask for a specific version — python311 exists on pfSense but not on OPNsense, which
has python313 instead. If python3 isn’t available either, find what is:
pkg search '^python3'2. Install the agent:
fetch -o - https://get.hle.world | sh -s -- --agentNote fetch rather than curl — it’s the FreeBSD equivalent and is already present.
It prompts for an agent token, enrolls the firewall, and installs the service. Get a token from Connections → New → Agent in the dashboard; it’s shown only once.
3. Confirm it’s running:
hle service status --agentPrefer this over service hle_agent status, which only reports that a process exists. If the
agent is running but has no token — so it starts and exits, over and over — this says so.
The agent should appear as online under Connections → Agents within a few seconds.
Unattended install
To skip the prompt — useful from Ansible or a config-management tool:
fetch -o - https://get.hle.world | sh -s -- --agent --token hlea_xxxxxExpose the pfSense web UI
One command. It publishes the firewall’s web UI behind SSO and tells pfSense to accept the new hostname, which are otherwise two separate jobs in two different places:
fetch -o /tmp/hle-pfsense.sh https://hle.world/scripts/pfsense.shsh /tmp/hle-pfsense.sh exposeIt prompts for an API key — create one at hle.world/dashboard/keys — then prints the URL your firewall is now reachable at.
Pass a label if you want something other than firewall, and the key as an environment
variable if you’d rather not be prompted:
HLE_API_KEY=hle_xxxxx sh /tmp/hle-pfsense.sh expose myfirewallWith more than one agent on your account, name the one to use:
HLE_AGENT="pfsense" sh /tmp/hle-pfsense.sh exposeWhat it did, and why the second half matters
It created a tunnel pointing at https://127.0.0.1 with SSO auth, then added the tunnel
hostname to pfSense’s Alternate Hostnames.
Without that second step the first page load fails, because pfSense validates the Host
header and the Referer against its own hostname:
An HTTP_REFERER was detected other than what is defined in System > Advanced
The script writes the same setting the GUI does, through write_config(), so it records a
config revision and stays revertable from Diagnostics → Backup & Restore → Config History.
Running it twice is safe — the second run reports nothing changed.
Prefer this over disabling the referer check, which is real CSRF protection for the admin UI.
Or do both steps yourself
Add a tunnel in the dashboard against your agent — Connections → New → Tunnel, then Through an agent:
| Field | Value |
|---|---|
| Name | firewall |
| Local service | https://127.0.0.1 |
| Who can reach it | Behind your login (SSO) |
Then allow the hostname it gives you:
sh /tmp/hle-pfsense.sh allow-hostname firewall-x7k.hle.worldOr do it in the GUI
System → Advanced → Admin Access → Alternate Hostnames, add the hostname only — no scheme, no trailing slash:
firewall-x7k.hle.worldpfSense’s self-signed certificate is not a problem: HLE terminates TLS at the relay and does
not verify the upstream certificate unless you ask it to. Use https:// and move on.
Anything else behind the firewall works the same way — point a tunnel at
http://192.168.1.50:8096 and your Jellyfin is reachable, with no NAT rule and no route.
Managing the service
service hle_agent status # is it running?service hle_agent restart # restart itservice hle_agent stop # stop ittail -f /var/log/hle_agent.log # what is it doing?The service is defined at /usr/local/etc/rc.d/hle_agent and enabled with
sysrc hle_agent_enable=YES. Both are generated by hle service install, so there should be
no reason to edit them. daemon(8) handles backgrounding and restarts the agent if it exits.
To remove it:
hle service uninstall --agentSurviving firmware upgrades
The agent installs as a Python virtual environment under /root, not as a package, so a
firmware upgrade doesn’t delete it. Two things can still go wrong, and both end the same way —
no remote access to your firewall, at exactly the wrong moment.
The venv is linked to whichever interpreter it was built against — Python 3.11 on pfSense 2.7,
3.13 on OPNsense. pfSense 2.8.0 moves to a FreeBSD 15 base, and if the Python minor version
changes, that link breaks. hle is still sitting there and still marked executable; it just
can’t run any more.
Separately, the service that starts the agent lives in /usr/local/etc/rc.d, which is not
/root and has no such protection. Lose it and the agent is intact but nothing launches it
at boot.
(Python itself is safe: it arrives as a dependency of unbound, Suricata and pfBlockerNG
from pfSense’s own repository, so it can’t disappear without taking the DNS Resolver with it.
You don’t need to install it.)
Run this once and it handles itself:
fetch -o /tmp/hle-pfsense.sh https://hle.world/scripts/pfsense.shsh /tmp/hle-pfsense.sh persist/conf is the one directory pfSense preserves across upgrades, so that’s where this puts a
copy of your agent token and a small bootstrap script. It then registers two hooks in
config.xml, which is also preserved: a boot command, and a job that runs every ten minutes.
Two, because losing access to your firewall is not a failure worth being clever about. The boot command is the fast path, but it runs early, before the network is necessarily up. The ten-minute job doesn’t care: it notices whenever the agent stops working and repairs it. So the worst case after an upgrade is a ten-minute wait, not a locked door.
The check is that the agent actually runs, not that its file exists — the broken-link case
above leaves an hle that looks perfectly healthy to anything that only tests for a file. If
the venv can’t start, the bootstrap rebuilds it; if only the service script is missing, it
reinstalls just that.
persist then runs the bootstrap script immediately and reads config.xml back to confirm
both hooks are really there. If either didn’t take, it says so and fails, rather than leaving
you believing you’re covered.
Your token is restored too, so the agent reconnects as the same agent with your tunnels intact. Nothing in the dashboard needs changing, and you never need a new token.
Check it’s in place — this also shows the last line of the bootstrap log:
sh /tmp/hle-pfsense.sh statusOr repair it by hand
rm -rf ~/.local/share/hle/venvfetch -o - https://get.hle.world | sh -s -- --agentRemoving the venv first is the point: reinstalling over a venv whose interpreter has moved leaves the broken one in place. The installer rebuilds it against whatever Python is present.
The token survives in /conf only if you ran persist; otherwise you’ll be prompted for a
new one.
Package upgrades within the same firmware version (pkg upgrade) don’t disturb anything.
Prefer not to install on the firewall?
You don’t have to. Run the agent on any other machine on the LAN — a NAS, a Pi, an LXC
container — and add pfSense as a tunnel pointing at its LAN address
(https://192.168.1.1). The agent reaches it exactly as your browser does.
That’s immune to firmware upgrades and keeps the firewall untouched, at the cost of depending on another always-on machine. Both setups are supported; pick whichever fits.
Troubleshooting
The service is “running” but nothing works. Check the log first —
tail /var/log/hle_agent.log. If it repeats No agent token. Run hle agent enroll first, the
service is restarting every few seconds and service hle_agent status will still show a
healthy pid, because it reports a process rather than a working agent. Enrol and restart:
hle agent enroll <token-from-the-dashboard>service hle_agent restartOn hle-client older than 2608.3 this happened on its own: the generated rc.d script set
no HOME, so the agent looked for the token somewhere other than where enrolment wrote it.
Upgrade, then regenerate the service:
fetch -o - https://get.hle.world | sh -s -- --agent --no-servicehle service install --agenthle: Command not found. after installing. Since 2608.3 the installer adds
~/.local/bin to your shell’s startup file automatically. tcsh caches what’s on the path,
so it keeps reporting the command as missing until told to look again:
rehashThe full path always works regardless: ~/.local/bin/hle service status --agent.
“An HTTP_REFERER was detected other than what is defined in System > Advanced”, or a redirect loop, or a complaint about the hostname. All the same root cause — the hostname step above was skipped, or a second tunnel was added later and needs its own entry:
fetch -o /tmp/hle-pfsense.sh https://hle.world/scripts/pfsense.shsh /tmp/hle-pfsense.sh allow-hostname <the-hostname-in-the-error>(/tmp is cleared on reboot, so fetch it again if the file is gone.)
A command hangs on a ? prompt. You’re in tcsh, pfSense’s default root shell, and it’s
waiting for input that will never come. Press Ctrl-C, then run sh before retrying — every
command on this page assumes sh.
The tunnel returns 502. The agent can’t reach the service. For the firewall’s own UI
that should be https://127.0.0.1; for anything else use the LAN address. Confirm from the
firewall with curl -sk https://127.0.0.1 -o /dev/null -w '%{http_code}\n'.
Everything looks right but nothing connects. pfSense’s outbound rules must let the agent
reach hle.world on 443. That’s usually the default, but a locked-down WAN policy can block
it.
See the agent guide for how tunnels, tokens, and reconnection work, and troubleshooting for anything not covered here.