Skip to main content

Security Architecture

Celistra is zero-trust between the browser and the daemon: the daemon assumes every inbound request is hostile until proven otherwise.

1. Machine Secret

Each celistrad generates a 256-bit hex secret on first run (stored in ~/.celistra_auth.json, chmod 600). Every authenticated request carries Authorization: Bearer <secret>.

The secret is never sent to Firestore. Rotating it requires re-pairing.

2. Pairing (Challenge–Response)

Dashboards obtain the secret via a transient handshake:

  1. User clicks Pair new machine in the tray. Daemon mints an 8-digit one-time token (TTL 120s).
  2. Tray opens https://app.celistra.dev/?t=<token> in the default browser.
  3. Dashboard POSTs { firebaseIdToken, token } to POST /v1/auth.
  4. Daemon verifies the Firebase ID token against the configured UID, checks the challenge, and — on success — returns the Bearer secret + tunnel URL.
  5. Token is burned; a second attempt returns 401.

If the Firebase UID doesn't match the daemon's expected owner UID, the request is rejected regardless of the token.

3. Transport

  • REST / SSE: Authorization: Bearer.
  • WebSocket: browsers can't set headers on upgrade, so the secret goes in the query string (?secret=<s>). The daemon accepts this only on /v1/terminal.
  • CORS: allowed origins are the app domain and null/file:// for the tray's local dashboard. Credentials are not required.
  • Private Network Access: daemon replies with Access-Control-Allow-Private-Network: true so Chrome lets the public app call into 127.0.0.1.

4. Command Policy

isSafeCommand in main.go is an allowlist-plus-denylist:

  • Blocks destructive patterns (rm -rf /, sudo, mkfs, dd, shell redirect-to-device, fork bombs).
  • Returns 403 with Security Policy Violation.
  • Policy is per-daemon and hardcoded — no remote override.

5. Local-Only Dashboard

GET / on 127.0.0.1:33120 serves dashboard_html.go without the bearer check because it runs on loopback and embeds the secret into its own <script> block at render time. It is not reachable through the SSH tunnel: the dashboard handler rejects any request whose Host is not 127.0.0.1 / localhost.

6. Auto-Restart Safety

autoRestart: true is gated by the restart supervisor:

  • Max 5 restarts in a 60-second window.
  • On breach, the agent is terminated with exit reason restart_limit and written to history.
  • Prevents fork-bomb-on-exit scenarios.

7. Secret Hygiene

  • Never call saveConfig on a blank Config{} — always loadConfig() first, mutate, then save. Overwriting loses the machine secret and bricks every paired dashboard.
  • Factory Reset in the dashboard clears only the browser's local state (Zustand persist). The daemon's secret is untouched; re-pair to reconnect.
  • RevenueCat App User ID = Firebase UID, so anonymous purchases can't carry Pro entitlements across accounts.