Skip to content

Security: dimakis/mitzo

Security

SECURITY.md

Security Model

Threat Model

This server is designed to run on a private home server (e.g., Mac Mini) and be accessed exclusively over Tailscale — a zero-trust mesh VPN with WireGuard encryption. It is never exposed to the public internet.

Given that deployment model, the security posture prioritizes simplicity over defense-in-depth. The Tailscale network boundary is the primary perimeter.

Authentication

  • Passphrase + JWT cookie: login via POST /api/auth/login with a plaintext passphrase, which returns an HS256 JWT stored as an httpOnly cookie.
  • Cookie scope: sameSite: strict, no secure flag (HTTP over Tailscale, not HTTPS).
  • Cookie expiry: configurable via a positive whole-number COOKIE_MAX_AGE_HOURS (default: 24h). Active browser SSE and chat WS/SSE streams are closed at the JWT expiry boundary.
  • WebSocket auth: upgrade requests are verified against the same JWT. A native query token is explicit and takes precedence over any ambient cookie.
  • Credential precedence: explicit bearer and allowed SSE query credentials take precedence over cookies. An invalid explicit credential is rejected instead of falling back to another login context.
  • Logout: the presented JWT is revoked in the running server and all active streams using it are closed. Revocations are process-local; expiry remains the durable upper bound after a server restart.
  • Insecure defaults blocked: auth.ts refuses to start for example credentials, secrets shorter than 32 bytes, or an invalid cookie TTL.

Secrets Management

Secret Storage Exposure
AUTH_PASSPHRASE .env (gitignored) Never logged, never sent to SDK
AUTH_SECRET (JWT signing key) .env (gitignored) Never logged, never sent to SDK
NTFY_AUTH_TOKEN .env (gitignored) Used in ntfy API calls and permission endpoint auth. Never sent to SDK
Vertex AI credentials GCP Application Default Credentials ANTHROPIC_VERTEX_PROJECT_ID is a project name (not sensitive), actual auth uses ADC
REPO_PATH .env (gitignored) Exposed to frontend via /api/config (non-sensitive — it's a local path)
MCP server credentials ~/.cursor/mcp.json Read by mcp-config.ts, passed to Agent SDK subprocess. Server names (not tokens) exposed via /api/config. Credentials never logged or sent to frontend.

The sdkEnv() function in chat.ts explicitly deletes AUTH_PASSPHRASE, AUTH_SECRET, and NTFY_AUTH_TOKEN from the environment before passing it to the Agent SDK, preventing accidental exposure to Claude sessions.

Unauthenticated Endpoints

Two endpoints bypass cookie auth:

  1. GET /api/version — returns a build hash. No sensitive data.
  2. POST /api/permission/:permId/respond — used by ntfy action buttons to approve/deny tool permissions remotely. Authenticated by NTFY_AUTH_TOKEN query parameter instead of cookie. This is intentional: ntfy push notification actions can't carry cookies, so token auth is used instead.

Both are acceptable given the Tailscale-only access model.

Agent SDK Sessions

Each chat session spawns a Claude Code process via the Agent SDK with:

  • cwd set to the base repo (default) or a worktree (if sandbox mode is enabled)
  • Project-level settings from .cursor/rules/ (read from the cwd)
  • MCP servers loaded from ~/.cursor/mcp.json (stdio-type only, disabled servers excluded)
  • Environment variables with secrets stripped

Claude sessions have full filesystem access within their cwd. This is by design — the Agent SDK's permission system (canUseTool) controls tool-level access, and the user approves or denies from the UI.

Tools are classified into risk tiers (safe, standard, elevated, unknown) in tool-tiers.ts. Safe tools (reads) are always auto-allowed. Standard tools (file writes) are auto-allowed in Agent and Auto modes. Elevated tools (shell) require explicit approval in Agent mode but are auto-allowed in Auto mode. Unknown tools (MCP, etc.) always require approval. Agent and Auto both use the SDK's default mode. A Mitzo PreToolUse hook enforces the live policy even when project allow rules would otherwise skip canUseTool. Unknown tools still prompt. Codex and Responses use the same host policy; their Bash tool executes through an OS sandbox with credential protection and no network access. See unified permissions for provider mappings and verified limits.

Session Resilience

WebSocket disconnects detach (not abort) the active session via SessionRegistry. The Agent SDK query continues running server-side. A reconnected WebSocket can reattach within a 10-minute TTL. After the TTL, abandoned sessions are aborted and cleaned up. This prevents session state accumulation from repeated disconnects.

The frontend WS pool buffers messages (capped at 500) while the user navigates between pages, replaying them on return. The buffer only stores UI-relevant message types (text, tool calls, errors, etc.) — connection lifecycle messages are not buffered.

Repo Configuration

.mitzo.json in the repo root is read-only at startup. It cannot execute code — it provides JSON data (quick action labels/prompts and venv path strings). Quick action prompts are sent to the Agent SDK as user messages (same as typing them manually). Venv paths are prepended to PATH in the Agent SDK subprocess environment.

File Browser

The /api/files, /api/files/read, and /api/files/write endpoints restrict access to paths under REPO_PATH and its worktree sessions directory. Path traversal is blocked by resolve() + startsWith() checks. The root query parameter (used for worktree browsing) is validated through the same isAllowedPath() check.

PUT /api/files/write only overwrites existing files within allowed paths — it cannot create new files or write outside the repo boundary. All file endpoints are behind cookie auth middleware.

Skills System

Skills (invoked via /slash-command) can declare allowed-tools in their YAML frontmatter. This is enforced by skill-policy.ts as a ceiling — skills can only restrict the tools available to a session, never expand them. A skill that declares allowed-tools: [Read, Grep] will block writes and shell access for that turn, regardless of the session's mode. Native commands (TypeScript) bypass this — they are product behavior, not prompt-based.

Skill files are read-only markdown loaded from disk (bundled, ~/.mitzo/skills/, or ${cwd}/.mitzo/skills/). They cannot execute code — they expand into system prompt content.

Known Limitations

  • No HTTPS: traffic is encrypted by Tailscale (WireGuard), but the HTTP layer itself is plaintext. If accessed outside Tailscale, cookies and passphrases would be transmitted in the clear.
  • Process-local logout revocation: restarting the service clears the in-memory JWT revocation set. Clients still remove browser/native credentials on logout, and every token remains bounded by its signed expiry.
  • Offline cookie revocation: an offline client cannot delete the server-issued httpOnly cookie. The UI records the logout intent, closes active transports, and refuses cookie-based session restoration until the user explicitly signs in again; server-side revocation still requires the logout request to reach the service.
  • No CSRF protection: sameSite: strict mitigates most CSRF vectors, but there's no explicit CSRF token.
  • Single-user: no user accounts, roles, or audit logging. The passphrase is shared across all access.
  • MCP credentials in mcp.json: MCP server tokens (e.g., Jira API tokens) are stored in ~/.cursor/mcp.json. This file is not managed by Mitzo — it's the user's existing Cursor configuration. Mitzo reads it but never modifies or copies it.

There aren't any published security advisories