Authenticationalpha
Protect moxzid with a bearer token, and what is and isn't covered by it.
moxzid uses a single bearer token. Every request except health checks, the version, the inspector page and website traffic must carry it. The token protects the server from strangers; it doesn't identify users.
Set a token#
export MOXZID_TOKEN=$(openssl rand -hex 32)
moxzid -m actors.json -s ./state -l 0.0.0.0:7000
--auth-token TOKEN does the same as MOXZID_TOKEN; the option wins if both are set. The token is read once at startup. To change it, restart the server.
Clients send it in the Authorization header:
curl -s -w '\n' -H "Authorization: Bearer $MOXZID_TOKEN" \
-d '["ada"]' http://127.0.0.1:7000/call-json/greeter/greet
On a loopback address (127.0.0.1, ::1, localhost) the token is optional. On any other address moxzid won't start without one:
moxzid -m actors.json -l 0.0.0.0:7001
Error: refusing to bind 0.0.0.0:7001 without authentication: anyone who can reach this port could install and run arbitrary wasm.
Fix: pass --auth-token <t> (or set MOXZID_TOKEN), or bind 127.0.0.1, or state the decision with --insecure.
--insecure starts it anyway. Only use that on a network you control.
What needs the token#
| Open to anyone | Needs the token |
|---|---|
GET /health, GET /health/ready, GET /version | Calls and queries: /call, /query, /call-json, /query-json |
GET /inspector, and GET / without --web (the page itself; its requests carry the token) | Managing actors: /install, /upgrade, /cycles |
Website routes: /site/ACTOR/…, and every other path when --web is set | Observing: /actors, /inspect, /logs, /events, /history, /metrics, /openapi |
CORS preflight (OPTIONS) on non-website paths | /mops/closure |
A request without a valid token gets 401 missing or wrong bearer token. The token is checked before the request body is read.
Open paths must match exactly: with a token set, /health?x=1 needs the token.
Website routes are public#
Website routes behave like a canister's website on the IC: anyone can request them. GET requests reach the actor's http_request method as a query, which can't change state. An actor changes state only if it answers upgrade = ?true and handles the request again in http_request_update. The Authorization header is never passed to the actor.
Public actor and website request bodies are limited to 8 MiB. Authenticated control, image and actor-service requests can be up to 192 MiB, which covers the 128 MiB stored-image ceiling after JSON/base64 expansion.
Callers aren't authenticated#
The token says a request is allowed, not who sent it. Calls arrive from the anonymous principal, or from whatever principal the x-caller header claims. Anyone with the token can claim any principal, so msg.caller checks in a hosted actor don't protect anything.
If your actors need to know who's calling, authenticate users in front of moxzid (in your application or a proxy) and pass the verified principal in x-caller.
Keep the token safe#
Anyone with the token can install and run arbitrary code on the server, and read every actor's logs and recent messages.
| Do | Why |
|---|---|
| Put a TLS proxy in front of any non-local server | moxzid doesn't do TLS, so the token would cross the network in plain text |
| Keep the token out of web pages you don't control | It allows installs as well as calls |
| Don't print secrets from actors | /logs and /history return them to anyone with the token |
Point --project only at directories you'd publish | /mops/closure serves files from it |
Limitations#
| Limitation | What to do |
|---|---|
| One token; no read-only or per-user tokens | Put a proxy in front that enforces finer rules |
| No IP allowlists or rate limits | Configure them in the proxy |
| Changing the token needs a restart | Restart with SIGTERM; state is kept |