Authentication

aiod supports one optional startup key.

When no key is configured, every route is open and aiod is fully exposed. Bind it to 127.0.0.1, or put a gateway with its own authentication in front.

Starting with an API key

The flag and the environment variable set the same key:

aiod start --api-key my-secret-key
# equivalently
AIO_API_KEY=my-secret-key aiod start

Sending the API key

Three equivalent forms:

curl "$BASE_URL/v1/sandbox" -H "Authorization: Bearer my-secret-key"
curl "$BASE_URL/v1/sandbox" -H "x-api-key: my-secret-key"
curl "$BASE_URL/v1/sandbox?api_key=my-secret-key"
ChannelWhen to use itNotes
Authorization: BearerThe default for an HTTP clientA standard bearer token
x-api-keyA client that already speaks this headerThe prebuilt images' gateway also accepts X-AIO-API-Key
?api_key=Only where headers are impossibleDeprecated; percent-decoded before the compare

Every channel is compared in constant time, so neither the key's length nor how far a guess matched shows up in response timing.

A credential in a URL can leak into access logs, browser history, and the Referer of a later request.

Use the query form only when a request header is not possible, such as with a browser-native WebSocket or a VNC page:

{ws_base_url}/v1/shell/ws?session_id=SESSION_ID&api_key=my-secret-key

A WebSocket is authenticated before the upgrade. Without a valid key, the handshake answers 401 instead of opening and closing.

Except on the public routes below, every request without a valid key answers 401 with the standard envelope:

{
  "success": false,
  "message": "Unauthorized",
  "data": null,
  "hint": null
}

Public routes

These skip the key check:

  • /
  • /health
  • /v1/ping
  • /v1/openapi.json
  • /v2/openapi.json
  • /internal/auth

Public paths match by prefix. For example, /health also covers /health/details, while / covers only itself and cannot exempt the whole API.

Public does not mean that a route has content: the daemon serves nothing at /, which is normally where a gateway puts its landing page.

Everything else requires the key when one is set, including /metrics — a scrape surface exposes route names and traffic shape, so there is no exemption for ops endpoints.

Behind a gateway

/internal/auth is the subrequest endpoint used by nginx auth_request. It reads the original request path from X-Original-URI and applies the same key check:

  • It answers 204 when the request is authorized, the target is public, or no key is configured.
  • It answers 401 otherwise.
  • It never returns a body.

One auth_request can therefore gate everything the gateway fronts, including the VNC stream that never reaches aiod's middleware. When called directly, with no X-Original-URI, it checks the current request's own credentials.

CORS

CORS is open by design. aiod does not enforce an origin policy; the gateway in front of it does. Never expose the CDP port (default 9222) outside a trusted network — it has no authentication of its own.

Identity: execution vs. ownership

user on a request means two different things, depending on the API. AIO_DEFAULT_USER sets the same default:

On commands, PTY, and code, user decides which account runs the process; on files, it only decides who owns new objects.

On files, the daemon still reads and writes with its own privileges; user does not change the execution identity.

This only applies on Linux. Windows answers 400 for an explicit user, and 503 when AIO_DEFAULT_USER would apply — see Windows.