Skip to content

Commit bc3037b

Browse files
docs(security): add SECURITY.md covering threat model, auth, reporting
1 parent 97244eb commit bc3037b

1 file changed

Lines changed: 172 additions & 0 deletions

File tree

SECURITY.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Security Policy
2+
3+
Rusty Red is an in-memory graph + vector database that runs as a
4+
single web service. This document describes the threat model, the
5+
default security posture, what an operator is responsible for, and
6+
how to report a vulnerability.
7+
8+
## Default posture
9+
10+
The shipped Dockerfile defaults to **authentication required**:
11+
12+
```
13+
RUSTY_RED_REQUIRE_AUTH=true
14+
RUSTY_RED_MCP_READ_ONLY=true
15+
RUSTY_RED_MCP_ALLOW_ADMIN=false
16+
RUSTY_RED_REQUIRE_VOLUME=true
17+
```
18+
19+
In this posture:
20+
21+
- `/v1/*` and `/mcp` reject requests that do not present a valid
22+
bearer token from `RUSTY_RED_API_TOKENS`.
23+
- `/health`, `/ready`, `/openapi.json`, `/.well-known/agent.json`,
24+
`/.well-known/mcp/thg.json`, and `/metrics` remain unauthenticated;
25+
they expose no tenant data or mutable surface.
26+
- MCP starts in read-only mode. Write tools are unreachable until the
27+
operator explicitly enables them.
28+
- The service refuses to start without a persistent volume mounted
29+
at `RUSTY_RED_DATA_DIR`, so a misconfigured deploy fails loudly
30+
rather than running on ephemeral storage and silently losing data.
31+
32+
Operators who change any of these defaults are responsible for the
33+
resulting risk.
34+
35+
## Authentication model
36+
37+
Authentication is bearer-token. Tokens are configured via the
38+
`RUSTY_RED_API_TOKENS` environment variable as a comma-separated
39+
list, each entry of the form `<token>:<scope>[,<scope>...]` where
40+
the supported scopes are:
41+
42+
| Scope | Grants |
43+
|---|---|
44+
| `read` | All `GET` routes and read-only `POST` queries (`/v1/query`, `/v1/cypher` with non-mutating clauses, `/v1/cache/get`, etc.). |
45+
| `write` | All `read` plus mutating routes (`/v1/cypher` with `CREATE`/`MERGE`/`SET`/`DELETE`, `/v1/tenants/{id}/graph/nodes`, bulk ingest, etc.). |
46+
| `admin` | All `write` plus `/v1/tenants/{id}/graph/rebuild-indexes`, `/v1/tenants/{id}/graph/verify`, and the MCP admin tool surface (only when `RUSTY_RED_MCP_ALLOW_ADMIN=true`). |
47+
48+
Tokens are matched against the `Authorization: Bearer <token>` header
49+
in HTTP requests and against the MCP `auth` parameter for the `/mcp`
50+
endpoint. Token strings should be ≥ 32 bytes of cryptographically
51+
random data; we recommend generating them with
52+
`openssl rand -hex 32`. Rotate tokens by editing the env and
53+
restarting the service — there is no in-band token rotation API.
54+
55+
Tokens **do not** carry per-tenant scope. A token with `write` can
56+
write to any tenant whose data lives in this Rusty Red instance.
57+
Multi-tenant deployments that need per-tenant scoping should run
58+
one Rusty Red instance per tenant or front the service with an
59+
external auth layer that issues per-tenant signed requests.
60+
61+
## Tenancy isolation
62+
63+
Tenant isolation is enforced at the keyspace layer: all per-tenant
64+
state is stored under keys prefixed with
65+
`{RUSTY_RED_KEY_PREFIX}:{tenant_id}:...`. Routes under
66+
`/v1/tenants/{tenant_id}/...` operate on that tenant's keyspace and
67+
only that keyspace. There is no cross-tenant query API.
68+
69+
The tenant_id is operator-supplied; the service does not validate
70+
ownership. The auth layer described above gives the operator total
71+
access — tenant separation in Rusty Red is a data-organization
72+
boundary, not a trust boundary against an authenticated caller.
73+
74+
## What is in scope
75+
76+
We treat the following as security issues and will respond to
77+
reports about them:
78+
79+
- Authentication bypass on `/v1/*` or `/mcp` when
80+
`RUSTY_RED_REQUIRE_AUTH=true`.
81+
- Cross-tenant data leakage via `/v1/tenants/{tenant_id}/...`
82+
routes — a request scoped to one `tenant_id` returning data
83+
belonging to a different `tenant_id`.
84+
- Privilege escalation across scopes — a `read` token executing
85+
a write or admin operation, a `write` token executing an admin
86+
operation.
87+
- MCP read-only bypass — a write tool reachable when
88+
`RUSTY_RED_MCP_READ_ONLY=true`.
89+
- Memory-safety bugs in the Rust crates that lead to crashes,
90+
uninitialized reads, or out-of-bounds writes triggered by
91+
attacker-controlled input.
92+
- Persistence-layer corruption triggered by valid input — i.e., an
93+
AOF or snapshot write path that produces a state the service
94+
cannot reload.
95+
- Information disclosure via unauthenticated routes that goes
96+
beyond intentional surface (`/health`, `/ready`, `/openapi.json`,
97+
`/metrics`, `/.well-known/*` are intentionally open and not
98+
considered leakage).
99+
100+
## What is out of scope (for now)
101+
102+
We do not currently treat the following as security issues; pull
103+
requests improving them are welcome but not under embargo:
104+
105+
- Denial of service via expensive Cypher queries, large bulk
106+
ingests, or unbounded HNSW searches. Operators are expected to
107+
rate-limit or quota at the ingress layer.
108+
- Side-channel timing attacks against token comparison or
109+
HNSW search.
110+
- Algorithmic complexity attacks against graph algorithms (PPR,
111+
PageRank, community detection) on adversarial inputs.
112+
- Supply-chain attacks on transitive crate dependencies.
113+
- The legacy `RUSTY_RED_MODE=redis` compatibility path. This mode
114+
exists only for migrating off older deployments and is not part
115+
of the recommended production posture.
116+
- The `/v1/tenants/{tenant_id}/graph/query` debug bridge. Use
117+
`/v1/query`, `/v1/cypher`, and `/v1/cypher/explain` for the
118+
product surface.
119+
120+
## Operator responsibilities
121+
122+
These are not security issues if you ignore them; they are how
123+
you remain secure:
124+
125+
1. **Run with `RUSTY_RED_REQUIRE_AUTH=true` on any reachable
126+
endpoint.** The Dockerfile default. Do not flip it back to
127+
`false` unless the service is on a private network with
128+
another trust layer in front.
129+
2. **Generate tokens cryptographically and rotate them.**
130+
`openssl rand -hex 32` per token. Rotate on operator changes
131+
and on suspected compromise.
132+
3. **Never bake tokens into a client or commit them to git.**
133+
Inject via Railway environment variables or equivalent.
134+
4. **Keep the volume backed up.** The persistence layer is
135+
AOF + snapshot. Snapshot is taken every
136+
`RUSTY_RED_SNAPSHOT_INTERVAL_WRITES`; AOF replays the gap.
137+
Volume loss = data loss.
138+
5. **Pin a tagged release rather than `main`.** `main` may carry
139+
unreleased changes. Tagged releases are what receive security
140+
patches.
141+
6. **Watch `/metrics`.** Sudden auth-rejection spikes or
142+
unexpected write-rate growth are the first signal of an
143+
attempted compromise.
144+
145+
## Reporting a vulnerability
146+
147+
Email **security@\<your-domain\>** with:
148+
149+
- A description of the issue.
150+
- Steps to reproduce, including the route, payload, and
151+
environment configuration.
152+
- The Rusty Red version (`git rev-parse HEAD` or tag).
153+
- Any logs or output that demonstrates the problem.
154+
155+
If you prefer, file a private GitHub Security Advisory on the
156+
repository instead of email.
157+
158+
We will acknowledge receipt and provide a response with a
159+
disclosure timeline. We do not currently run a bug-bounty program;
160+
we will credit reporters in release notes unless asked to be
161+
anonymous.
162+
163+
## Supported versions
164+
165+
| Version | Supported |
166+
|---|---|
167+
| `main` | Latest commit — receives all fixes. Not recommended for production. |
168+
| Latest tagged release | Receives security fixes. |
169+
| Older tagged releases | Best-effort; no guarantee. |
170+
171+
There is no LTS branch at this time. Operators should plan to
172+
track tagged releases.

0 commit comments

Comments
 (0)