|
| 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