Keep it off the internet
Never bind the open API where untrusted clients can reach it. Front it with a proxy or keep it LAN-only.
hal0 makes one security decision explicit and central: it does not ship
built-in authentication or TLS. The API binds 0.0.0.0:8080 and every endpoint
is open to anything that can reach that port. This is a deliberate design choice
(ADR-0012), and the rest of your security posture is built on top of it rather
than inside it.
Earlier designs bundled an auth layer and a TLS-terminating proxy. That was removed (ADR-0012) because it duplicated, badly, what every operator already runs better elsewhere: a real reverse proxy. Bundling auth meant maintaining credential storage, token rotation, and TLS certificate plumbing inside the inference server — surface area that is someone else’s core competency.
Instead, hal0 assumes one of two postures:
To add TLS and authentication, place a reverse proxy of your choice — Traefik and nginx are common choices — between your clients and hal0-api. The proxy owns:
TLS termination — present a certificate for your chosen hostname and terminate HTTPS at the proxy.
Authentication — enforce whatever you need (basic auth, OAuth/OIDC, mTLS, an allowlist) before a request is forwarded.
Forwarding — proxy the authenticated request to hal0-api on
127.0.0.1:8080 (or its LAN address), and bind hal0 itself so only the proxy
can reach it.
This is the standard “auth at the edge” pattern. hal0 stays a pure inference and control plane; your proxy is the single, well-understood place where access control lives.
The one place hal0 does enforce a network gate is the MCP mount. The admin and
memory MCP servers are mounted as sub-applications under /mcp/admin and
/mcp/memory, and the MCP transport applies DNS-rebinding protection.
The dashboard’s MCP tab displays the live admin and memory server status.
By default that protection is localhost-only: only 127.0.0.1, localhost,
and [::1] (any port) are accepted as Host headers and request origins. A
client reaching the mount from any other host gets a bare 421 Invalid Host header
response. Because hal0 binds 0.0.0.0 on a LAN, you usually need to widen this
to reach /mcp/* from another machine or through your proxy:
HAL0_MCP_ALLOWED_HOSTS — a comma-separated list of host,
host:port, or host:* values added to the localhost allowlist. The single
value * disables DNS-rebinding protection entirely (the fully-open posture
some LAN-only deployments want).HAL0_MCP_ALLOWED_ORIGINS — a comma-separated list of browser origins.
When left unset, http and https origins are derived automatically from
each host you add, so the dashboard and a reverse-proxy vhost work without a
second knob.This is a host/origin gate, not authentication — it stops a browser on another site from rebinding DNS to reach your loopback MCP server. Real access control still belongs at your reverse proxy.
hal0’s agent and memory surfaces don’t use bearer tokens for identity. Instead,
a caller’s identity rides on the X-hal0-Agent request header (validated to a
bounded [a-zA-Z0-9_-] id), and the same identity resolves the same memory
namespace whether it arrives over REST or MCP. Privileged or destructive agent
actions are gated: rather than executing immediately, they enqueue an
approval that a human clears from the dashboard, the CLI, or via the approval
API. See Agents for how personas, tool gating, and the
approval queue fit together.
Keep it off the internet
Never bind the open API where untrusted clients can reach it. Front it with a proxy or keep it LAN-only.
Terminate TLS at a proxy
Add HTTPS and auth at Traefik / nginx; let only the proxy reach hal0.
Scope the MCP allowlist
Widen HAL0_MCP_ALLOWED_HOSTS only to the hosts that must reach /mcp/*;
avoid * unless the whole LAN is trusted.
Treat memory as opt-in
The memory subsystem is disabled unless you enable it; keep it off if you don’t need it. See Memory.