HTTPS Reverse Proxy
Put pfSense Actions behind an HTTPS reverse proxy so tokens and console sign-ins stay encrypted between clients and the Docker host.
Why Use a Reverse Proxy?
- Encrypts each personal access token in transit, which matters because a token stays valid until it expires or is revoked.
- Gives the console an HTTPS address, which passkeys and the
Secureflag on its cookies rely on. - Lets you limit the plain-HTTP listener to
127.0.0.1, where only the proxy on the same host can reach it.
Use a Loopback Binding
The included compose.yml publishes the port as ${PFSENSE_ACTIONS_PORT:-22603}:22603, on every IPv4 and IPv6 address, so devices on your network can reach the plain-HTTP listener. When the proxy runs on the same host, change it to 127.0.0.1:${PFSENSE_ACTIONS_PORT:-22603}:22603 for a loopback binding and point the proxy at that address, unless another encrypted boundary protects the listener.
Configure Caddy
Caddy is one option, and it gets certificates and adds the forwarding headers that pfSense Actions reads.
actions.example.net {
reverse_proxy 127.0.0.1:22603
}
Any other proxy works the same way if it sets X-Forwarded-For and X-Forwarded-Proto, and either sets X-Forwarded-Host or passes the browser's Host header through.
Forwarded Headers
pfSense Actions reads the X-Forwarded-* headers only from connections that come from a private or loopback address, such as a proxy on the same host or on the Docker network. There's nothing to configure.
- Client address — The rate limits and the sign-in limit use the client address from
X-Forwarded-For, which the Sign-in locked alert also names. pfSense Actions reads the list from the end and stops at the first public address, or at the first entry when every address is private, so a chain of proxies on your own network still reports the real client. - HTTPS —
X-Forwarded-Proto: httpstells pfSense Actions that the browser used HTTPS, which adds theSecureflag to the console's cookies and allows passkeys. - Hostname —
X-Forwarded-Host, or theHostheader when it's missing, gives the hostname that passkeys are tied to. - Missing headers — A proxy that doesn't send
X-Forwarded-Formakes every client share the proxy's address, and with it one rate limit. - Devices on your network — A device with a private address that can reach the listener directly can set these headers itself, which is another reason to use the loopback binding.
Serve at the Origin Root
The API routes, the console at /admin, and its cookies all assume pfSense Actions owns the root of its address. Give it its own hostname, such as https://actions.example.net, because a sub-path such as https://example.net/actions/ isn't supported.
Passkeys and the Address
Passkeys need no settings. pfSense Actions ties each passkey to the hostname in the browser when you add it, which becomes its relying party.
- The console offers Add passkey on a domain name served over HTTPS, such as
actions.example.net, or onlocalhost. On an IP address or plain HTTP, it explains why passkeys aren't available there. - A passkey added at one hostname doesn't work at another, so moving the console to a new hostname means adding your passkeys again there.
- Password and TOTP sign-in work at any address.
warning
A passkey added at localhost, for example through an SSH tunnel, works only at localhost. Add the passkeys you'll use every day at the HTTPS address you plan to keep.
Verify over HTTPS
Run a connectivity check through the proxy, then open https://actions.example.net/admin in a browser.
read -r -s PFSENSE_ACTIONS_TOKEN
printf 'header = "Authorization: Bearer %s"\n' "$PFSENSE_ACTIONS_TOKEN" \
| curl --config - --silent --show-error \
--write-out '\nHTTP %{http_code}\n' \
https://actions.example.net/api/v1/connectivity
unset PFSENSE_ACTIONS_TOKEN
HTTP 200 confirms the whole path from the proxy to the firewall.