Threat model#
This is the threat model of the needtoknow library: what it protects, where the trust boundaries
are, what can go wrong at each, and what stops it. The example gateway in examples/gateway has
its own threat model, for a deployed service. Security model states the guarantees
and the invariants (S1 to S10) these threats refer to; Revocation and consistency
covers how quickly permission changes take effect.
What is protected#
Asset |
Why it matters |
|---|---|
Document chunks: text and metadata |
The confidential content itself. Metadata can name people, groups and projects. |
Embeddings |
Can be inverted to approximate the text they came from. |
Permission data: chunk ACLs, policies, relationship tuples |
Decide who reads what. |
Cached decisions and filters |
A decision that outlives its permission grants access after revocation. |
Subjects: user ids, groups, tenant and other attributes |
Who is asking. A wrong subject is a wrong answer. |
Queries |
Reveal what people look for; audit events must not store them. |
Credentials for stores, permission services and Redis |
Give direct access around needtoknow. |
Trust boundaries#
end user / agent ──▶ your application ──▶ needtoknow ──▶ vector store
(untrusted) authenticates, (in process) (trusted to search,
builds the Subject │ checked for filtering)
├──▶ permission service (OPA, OpenFGA, SpiceDB, Cerbos)
│ (source of truth for decisions)
├──▶ shared cache (Redis), trusted only when signed
├──▶ reranker, then the language model
│ (sees only permitted chunks)
└──▶ observers: audit log, OpenTelemetry
(see counts and ids, never content)
User or agent to your application. Untrusted. needtoknow does not authenticate anyone: your application builds the
Subjectfrom a verified session or token. A language model is on the untrusted side; it chooses what to search for, never whose permissions apply.needtoknow to the vector store. The store is trusted to search and to return each chunk’s metadata as stored. It is not trusted to apply the permission filter correctly: every pre-filtered hit is checked again.
needtoknow to the permission service. Trusted for its answers, never for their shape: a missing, partial, malformed or late answer fails closed.
needtoknow to the shared cache. Trusted with decisions only when values are signed.
needtoknow to the reranker and the model. They receive permitted chunks only, after the check.
Ingestion and permission sync. Outside the request path: whatever writes permissions onto chunks (
set_metadata, the Google Drive sync, your pipeline) is trusted, and its delay is a revocation delay.
Threats#
Severity is the impact if the threat were realised with no mitigation: critical (any user reads another tenant’s or another user’s confidential data at will), high (confidential data reaches someone not entitled to it under some conditions), medium (data hidden from those entitled to it, staleness within a stated bound, or metadata disclosure), low (hints, cost).
Id |
Threat |
Severity |
Mitigation |
Remaining risk |
Tests |
|---|---|---|---|---|---|
T1 |
Subject impersonation: the client or a model chooses whose permissions apply |
Critical |
No API takes the subject from the client. The MCP tool reads it from the verified access token; LangChain and LlamaIndex bind it per session. |
Your application must build the |
|
T2 |
The store ignores or mistranslates the filter |
High |
Every pre-filtered hit is checked again with the authorizer ( |
With |
S1, S5 |
T3 |
The permission service fails, times out or is unreachable |
High |
|
Availability: retrieval stops while the service is down. |
S2 |
T4 |
A malformed, partial or misordered answer: wrong count, non-boolean, an unknown permissionship, a truncated list |
High |
Decisions are validated for count and exact |
An authorizer that returns well-formed wrong answers is trusted (it is the source of truth). |
S2, S7 |
T5 |
Tenant crossover: shared user ids, group names or document ids, or a cached decision for another tenant |
Critical |
|
Resource ids in a shared OpenFGA or SpiceDB store must be tenant-qualified ( |
S3, S4 |
T6 |
Stale permissions from caching |
Medium |
|
Up to |
S9 |
T7 |
Stale permissions on the chunks (metadata ACLs) |
High |
|
Until the chunks are updated, the old permissions apply. |
Conformance kit ( |
T8 |
A shared cache written by someone else |
High |
|
Without a signing key, anyone who can write to that Redis can plant decisions. With one, an old signed entry put back can last until its own expiry, at most |
|
T9 |
Two differently configured authorizers sharing one cache namespace |
High |
Documented: one namespace per authorizer configuration. |
Not enforced in code; a shared Redis with the default namespace for two policies mixes their answers. |
|
T10 |
Injection through field names, values or query text into store queries |
High |
Values are bound as parameters or sent as hex-encoded terms; field names are validated; filter parsing is depth-limited. Fuzzed with ClusterFuzzLite. |
|
|
T11 |
Content the subject may not read reaches a reranker, the model, a tool or a callback |
High |
The reranker runs after the check and can only reorder, score or drop; its output is mapped back to the chunks it was given. The MCP tool returns no metadata unless named. |
Framework callbacks and traces (LangSmith, OpenTelemetry exporters you add) receive the permitted text; they are your systems. |
S6 |
T12 |
Disclosure through errors, logs and telemetry |
Medium |
Events carry ids and counts, never query or chunk text; the query only as an HMAC. Spans omit the subject unless asked. The MCP tool reports failures without their text. |
|
S10 |
T13 |
Decisions carried over between concurrent requests, threads or tasks |
High |
The retriever keeps no per-request state between calls; the cache is keyed by subject and action, and its updates are locked. |
S4 |
|
T14 |
Exhaustion: deeply nested filters, subjects in thousands of groups, a slow permission service |
Medium |
Filters nest at most 100 deep; pgvector uses array parameters above a threshold; timeouts, a circuit breaker and |
A subject in very many groups makes every query larger. |
|
T15 |
A compromised dependency or release |
High |
CI and the release install everything from |
The optional extras pull each store’s client and its dependencies. Advisories with no fixed release are listed with reasons in |
Scorecard, |
T16 |
An incomplete result presented as complete |
Medium |
|
An approximate index can miss permitted chunks without anyone knowing; tune the index and measure recall. |
S7 |
T17 |
Ranking statistics computed over chunks the subject may not read |
Medium |
Only permitted chunks are returned, whatever the ranking. Vector search scores each chunk on its own, and |
In keyword search ( |
Out of scope#
These are real risks for a RAG system; needtoknow does not address them:
Authentication. Verifying users, tokens and sessions.
The model’s output. A model can repeat, summarise or be steered into misusing a chunk the user was allowed to read.
Prompt injection in permitted content. A readable chunk can carry instructions for the model.
The permission policy itself. If the policy or the tuples grant too much, needtoknow enforces too much.
Infrastructure. TLS, network isolation and access control for the stores, the permission service and Redis.
Reviewing this model#
The model is reviewed with every change that adds a backend, an authorizer, a cache or an integration, and the invariants table in Security model is the checklist. It has been reviewed internally only: no independent security audit has been carried out yet.