Skip to content

Threat model

What Pluma defends against, what it doesn't, what's on you.

Assets

Ranked by sensitivity:

  1. Chat content — conversations may include personal context, drafts, internal documents.
  2. API keys — OpenAI, Anthropic, etc.; exfiltrated keys cost money + leak access.
  3. Character cards + personas — share-format by design (Tavern PNGs), but a leak still maps to user identity.
  4. Generated images — context-bound to chats; less sensitive than chat text but worth protecting.
  5. Server config + connection metadata — base URLs, hostnames, network topology.

Defended

Threat Defence
Casual filesystem snooping Chat AES-256-GCM at rest, key in OS keyring. Filesystem-level access without keyring access yields ciphertext.
API-key leak via backup tarball Keys live in OS keyring, not on disk. A tarball of <datadir> has no usable secrets.
Unauthenticated /api/* from other devices on the LAN WebAuthn passkey gate (require_auth = true) + host allowlist.
Phishing WebAuthn is origin-bound by design; a phishing site at pluma-evil.example.com can't replay your tailnet credential.
SSRF via user-supplied URLs (character imports, voice URL imports) SSRF guard refuses to dial loopback / private / link-local / cloud-metadata addresses; bounded download size + timeout.
Spoofed X-Forwarded-For from an untrusted peer XFF only honoured when the immediate peer sits in a trusted CIDR; rightmost-untrusted walk inside the trusted segment.
Memory exhaustion via giant request bodies http.MaxBytesReader per handler. Caps documented in Storage & encryption.
Cookie hijacking / replay __Host- cookie prefix on HTTPS, HttpOnly + SameSite=Lax, signed with a per-install session key.
Brute-force passkey enrollment First-per-RPID grace closes the moment a passkey enrols; subsequent enrol attempts go through the standard authenticated flow.
HTTPS downgrade HSTS header on HTTPS responses (max-age=31536000; includeSubDomains).

Not defended (yet)

Gap Tracking
No Content-Security-Policy header smelt-sze — bundle this with the shareable-themes validator before user-supplied themes can be data.
No code signing on release binaries smelt-37b covers Windows MSI + Authenticode; Apple notarisation untracked. SmartScreen / Gatekeeper warnings on first run are expected.
No SBOM publication on release Untracked. The Go binary is reproducible from the tagged source.
No rate-limit middleware Untracked. Pluma's blast radius is small (single-user local-first); rate-limit lives at the proxy layer if you front it with one.
No FE-side encrypted-conversation cache smelt-0wc. Browser localStorage / IndexedDB holds nothing encrypted today; chat list re-fetches on mount.

User responsibility

Pluma can't help with these:

  • Lost OS-keyring access — encrypted chats become unreadable. Use pluma -export-storage-key for backup.
  • Sharing your character PNGs in public places — they're share-format on purpose; treat them as public artifacts.
  • Exposing Pluma to the open internet without authrequire_auth = false is for dev only; never on a machine that's reachable beyond your trust boundary.
  • Trusting a malicious LLM upstream — the upstream sees every prompt you send, including chat history. Pick providers you'd trust with the content.
  • Running unverified plugin binaries (once the plugin axis lands) — same as any out-of-tree extension. Audit before installing.

Out of scope

  • Multi-tenancy. Pluma is single-user. No per-user data isolation, no role/permission system. Don't deploy a shared instance.
  • Compliance frameworks. No SOC 2 / HIPAA / GDPR controls beyond "the data stays on your hardware unless you tell it not to." Cloud LLM providers ship your prompts off-machine.
  • Forensic logging. Access log redacts content; no audit trail beyond that. Compliance-grade audit isn't a goal.

Cryptography

  • AES-256-GCM for at-rest encryption (Go crypto/aes + crypto/cipher standard library).
  • WebAuthn via go-webauthn/webauthn. ES256 + RS256 algorithms supported.
  • Session-cookie signing via HMAC-SHA256 with a per-install key.
  • Passphrase wrap for storage-key export via age (filippo.io/edwards25519 chain).

No custom crypto. Every primitive is a stdlib or audited-library call.