Threat model¶
What Pluma defends against, what it doesn't, what's on you.
Assets¶
Ranked by sensitivity:
- Chat content — conversations may include personal context, drafts, internal documents.
- API keys — OpenAI, Anthropic, etc.; exfiltrated keys cost money + leak access.
- Character cards + personas — share-format by design (Tavern PNGs), but a leak still maps to user identity.
- Generated images — context-bound to chats; less sensitive than chat text but worth protecting.
- 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-keyfor 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 auth —
require_auth = falseis 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/cipherstandard 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/edwards25519chain).
No custom crypto. Every primitive is a stdlib or audited-library call.