moxzi
Docs / Server / Authentication

Authenticationalpha

Which routes are protected, and what must never be exposed?

moxzid's authentication is one shared bearer token, compared in constant time, required on every endpoint that is not health, version, the inspector page, or the public HTTP gateway. That is the whole model, stated plainly because it is alpha-grade: it is enough to stop a stranger who can reach the port from installing wasm, and no more. There are no roles, no per-user keys, and no notion of a caller identity — every message an actor receives arrives from the anonymous principal 0x04 unless the client names one with x-caller: <principal>. That header is a claim, not authentication — the bearer-token holder can already install code, so letting them speak as a principal adds no power a local replica does not also grant its operator. The WAL records the claimed caller.

Setting the token#

# loopback development: no token needed
moxzid -m actors.json -s ./state

# exposed: a token, or it refuses to start
MOXZID_TOKEN=$(openssl rand -hex 32) \
  moxzid -m actors.json -s /var/lib/moxzi -l 0.0.0.0:7000

--auth-token <t> and the MOXZID_TOKEN environment variable are the same setting; the flag wins. moxzid refuses to bind a non-loopback address without one and says so, with the reason: POST /install runs arbitrary wasm, so an open port must be a decision someone spelled out. --insecure is that decision, spelled out. Whether an address is loopback is determined by resolving it, not by matching the string, so localhost:7000, 127.0.0.1:7000 and [::1]:7000 all qualify and 0.0.0.0:7000 does not.

Clients send Authorization: Bearer <token>. The header name is matched case-insensitively; the token value is sliced from the original line, so it stays case-sensitive. Constant-time comparison hides the token's prefix; its length still leaks, which is acceptable for a high-entropy secret.

The open set#

PathOpen because
GET /healtha liveness probe must answer before anything else does
GET /health/readya readiness probe must answer while the server is not ready — it is the route that returns 503 with a progress note
GET /versionthe version is also on every reply as moxzid-version; hiding it buys nothing
GET /inspector, and GET / when --web is unseta static HTML page that discloses nothing. Every data call it makes still carries the token, typed into its token box
/site/<actor>/<path>, and every non-builtin path when --web is setthis is a website. A page behind a bearer token is not a page
OPTIONS on any non-gateway patha browser sends preflights without credentials by design; answering 401 to one breaks every web console. The actual request is still checked

Everything else — /metrics, /inspect, /actors, /call, /query, /install, /upgrade, /cycles, /logs, /history, /events, /mops/closure — requires the token and answers 401 missing or wrong bearer token without it.

The open set is matched on the exact target. With a token configured, GET /health?x=1 is 401, not 200: the query-string form is not in the list.

Order of checks#

Auth is evaluated before the request body is read. This is deliberate and was a fix, not a design accident: the earlier order read the whole body first, so an unauthenticated peer could make the server allocate an arbitrary amount of memory before ever reaching the 401.

OrderCheckFailure
1OPTIONS on a non-gateway pathshort-circuits to 204
2bearer token, unless the path is open401
3Content-Length against the cap413, body never read
4readiness, for anything that is not a health probe503 starting: <note>
5route and read the body

Body caps differ by trust. Token-guarded routes accept 64 MiB, because /install and /upgrade carry wasm modules and a linked module can be ~10 MB. The gateway accepts 8 MiB, because it is open to an anonymous peer and must not let one make the server allocate an install-sized body. A 30-second read timeout bounds a slowloris.

What the gateway is, and is not#

The gateway is public on purpose. The security boundary there is not the token, it is the actor: a GET reaches http_request as a query, whose writes are rolled back, and the only way to mutate anything is for the actor itself to answer upgrade = ?true and accept the request again in http_request_update. If your actor mutates on a GET, that is your decision and moxzid will honour it.

The Authorization header is deliberately not forwarded to the actor. It is moxzid's credential, and a page's actor has no business seeing the operator's token. Every other request header is forwarded verbatim, the way a boundary node would.

Things you cannot do#

Not possibleConsequence
Distinguish two callersevery ingress message is the anonymous principal 0x04. msg.caller cannot be used for authorization; an actor's controller checks pass for whoever holds the token
Give one client read-only accessthe token is all-or-nothing. /call and /install are behind the same string as /logs
Rotate a token without a restartit is read once at startup, from the flag or the environment
Terminate TLS in moxzidthere is no TLS stack in the binary. The token crosses the wire in plaintext without a proxy in front
Restrict by origin or IP, or rate-limitnone of it exists in the binary; it belongs in the reverse proxy alongside TLS
Trust a certificatethere are none off-chain. ic0.data_certificate_present is 0, and @dfinity/agent's Actor cannot be used

What must never be exposed#

Treat the token-guarded surface as root on the host process. POST /install/<name> compiles and runs arbitrary WebAssembly inside moxzid; GET /mops/closure/<entry> reads files from the operator's disk (confined to --project, but that is a directory of yours); GET /logs/<actor> and GET /history/<actor> return whatever the hosted actors printed and were sent, argument previews included.

RuleWhy
Never bind a public interface without a tokenanyone who can reach the port can install and run wasm. moxzid refuses this unless you pass --insecure
Never expose the port directly to the internetput a TLS-terminating reverse proxy in front; the token is plaintext otherwise
Never use --insecure outside a network you already controlit exists so an unauthenticated bind is a stated decision, not a default
Never share the token with a page you do not controlit authorizes installs, not just calls
Never put secrets in an actor and assume the log is private/logs and /history hand them to anyone with the token
Never leave --project pointed at a directory containing anything you would not publishit is a file reader for whoever holds the token

Next#

On this pageSetting the tokenThe open setOrder of checksWhat the gateway is, and is notThings you cannot doWhat must never be exposedNext