2 Network Mode and Security
uncaney edited this page 2026-08-14 14:54:09 +02:00

Network Mode and Security

By default the API is reachable only over the local Unix socket (mode 0600) or Windows named pipe, which are same-user only and fully trusted: no authentication, all scopes, no path restrictions. An optional TCP (and WebSocket) transport can be enabled for off-box or cross-process clients. That transport is authenticated, scope-checked and path-jailed on every request; the local socket is unaffected.

Enabling the network transport

Set environment variables before starting the launcher. The variables are currently named PANDAPI_API_*; PANDAPI_* aliases are planned as part of the Pandapi rebrand (with the PANDAPI_API_* names staying authoritative), but today only these work:

Variable Effect
PANDAPI_API_TCP Bind a TCP listener. Value is host:port, or a bare port (interpreted as 127.0.0.1:<port>). Unset = no network transport.
PANDAPI_API_WS 1/true to also accept WebSocket clients on the same port (the listener sniffs an HTTP GET upgrade; raw NDJSON still works on the same port).
PANDAPI_API_NO_AUTH 1/true to disable authentication. Honored only for a loopback bind; a non-loopback bind forces auth back on and logs a warning.
PANDAPI_API_PATH_ALLOW Colon-separated directories that network clients may reference in path parameters. Unset or empty = every path parameter denied.

Example (headless host):

PANDAPI_API_TCP=127.0.0.1:8730 PANDAPI_API_WS=1 \
PANDAPI_API_PATH_ALLOW="$HOME/mc-share" \
ekaii-launcher --headless

net.status reports the live configuration (bind, websocket, require_auth, allowlist, active connections, token count).

Minting tokens

auth.mint_token is local-socket only: tokens are minted on the launcher host, never over the network, so the network can never bootstrap its own credentials.

# On the launcher host:
ekaii-launcher --api auth.mint_token \
  --api-params '{"scopes":["read","write"],"ttl_secs":86400,"label":"ci"}'
# {"token":"Zm9vYmE.c2VjcmV0...","token_id":"Zm9vYmE","scopes":["read","write"],
#  "expires_unix":1765486400,"warning":"shown once; store it now, it is not recoverable"}
  • The full token (<token_id>.<secret>) is shown once; only a hash is persisted (api_tokens.json, mode 0600).
  • ttl_secs is optional; omit for a non-expiring token. label is free text for auditing.
  • Manage with auth.list_tokens and auth.revoke_token (both local-only). Revocation and TTL expiry take effect on the next request of any connection already using the token, not just on reconnect.

Authenticating a network connection

Pre-auth, only these commands are allowed: auth.hello, auth.bearer, auth.prove, auth.whoami, launcher.version. A typical session over nc 127.0.0.1 8730 or a WebSocket:

{"id":1,"cmd":"auth.hello"}
{"id":1,"ok":true,"data":{"auth_required":true,"methods":["bearer","hmac"],"nonce":"9f86d081...","transport":"tcp","session_id":"18f3c0a1b2"}}
{"id":2,"cmd":"auth.bearer","params":{"token":"Zm9vYmE.c2VjcmV0..."}}
{"id":2,"ok":true,"data":{"authenticated":true,"token_id":"Zm9vYmE","scopes":["read","write"],"label":"ci"}}
{"id":3,"cmd":"instances.list"}

Two methods:

  • auth.bearer: send the full token string. Simple; the secret crosses the (tunneled) wire.
  • auth.prove: send token_id plus the hex HMAC-SHA256(secret, nonce) over the nonce from auth.hello, so the secret never crosses the wire. Works only for tokens minted in the current launcher session (hmac_capable in auth.list_tokens); after a restart, fall back to auth.bearer.

auth.whoami reports the connection's transport, identity and scopes at any time.

Scopes

Scope Covers
read Observe-only commands: listings, logs, metrics, state.export, status.summary, api.describe, events.subscribe, ops.list/status/wait, net.status, settings.get, sync.state, accounts.list, ...
write Everything not in the read allowlist (fail-safe default for new commands). A write token also satisfies read.
accounts Account mutations (accounts.* except accounts.list)
settings settings.* and sync.* writes
* Everything

Mint the narrowest token that does the job: read for dashboards and observers, write without accounts/settings for most automation. The machine-readable version of this whole model (scopes, the exact read-only set, the local-only set, denied fields, env vars, error codes) is in api.describe under security.

Local-socket-only commands

Refused over TCP/WebSocket regardless of scope, with forbidden:

  • auth.mint_token, auth.list_tokens, auth.revoke_token (credential bootstrap)
  • launcher.quit, launcher.install_update (host control)
  • hooks.register, hooks.delete (a persistent outbound channel: SSRF + event exfiltration; hooks.list remains readable)
  • servers.status (connects to arbitrary caller-chosen addresses: an SSRF / port-scan primitive)

The path allowlist and denied fields

Every path parameter from a network client must canonicalize under an PANDAPI_API_PATH_ALLOW root or the request fails with path_denied. Covered: logs.read/logs.tail/diagnostics.analyze/logs.upload path, instances.export output, instances.import_file file, instances.relocate/instances.create_shortcut path, content.dev_link source_glob, skins.add file, worlds.restore backup, import.scan path, import.run root and paths[], the nested file.path entries of content.install/content.resolve/content.sync, and icon.path.

Independently of the allowlist, the execution-vector fields wrapper_command, linux_wrapper, system_libraries, jvm_binary and jvm_flags are refused outright over the network on any command, including through instances.ensure. A network client cannot make the launcher execute arbitrary programs.

No TLS: loopback and tunnels

The network transport is plaintext. It is intended for loopback or an already-encrypted tunnel; do not expose it directly to an untrusted network. Recommended patterns:

# SSH tunnel from your workstation to a launcher host bound to 127.0.0.1:8730
ssh -N -L 8730:127.0.0.1:8730 user@gamebox
# then connect to 127.0.0.1:8730 locally and auth.bearer as usual
  • WireGuard / VPN: bind to the VPN interface address and let the tunnel provide the encryption.
  • Reverse proxy: with PANDAPI_API_WS=1, put the WebSocket endpoint behind an HTTPS-terminating reverse proxy; keep the launcher bind on loopback.
  • Even inside a tunnel, keep auth on; PANDAPI_API_NO_AUTH is for loopback development only.

WebSocket details

With PANDAPI_API_WS=1, clients connect with a standard RFC6455 handshake (the implementation is built in, no external web stack). The protocol on top is unchanged: one JSON message per frame (one request per client frame; each response/event line is one server text frame). Fragmented messages are reassembled, ping/pong frames are ignored, client frames must be masked, and the message cap is 1 MiB.

Error codes

Authorization failures use the structured error object (see Concepts): unauthorized (authenticate first, or the token is unknown/revoked/expired/bad), forbidden (missing scope, or a local-socket-only command over the network), path_denied (path outside the allowlist, or an execution-vector field set). Branch on the code; message carries the specific reason.