Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Threat Model

Security is Quiver’s foundation, not a feature. This document states what we defend, against whom, and — crucially — what we honestly do not protect. Overclaiming would discredit a security-first project. Crypto mechanisms are in crypto.md; decisions in ADR-0010ADR-0014.

Assets

  • Vector data — embeddings can leak information about their source content (embedding-inversion attacks are real), so vectors are sensitive, not just metadata.
  • Payloads — arbitrary, often PII or business data.
  • Keys — master key, per-collection data-encryption keys (DEKs), API-key secrets.
  • Audit log integrity and service availability.

Adversaries

#AdversaryPrimary defense
A1Network attacker (MITM)TLS 1.3 (rustls); optional mTLS
A2Malicious / compromised clientAuthN (API key / mTLS), RBAC scopes, tenant isolation, query cost limits (ADR-0040)
A3Thief of disk / backups (data at rest)Envelope encryption-at-rest (AEAD); crypto-shredding
A4Curious / compromised server operator (payloads)Client-side payload encryption — server stores ciphertext it cannot read
A5Another tenantIsolation enforced at the data-access layer; default-deny RBAC
A6Curious / compromised server operator (vectors)DCPE (vector_encryption=dcpe, leaky, server ranks) or client-side opaque vectors (vector_encryption=client_side, semantically secure, server does not rank) — both opt-in, per collection
A6Supply-chain attackercargo deny/audit, minimal pinned deps, SBOM, gitleaks

Trust boundaries

flowchart LR
  c["Client app<br/>(+ client-side key)"] -- "TLS / mTLS" --> s
  subgraph host["Semi-trusted host"]
    s["Quiver server process<br/>(plaintext vectors in RAM)"] -- "AEAD pages" --> d[("Disk: ciphertext")]
    s -. "wrap/unwrap DEK" .-> k["KMS (optional)"]
  end
  1. Client ↔ Server (network). TLS 1.3 always for non-loopback; optional mTLS. The server authenticates the client, authorizes the request scope, and scopes all data access to the tenant.
  2. Server ↔ Disk. The filesystem is semi-trusted: everything at rest is AEAD-encrypted, so a stolen disk or backup yields only ciphertext.
  3. Server ↔ KMS (optional). The master key may live in a KMS; plaintext DEKs exist only in server RAM and are zeroized after use.
  4. Client ↔ Server for payloads (optional client-side encryption). When enabled, the server is untrusted for payload confidentiality — the trust boundary moves to the client, which encrypts payloads the server can only store and return as opaque blobs.

What the server can and cannot see — stated honestly

Without client-side encryption: to build and search an ANN index, the server necessarily holds vectors and payloads in plaintext in RAM while serving. At rest they are encrypted. Therefore at-rest encryption defends against A3 (stolen disk/backup) — it does not defend against an adversary with root on the live host who can read process memory. That residual risk is documented, not hidden.

With client-side payload encryption: the server never sees payload plaintext, even in RAM. But vectors remain plaintext server-side because standard ANN math requires them — and vectors can leak information about their source. So:

Client-side payload encryption protects payloads, not vectors. Confidentiality of vectors against the server is not provided by default. Two opt-in, per-collection modes address it, at opposite ends of the spectrum. DCPE (vector_encryption = dcpe) — a published distance-comparison-preserving scheme (ADR-0031, dcpe.md) — lets the server keep ranking ciphertexts but by design leaks the approximate distance-comparison relation, so it is not semantically secure and is broken by known-plaintext or strong-prior adversaries (its v2 hardening — a key-derived component shuffle and a global normalisation, ADR-0035 — hides axis alignment and canonicalises scale but does not change this leakage class). Client-side opaque vectors (vector_encryption = client_side) — XChaCha20-Poly1305 AEAD (ADR-0032, client-side-vectors.md) — is genuinely semantically secure (the server holds only ciphertext and never ranks), at the cost that the client fetches the entitled set and ranks locally. Quiver does not claim homomorphic-encrypted search in core, and never ships a home-grown scheme.

This precise boundary is the honest core of the security story.

The most recent code-level review of these controls — extended to cluster mode, the coordinator, per-shard Raft, and a dynamic OWASP ZAP pass — is the v0.29.0 audit note; the prior pass (migration-connector SSRF posture and a cleartext-credential fix) is the v0.17.0 audit note.

STRIDE summary

  • Spoofing → API-key/mTLS authentication; keys hashed at rest, shown once.
  • Tampering → AEAD integrity on every page; append-only audit log (optionally hash-chained).
  • Repudiation → audit log records actor, action, resource, time.
  • Information disclosure → the encryption layers above; tenant isolation; sanitized errors (no internal paths/secrets); secrets never logged.
  • Denial of service → query cost limits enforced at the op layer (caps on k, ef_search, fetch limit, vector dimension, payload size, upsert batch size, and HTTP request body size — ADR-0040), rejected with 400 / InvalidArgument so one oversized request cannot exhaust the single-writer engine; plus opt-in per-key rate limiting (ADR-0049, token bucket, 429). The rate limiter is post-authentication by design: it is keyed by the caller’s authenticated actor identity and therefore holds at most one bucket per configured key, so it cannot itself be turned into a memory-exhaustion vector by an attacker minting arbitrary source identities. Consequently it does not throttle unauthenticated traffic (a flood of anonymous requests that all fail auth) — that is the job of an upstream reverse proxy / load balancer / WAF, which every production deployment should terminate TLS and rate-limit at (see the deployment docs). A coarse pre-auth per-source limiter would reintroduce an unbounded-by-source map and is deliberately left to that layer. Deferred (stated, not claimed): concurrent-query caps and a work-cancelling query timeout (not achievable under the current spawn_blocking model without cooperative cancellation).
  • Elevation of privilege → default-deny RBAC scopes; tenant isolation at the data layer; no anonymous writes; no default credentials. In cluster mode the coordinator’s membership API is authenticated on the same footing: reshaping the cluster (POST/DELETE /cluster/shards*) requires an admin key and reading the map requires any valid key, so a network-reachable coordinator cannot be reshaped by an unauthenticated caller (only /healthz//readyz are open; a keyless coordinator refuses to start unless insecure).

Crypto-shredding

Per-collection DEKs make cryptographic erasure a first-class operation: destroy a collection’s wrapped DEK and its at-rest data — including any backups — becomes unrecoverable, satisfying “right to erasure” without hunting down every copy.

Verification

Fuzzing of the wire-protocol and on-disk parsers (cargo-fuzz targets for the Filter JSON parser and the page/WAL decoders — malformed input must reject cleanly, never panic); cargo audit/deny; tests asserting (a) data files are ciphertext, (b) a client-side-encrypted payload is unreadable server-side, (c) RBAC denies cross-tenant/over-scope access, (d) a crypto-shredded collection is unrecoverable, (e) the audit log records actor/action/resource without leaking secrets, (f) a DCPE-encrypted query returns the right neighbour while the plaintext vector never reaches disk (the scoped ADR-0031 guarantee), and (g) a client-side-encrypted collection rejects a ranked query and never writes the plaintext vectors to disk while the client still recovers the right neighbour (the ADR-0032 guarantee). Tracked under risks R3/R4/R8 in ../risk-register.md.