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#
| Path | Open because |
|---|---|
GET /health | a liveness probe must answer before anything else does |
GET /health/ready | a readiness probe must answer while the server is not ready — it is the route that returns 503 with a progress note |
GET /version | the version is also on every reply as moxzid-version; hiding it buys nothing |
GET /inspector, and GET / when --web is unset | a 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 set | this is a website. A page behind a bearer token is not a page |
OPTIONS on any non-gateway path | a 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.
| Order | Check | Failure |
|---|---|---|
| 1 | OPTIONS on a non-gateway path | short-circuits to 204 |
| 2 | bearer token, unless the path is open | 401 |
| 3 | Content-Length against the cap | 413, body never read |
| 4 | readiness, for anything that is not a health probe | 503 starting: <note> |
| 5 | route 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 possible | Consequence |
|---|---|
| Distinguish two callers | every 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 access | the token is all-or-nothing. /call and /install are behind the same string as /logs |
| Rotate a token without a restart | it is read once at startup, from the flag or the environment |
| Terminate TLS in moxzid | there 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-limit | none of it exists in the binary; it belongs in the reverse proxy alongside TLS |
| Trust a certificate | there 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.
| Rule | Why |
|---|---|
| Never bind a public interface without a token | anyone who can reach the port can install and run wasm. moxzid refuses this unless you pass --insecure |
| Never expose the port directly to the internet | put a TLS-terminating reverse proxy in front; the token is plaintext otherwise |
Never use --insecure outside a network you already control | it exists so an unauthenticated bind is a stated decision, not a default |
| Never share the token with a page you do not control | it 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 publish | it is a file reader for whoever holds the token |
Next#
- The HTTP API — the full endpoint table, including which are open.
- Operating moxzid — the reverse proxy, systemd and Docker recipes.
- What moxzid is — the model this auth grade belongs to, and its limits.