Skip to content
Login

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:

Terminal window
python3 --version

Anything 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:

Terminal window
pkg install python3

Don’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:

Terminal window
pkg search '^python3'

2. Install the agent:

Terminal window
fetch -o - https://get.hle.world | sh -s -- --agent

Note 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:

Terminal window
hle service status --agent

Prefer 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:

Terminal window
fetch -o - https://get.hle.world | sh -s -- --agent --token hlea_xxxxx

Expose 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:

Terminal window
fetch -o /tmp/hle-pfsense.sh https://hle.world/scripts/pfsense.sh
sh /tmp/hle-pfsense.sh expose

It 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:

Terminal window
HLE_API_KEY=hle_xxxxx sh /tmp/hle-pfsense.sh expose myfirewall

With more than one agent on your account, name the one to use:

Terminal window
HLE_AGENT="pfsense" sh /tmp/hle-pfsense.sh expose

What 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:

FieldValue
Namefirewall
Local servicehttps://127.0.0.1
Who can reach itBehind your login (SSO)

Then allow the hostname it gives you:

Terminal window
sh /tmp/hle-pfsense.sh allow-hostname firewall-x7k.hle.world
Or do it in the GUI

System → Advanced → Admin Access → Alternate Hostnames, add the hostname only — no scheme, no trailing slash:

firewall-x7k.hle.world

pfSense’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

Terminal window
service hle_agent status # is it running?
service hle_agent restart # restart it
service hle_agent stop # stop it
tail -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:

Terminal window
hle service uninstall --agent

Surviving 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:

Terminal window
fetch -o /tmp/hle-pfsense.sh https://hle.world/scripts/pfsense.sh
sh /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:

Terminal window
sh /tmp/hle-pfsense.sh status
Or repair it by hand
Terminal window
rm -rf ~/.local/share/hle/venv
fetch -o - https://get.hle.world | sh -s -- --agent

Removing 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:

Terminal window
hle agent enroll <token-from-the-dashboard>
service hle_agent restart

On 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:

Terminal window
fetch -o - https://get.hle.world | sh -s -- --agent --no-service
hle service install --agent

hle: 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:

Terminal window
rehash

The 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:

Terminal window
fetch -o /tmp/hle-pfsense.sh https://hle.world/scripts/pfsense.sh
sh /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.