Securing hal0
hal0 has no built-in authentication CLI, no user/password store, and no
bundled reverse proxy. hal0-api binds 0.0.0.0:8080 and every endpoint is
reachable by anything that can reach that port. This was a deliberate cut
(ADR-0012, “remove-auth-and-caddy”) — earlier betas bundled a password store
and a Caddy TLS-terminating proxy, and both were removed because they
duplicated, badly, what a real reverse proxy already does better. There is no
--auth=basic install flag and no hal0 auth subcommand.
Threat model, in short
Section titled “Threat model, in short”hal0 assumes one of two deployment postures:
- Single trusted LAN — hal0 is reachable only from machines you trust (e.g. your home network), and an open API is an acceptable risk.
- Reverse proxy at the edge — you put a proxy you control (Caddy, Traefik, nginx, a cloud load balancer, a tunnel) in front of hal0, and that proxy — not hal0 — owns TLS termination and authentication.
There is no third, hal0-managed option. If you want a login prompt, HTTPS, or per-request access control, that’s a proxy you stand up and run yourself.
Reverse proxy at the edge (your proxy, not hal0’s)
Section titled “Reverse proxy at the edge (your proxy, not hal0’s)”Run hal0 bound to 127.0.0.1 (or a private interface) and put a proxy in
front of it that terminates TLS and enforces auth before forwarding to hal0.
None of the following is installed, managed, or aware of hal0 — these are
plain examples of proxies you configure and operate.
hal0.example.com { basic_auth { alice $2a$14$... # bcrypt hash from `caddy hash-password` } reverse_proxy 127.0.0.1:8080}Caddy obtains and renews the certificate automatically via ACME as long as
port 80/443 are reachable for the challenge. Generate the bcrypt hash once
with caddy hash-password — Caddy never sees the plaintext password again.
http: routers: hal0: rule: "Host(`hal0.example.com`)" service: hal0 tls: certResolver: letsencrypt middlewares: - hal0-auth middlewares: hal0-auth: basicAuth: users: - "alice:$apr1$...htpasswd-hash..." services: hal0: loadBalancer: servers: - url: "http://127.0.0.1:8080"server { listen 443 ssl; server_name hal0.example.com;
ssl_certificate /etc/letsencrypt/live/hal0.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/hal0.example.com/privkey.pem;
auth_basic "hal0"; auth_basic_user_file /etc/nginx/hal0.htpasswd;
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}Generate /etc/nginx/hal0.htpasswd with htpasswd -c and obtain the
certificate however you already manage certs (certbot, an ACME client, or a
cert issued by internal infrastructure).
-
TLS termination — present a certificate for your chosen hostname and terminate HTTPS at the proxy.
-
Authentication — enforce whatever you need (basic auth as shown above, OAuth/OIDC, mTLS, an IP 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.
What hal0 does provide on its own
Section titled “What hal0 does provide on its own”hal0 doesn’t manage user accounts or TLS, but it isn’t a bare-open socket either. A few narrower gates exist inside hal0 itself:
- MCP host and origin allowlist — the admin (
/mcp/admin) and memory (/mcp/memory) MCP mounts default to a localhost-only DNS-rebinding allowlist (127.0.0.1,localhost,[::1]); a non-localhostHostheader gets a bare421 Invalid Host header. Widen it withHAL0_MCP_ALLOWED_HOSTS(and, if needed,HAL0_MCP_ALLOWED_ORIGINS) to reach/mcp/*from another machine or through your proxy. This is a host/origin gate against DNS rebinding, not authentication — see Security. - Agent-chat origin allowlist + session cookie — the agent-chat
WebSocket routes check the browser
OriginagainstHAL0_ALLOWED_ORIGINS(default covershttp://hal0.local,http://localhost:5173, andhttp://127.0.0.1:8080) on every upgrade, and require an HMAC-SHA256 session cookie (hal0_session,HttpOnly,SameSite=Lax, ~8h TTL) that is minted byGET /api/agents/{id}/session/handshakeand verified on both the WS upgrade and the REST session endpoints. The signing secret is generated on first use at/var/lib/hal0/agents/secret.bin(mode0600) and never leaves the hal0 service user. This stops a drive-by site from WebSocket-ing into the chat proxy using a browser session that’s already open to hal0 — it is not a login system. X-hal0-Agentidentity header — every agent/MCP caller identifies itself with theX-hal0-Agentheader (validated to a bounded[a-zA-Z0-9_-]{1,64}id), which stamps the audit log and resolves theprivate:<agent>memory namespace consistently whether the call arrived over REST or MCP. There is no Bearer/API-key surface — this header is an identity claim, not a secret, so it only means something once you’ve already gated access at the network layer (LAN-only or your proxy).- Gated agent actions — privileged or destructive agent/MCP actions don’t execute immediately; they enqueue onto an approval queue that a human clears from the dashboard, the CLI, or the approvals API. See Agents for how personas, tool gating, and the approval queue fit together.
None of the above is a substitute for authentication at the edge — they
narrow specific browser/DNS-rebinding attack surfaces and give you an audit
trail, but they don’t gate who can reach /api/* or /v1/* in the first
place. That’s still the reverse proxy’s job.
See also
Section titled “See also”- Security — the full security posture and threat model this page is a reference companion to.
- Connect an MCP client — configuring
HAL0_MCP_ALLOWED_HOSTS/HAL0_MCP_ALLOWED_ORIGINSand theX-hal0-Agentheader in practice. - Environment variables — the full env-var
reference, including
HAL0_ALLOWED_ORIGINSand the MCP allowlist knobs.