Files
Zecheng Zhang e316c1b639 refactor(server): move daemon entrypoint into the server package (align with TS) (#78)
* refactor(server): move daemon entrypoint into the server package

Mirror the TS layout: the daemon process is now a server-package module (mirage/server/daemon.py, like server/bin/daemon.ts) that reads the daemon env vars and builds the app, replacing cli/server_factory.py.

Because the server package now reads MIRAGE_IDLE_GRACE_SECONDS and MIRAGE_PERSIST_DIR, their name constants move from cli/env.py to server/env.py; cli/env.py re-exports them, matching cli/src/env.ts.

Spawners updated to 'uvicorn mirage.server.daemon:app': cli client, cli test conftest, CLI CI workflow, and the auth docs.

* docs(cli): add Python CLI README (basic usage + env vars)
2026-05-21 17:35:01 -07:00

102 lines
3.8 KiB
Plaintext

---
title: Daemon Auth
description: How the Mirage daemon authenticates HTTP clients in local, token, and JWT modes.
icon: shield-halved
---
## What It Does
The daemon serves a local HTTP API on `127.0.0.1:8765`. Every request except `/v1/health` must present a bearer token. Which token is accepted is decided by `MIRAGE_AUTH_MODE`:
| Mode | When to use | What the daemon accepts |
| --- | --- | --- |
| `local` (default) | One user, one machine. Zero config. | A random token the CLI mints into `~/.mirage/auth_token` (mode 0o600). |
| `token` | Shared daemon, operator-issued PAT. | The exact string in `MIRAGE_AUTH_TOKEN`. |
| `jwt` | Multi-tenant, external issuer (Clerk, Auth0, your own). | Any RS256-signed JWT that verifies against `MIRAGE_JWT_PUBKEY` / `_FILE`. |
`/v1/health` is always reachable without a token so load balancers and process supervisors can probe it.
## Local Mode (Default)
You usually do nothing. The first time the CLI spawns the daemon it writes a random 32-byte token to `~/.mirage/auth_token` at mode 0o600 and uses it on every subsequent request.
```bash
mirage workspace create workspace.yaml --id demo
cat ~/.mirage/auth_token # the token the daemon expects
```
Probe it directly:
```bash
TOKEN=$(cat ~/.mirage/auth_token)
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/workspaces # 200
curl http://127.0.0.1:8765/v1/workspaces # 401
curl http://127.0.0.1:8765/v1/health # 200
```
## Token Mode (Operator PAT)
For a daemon you run yourself (Docker, systemd, a shared dev box), pin one token across all clients.
```bash
export MIRAGE_AUTH_MODE=token
export MIRAGE_AUTH_TOKEN=<your-pat>
# Python
python -m uvicorn mirage.server.daemon:app --host 127.0.0.1 --port 8765
# TypeScript
node typescript/packages/cli/dist/bin/daemon.js
```
Clients pass the same value:
```bash
curl -H "Authorization: Bearer $MIRAGE_AUTH_TOKEN" http://127.0.0.1:8765/v1/workspaces
```
Both server and CLI use a constant-time compare, so timing leakage is not a concern.
## JWT Mode (External Issuer)
Hand the daemon a public key and it will accept any non-expired JWT signed by the matching private key. Verification is networkless: no JWKS fetch, no callback to the issuer.
```bash
export MIRAGE_AUTH_MODE=jwt
export MIRAGE_JWT_PUBKEY_FILE=/etc/mirage/issuer-pub.pem
export MIRAGE_JWT_ALG=RS256
# Optional hardening
export MIRAGE_JWT_ISSUER=https://your-issuer.example
export MIRAGE_JWT_AUDIENCE=mirage-daemon
export MIRAGE_JWT_AUTHORIZED_PARTIES=https://app.example,https://cli.example
export MIRAGE_JWT_CLOCK_SKEW_SECONDS=5
```
Hard rules the daemon enforces:
- `alg` is pinned to `MIRAGE_JWT_ALG`. A token signed with a different algorithm is rejected, which defeats alg-confusion attacks.
- `alg=none` is always rejected.
- `exp` is mandatory.
- `typ`, if present, must be `JWT`.
- Opaque (non-three-segment) values in `Authorization: Bearer` are rejected before key work, so probing is cheap.
## Environment Reference
| Variable | Modes | Purpose |
| --- | --- | --- |
| `MIRAGE_AUTH_MODE` | all | `local` (default), `token`, or `jwt`. |
| `MIRAGE_AUTH_TOKEN` | local, token | Local-mode override; required in `token` mode. |
| `MIRAGE_JWT_PUBKEY` | jwt | PEM string of the public key. |
| `MIRAGE_JWT_PUBKEY_FILE` | jwt | Path to a PEM file (alternative to inline). |
| `MIRAGE_JWT_ALG` | jwt | Signing algorithm to pin, e.g. `RS256`. |
| `MIRAGE_JWT_ISSUER` | jwt | Required `iss` claim. |
| `MIRAGE_JWT_AUDIENCE` | jwt | Required `aud` claim. |
| `MIRAGE_JWT_AUTHORIZED_PARTIES` | jwt | Comma-separated allow-list for `azp`. |
| `MIRAGE_JWT_CLOCK_SKEW_SECONDS` | jwt | Default `5`. |
## Where to Go Next
- [CLI](/home/cli) walks the daily Workspace flow that uses local-mode automatically.
- [Architecture](/home/architecture) shows where the auth middleware sits in the request path.