gRPC Security Model
August 9, 2026 · View on GitHub
The gRPC surface (grpc feature, src/grpc.rs) maps the MCP tools to protobuf
RPCs on the perseus_vault.v1 service. It is off by default and, like the HTTP
transport, is a remote surface reachable by attacker profile A4 the moment
it binds beyond loopback. This document is the security design; it is
implemented in serve_with and enforced whether or not individual RPC handlers
are filled in yet (many are still unimplemented!).
Threat model
Same as THREAT-MODEL.md: the local machine is the trust boundary and stdio is the trusted path. A network-exposed gRPC endpoint moves the boundary to the network, so it must provide transport confidentiality, authentication, and resource bounds before it carries real traffic.
Controls (implemented)
All configured via environment; defaults are "off" so behavior is unchanged until an operator opts in — with one exception: the secure-bind guard is always on.
| Control | Mechanism | Env |
|---|---|---|
| Secure-bind guard | Refuses to serve on a non-loopback address without either an auth token or mTLS. Mirrors the HTTP guard_bind policy. | PERSEUS_VAULT_ALLOW_INSECURE_BIND=1 to override (trusted network only) |
| Authentication | AuthInterceptor requires authorization: Bearer <token> metadata on every RPC; constant-time compared; rejects with UNAUTHENTICATED. | PERSEUS_VAULT_GRPC_AUTH_TOKEN |
| TLS | ServerTlsConfig with a PEM server identity. | PERSEUS_VAULT_GRPC_TLS_CERT, PERSEUS_VAULT_GRPC_TLS_KEY |
| Mutual TLS | Adds a client-CA root; clients must present a cert chaining to it. Counts as authentication for the bind guard. | PERSEUS_VAULT_GRPC_TLS_CLIENT_CA |
| Message-size cap | max_decoding_message_size / max_encoding_message_size bound per-message memory. | PERSEUS_VAULT_GRPC_MAX_MSG_BYTES (default 4 MiB) |
| Error hygiene | Internal error text (SQLite constraints, paths) is logged server-side and genericized to INTERNAL before reaching clients (sanitize_error). | — |
Example: authenticated + mTLS
PERSEUS_VAULT_GRPC_AUTH_TOKEN="$(openssl rand -hex 32)" \
PERSEUS_VAULT_GRPC_TLS_CERT=/etc/perseus/server.crt \
PERSEUS_VAULT_GRPC_TLS_KEY=/etc/perseus/server.key \
PERSEUS_VAULT_GRPC_TLS_CLIENT_CA=/etc/perseus/client-ca.crt \
perseus-vault --db /path/to/perseus-vault.db # (once a --grpc flag is wired)
Deferred / future work
- Per-method authorization (read vs. write). Today auth is all-or-nothing.
A future step is a scope/role model so a token can be granted read-only
(
recall,get_entity,stats) without write (remember,forget,purge). The interceptor is the natural enforcement point. - Per-client rate limiting. The HTTP surface has a global token bucket; gRPC
currently relies on the message-size cap plus a fronting proxy. A tonic layer
(e.g.
towerload-shed/concurrency-limit) can be added when the RPCs are implemented and traffic patterns are known. - Wiring
serveinto the CLI.serve/serve_withexist and are secured, but there is no--grpcflag yet; the endpoint is not startable from the CLI until the handlers are implemented. When that lands, add the flag and route it through the sameguard_bind-style checks.
Rationale for "secure by default, opt-in features"
TLS and auth default to off because the common case is a loopback developer setup where they add friction with no benefit. The bind guard is the backstop: it makes the one dangerous combination — exposed, unauthenticated, plaintext — fail loudly instead of silently, so an operator can't accidentally publish an open endpoint.