Rusty Red is an in-memory graph + vector database that runs as a single web service. This document describes the threat model, the default security posture, what an operator is responsible for, and how to report a vulnerability.
The shipped Dockerfile defaults to authentication required:
RUSTY_RED_REQUIRE_AUTH=true
RUSTY_RED_MCP_READ_ONLY=true
RUSTY_RED_MCP_ALLOW_ADMIN=false
RUSTY_RED_REQUIRE_VOLUME=true
In this posture:
/,/search,/search.json,/crawl,/federate/submit,/v1/*,/mcp, and/metricsreject requests that do not present a valid bearer token fromRUSTY_RED_API_TOKENS./health,/ready,/openapi.json,/.well-known/agent.json, and/.well-known/mcp/rustyred.jsonremain unauthenticated; they expose no tenant data or mutable surface.- MCP starts in read-only mode. Write tools are unreachable until the operator explicitly enables them.
- The service refuses to start without a persistent volume mounted
at
RUSTY_RED_DATA_DIR, so a misconfigured deploy fails loudly rather than running on ephemeral storage and silently losing data.
Operators who change any of these defaults are responsible for the resulting risk.
Authentication is bearer-token. Tokens are configured via the
RUSTY_RED_API_TOKENS environment variable as a comma-separated
list. Each entry has the form:
<secret>=<scope>|<scope>|...
<secret> is the bearer token presented by clients; <scope> is a
permission string. Pipes separate scopes within one entry, commas
separate entries.
Example (one admin token):
RUSTY_RED_API_TOKENS=6267f6...fed5=graph:read|graph:write|admin:read|run:read|run:write
Supported canonical scopes:
| Scope | Grants |
|---|---|
graph:read |
All read routes on the graph: /v1/query, /v1/cypher with non-mutating clauses, /v1/tenants/{id}/graph/nodes/{node_id} (GET), /v1/tenants/{id}/graph/stats, /v1/tenants/{id}/graph/vector/search, /v1/tenants/{id}/graph/fulltext/search, /v1/tenants/{id}/graph/spatial/radius, etc. |
graph:write |
All graph:read plus mutating routes: /v1/cypher with CREATE/MERGE/SET/DELETE, /v1/tenants/{id}/graph/nodes (POST), /v1/tenants/{id}/graph/edges (POST), /v1/tenants/{id}/graph/bulk/nodes, /v1/tenants/{id}/graph/bulk/edges, /v1/cache/put, /v1/cache/invalidate. |
context:read |
/v1/tenants/{id}/context/pack. |
admin:read |
/v1/tenants/{id}/graph/verify, /v1/tenants/{id}/graph/rebuild-indexes, /v1/diagnostics/config, and the MCP admin tool surface (only when RUSTY_RED_MCP_ALLOW_ADMIN=true). |
federation:write |
Submit signed RustyWeb Web Commons fragments to /federate/submit. |
run:read / run:write |
The legacy compat-server's run-lifecycle routes (/v1/runs/{id}, etc.). Only meaningful with the legacy server. |
Scope aliases are also accepted for backward compatibility:
rustyred:graph:read, rustyred:graph:query, rustyred:graph:index:read
all map to graph:read; rustyred:graph:write:propose and
rustyred:graph:write:apply both map to graph:write;
rustyred:graph:context maps to context:read;
rustyred:graph:admin:verify maps to admin:read;
rustyred:federation:write maps to federation:write.
A * scope in any entry grants all of the above and should be used
sparingly — operator emergency access, not application tokens.
Tokens are matched against the Authorization: Bearer <token> header
in HTTP requests and against the MCP auth parameter for the /mcp
endpoint. Token strings should be ≥ 32 bytes of cryptographically
random data; we recommend generating them with
openssl rand -hex 32. Rotate tokens by editing the env and
restarting the service — there is no in-band token rotation API.
Tokens do not carry per-tenant scope. A token with graph:write
can write to any tenant whose data lives in this Rusty Red instance.
Multi-tenant deployments that need per-tenant scoping should run
one Rusty Red instance per tenant or front the service with an
external auth layer that issues per-tenant signed requests.
Tenant isolation is enforced at the keyspace layer: all per-tenant
state is stored under keys prefixed with
{RUSTY_RED_KEY_PREFIX}:{tenant_id}:.... Routes under
/v1/tenants/{tenant_id}/... operate on that tenant's keyspace and
only that keyspace. There is no cross-tenant query API.
The tenant_id is operator-supplied; the service does not validate ownership. The auth layer described above gives the operator total access — tenant separation in Rusty Red is a data-organization boundary, not a trust boundary against an authenticated caller.
We treat the following as security issues and will respond to reports about them:
- Authentication bypass on protected search, crawl, federation,
/v1/*,/mcp, or metrics routes whenRUSTY_RED_REQUIRE_AUTH=true. - Cross-tenant data leakage via
/v1/tenants/{tenant_id}/...routes — a request scoped to onetenant_idreturning data belonging to a differenttenant_id. - Privilege escalation across scopes — a
readtoken executing a write or admin operation, awritetoken executing an admin operation. - MCP read-only bypass — a write tool reachable when
RUSTY_RED_MCP_READ_ONLY=true. - Memory-safety bugs in the Rust crates that lead to crashes, uninitialized reads, or out-of-bounds writes triggered by attacker-controlled input.
- Persistence-layer corruption triggered by valid input — i.e., an AOF or snapshot write path that produces a state the service cannot reload.
- Information disclosure via unauthenticated routes that goes
beyond intentional surface (
/health,/ready,/openapi.json,/.well-known/*are intentionally open and not considered leakage).
We do not currently treat the following as security issues; pull requests improving them are welcome but not under embargo:
- Denial of service via expensive Cypher queries, large bulk ingests, or unbounded HNSW searches. Operators are expected to rate-limit or quota at the ingress layer.
- Side-channel timing attacks against token comparison or HNSW search.
- Algorithmic complexity attacks against graph algorithms (PPR, PageRank, community detection) on adversarial inputs.
- Supply-chain attacks on transitive crate dependencies.
- The legacy
RUSTY_RED_MODE=rediscompatibility path. This mode exists only for migrating off older deployments and is not part of the recommended production posture. - The
/v1/tenants/{tenant_id}/graph/querydebug bridge. Use/v1/query,/v1/cypher, and/v1/cypher/explainfor the product surface.
These are not security issues if you ignore them; they are how you remain secure:
- Run with
RUSTY_RED_REQUIRE_AUTH=trueon any reachable endpoint. The Dockerfile default. Do not flip it back tofalseunless the service is on a private network with another trust layer in front. - Generate tokens cryptographically and rotate them.
openssl rand -hex 32per token. Rotate on operator changes and on suspected compromise. - Never bake tokens into a client or commit them to git. Inject via Railway environment variables or equivalent.
- Keep the volume backed up. The persistence layer is
AOF + snapshot. Snapshot is taken every
RUSTY_RED_SNAPSHOT_INTERVAL_WRITES; AOF replays the gap. Volume loss = data loss. - Pin a tagged release rather than
main.mainmay carry unreleased changes. Tagged releases are what receive security patches. - Watch
/metrics. Sudden auth-rejection spikes or unexpected write-rate growth are the first signal of an attempted compromise.
Email security@<your-domain> with:
- A description of the issue.
- Steps to reproduce, including the route, payload, and environment configuration.
- The Rusty Red version (
git rev-parse HEADor tag). - Any logs or output that demonstrates the problem.
If you prefer, file a private GitHub Security Advisory on the repository instead of email.
We will acknowledge receipt and provide a response with a disclosure timeline. We do not currently run a bug-bounty program; we will credit reporters in release notes unless asked to be anonymous.
| Version | Supported |
|---|---|
main |
Latest commit — receives all fixes. Not recommended for production. |
| Latest tagged release | Receives security fixes. |
| Older tagged releases | Best-effort; no guarantee. |
There is no LTS branch at this time. Operators should plan to track tagged releases.