Security model

spannora is a web app that gives Claude root access to your VM. That’s the feature, not a bug. This page explains the auth gate that stands between the internet and Claude, the rationale for running as root, and what you should know before exposing a spannora install publicly.

The threat model

A spannora instance is functionally a remote root shell with an LLM in front. If an attacker authenticates to the web UI, they can ask Claude to read any file on the VM, exfiltrate credentials, modify systemd units, or run arbitrary commands.

The defense is the auth gate. Treat it accordingly.

Authentication

Single user per install. spannora is a personal tool. There’s one account; no multi-tenant logic.

Both auth methods resolve through the same lookup, so all downstream code is auth-kind-agnostic. Cookies and bearer tokens are interchangeable from the server’s perspective.

Why the server runs as root

The systemd unit has no User= line. spannora’s process runs as root.

The use case is “ask Claude to do something on a VM.” Claude’s tools — Bash, Edit, Write — need filesystem access to do anything useful. Sandboxing to an unprivileged user defeats the purpose: you can’t apt install, can’t edit systemd units, can’t write /etc/, can’t read root-owned config.

So spannora runs as root, and the auth gate becomes the single line of defense. There’s no defense-in-depth via OS permissions because there’s nothing to defend.

Implication: do not expose a spannora install to the public internet without HTTPS and a strong password. Do not run spannora on a multi-tenant box. Treat the web UI like an ssh root@vm session.

HTTPS

The one-line installer provisions a Let’s Encrypt certificate automatically — Caddy handles it transparently, nginx via certbot. Cookies are marked Secure only when HTTPS is detected; over plain HTTP, sessions still work but cookies aren’t restricted to TLS connections (don’t run plain HTTP in production).

CORS

Cross-origin access is off by default. Set SPANNORA_ALLOWED_ORIGINS to opt in:

SPANNORA_ALLOWED_ORIGINS=https://spannora.dev,http://localhost:5173

When SPANNORA_ALLOWED_ORIGINS is unset, the server emits no CORS headers and behaves exactly like pre-CORS spannora — same-origin only.

What’s logged

No telemetry to third parties.

Retention

Old conversations are deleted by a retention job (packages/server/src/retention.ts). Default: 30 days. Removes both the SQLite rows and the matching JSONL files. Configure via env if needed.

What spannora doesn’t do

Checklist before exposing


Next: FAQ →