Authentication & Security
hal0-api binds on :8080 with no built-in network gate. That is the
correct default for the common case: an LXC behind your homelab router,
reachable only over the LAN or through a reverse proxy you already
operate and trust.
If hal0 will be reachable from anything outside that trust boundary — a port-forward, a cloud VM, a shared Tailscale network, a public hostname — put a reverse proxy in front of it. hal0 has no built-in account system and does not enforce credentials at the API layer; the proxy is where authentication and TLS termination live.
Reverse proxy at the edge
Section titled “Reverse proxy at the edge”Three patterns cover the common deployments.
Traefik (homelab router)
Section titled “Traefik (homelab router)”If you already have Traefik routing *.thinmint.dev (or similar),
point a router at http://<hal0-host>:8080. Traefik handles TLS and
whatever middleware you use (basic auth, Forward Auth, Authelia, etc.).
# traefik dynamic config snippethttp: routers: hal0: rule: "Host(`hal0.thinmint.dev`)" entryPoints: [websecure] tls: certResolver: le middlewares: [my-auth] service: hal0
services: hal0: loadBalancer: servers: - url: "http://10.0.1.142:8080"hal0-api trusts the LAN. No special header wiring is required; the proxy is the authentication boundary.
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;
# Put your auth directive here — basic_auth, auth_request, etc. auth_basic "hal0"; auth_basic_user_file /etc/nginx/.htpasswd;
location / { proxy_pass http://10.0.1.142:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_http_version 1.1; }}The Upgrade / Connection headers are required for the dashboard’s
WebSocket connections to pass through.
Cloudflare Tunnel
Section titled “Cloudflare Tunnel”For internet-facing deployments without opening inbound firewall ports:
tunnel: <tunnel-id>credentials-file: /root/.cloudflared/<tunnel-id>.json
ingress: - hostname: hal0.example.com service: http://localhost:8080 - service: http_status:404Add a Cloudflare Access policy on hal0.example.com to gate entry
with email OTP, GitHub SSO, or any other identity provider. The
tunnel process on the hal0 host dials outbound; no inbound port is
opened.
Built-in gate: chat-proxy Origin allowlist
Section titled “Built-in gate: chat-proxy Origin allowlist”The one in-process network control hal0-api enforces is an HTTP Origin allowlist on the chat-proxy WebSocket endpoint. This prevents a page on a foreign origin from opening a WebSocket to the local API (DNS-rebinding / cross-site WebSocket hijacking).
Knob: HAL0_ALLOWED_ORIGINS in /etc/hal0/api.env
# Default — only the local dashboard originHAL0_ALLOWED_ORIGINS=http://localhost:8080
# Multiple origins (comma-separated)HAL0_ALLOWED_ORIGINS=https://hal0.thinmint.dev,http://localhost:8080The API also issues an HMAC-signed HttpOnly / SameSite=Strict session cookie for browser sessions initiated from an allowed origin. This cookie:
- Is signed with a server-side secret (
HAL0_SESSION_SECRETin/etc/hal0/api.env); tampering invalidates it. - Is
HttpOnly— not readable by JavaScript. - Is
SameSite=Strict— not sent on cross-site requests. - Does not replace proxy-level auth. It scopes the browser session after the proxy has already admitted the request.
MCP transport security
Section titled “MCP transport security”The MCP endpoint has its own DNS-rebinding allowlists, separate from
HAL0_ALLOWED_ORIGINS.
| Variable | Purpose |
|---|---|
HAL0_MCP_ALLOWED_HOSTS | Comma-separated Host header values the MCP server will accept. Prevents DNS-rebinding attacks where a browser navigates to a hostile name that resolves to 127.0.0.1. |
HAL0_MCP_ALLOWED_ORIGINS | Comma-separated Origin header values for the MCP WebSocket upgrade. |
HAL0_MCP_ALLOWED_HOSTS=hal0.thinmint.dev,localhostHAL0_MCP_ALLOWED_ORIGINS=https://hal0.thinmint.dev,http://localhost:8080An MCP request whose Host is not in the allowlist gets 421 Invalid Host. Missing or empty HAL0_MCP_ALLOWED_HOSTS defaults to
localhost,127.0.0.1.
Quick reference
Section titled “Quick reference”| Variable | Default | Effect |
|---|---|---|
HAL0_ALLOWED_ORIGINS | http://localhost:8080 | WebSocket origin allowlist for the chat proxy. |
HAL0_SESSION_SECRET | auto-generated at install | HMAC key for the HttpOnly session cookie. |
HAL0_MCP_ALLOWED_HOSTS | localhost,127.0.0.1 | Host header allowlist for the MCP endpoint. |
HAL0_MCP_ALLOWED_ORIGINS | http://localhost:8080 | Origin allowlist for the MCP WebSocket upgrade. |
All four live in /etc/hal0/api.env. Edit the file and restart
hal0-api for changes to take effect:
sudo systemctl restart hal0-api