Security & Authentication
This guide covers the SochDB v2 server security model: how to turn authentication on, the RBAC roles and capabilities, how clients present credentials (JWT or API key) over gRPC, per-namespace policy, mTLS, Kubernetes secrets, rate limiting, and audit logging.
All of this lives in the sochdb-grpc-server binary (crate sochdb-grpc). For
the binary's full flag list, ports, and operational setup, see
Deploying to Production.
This page targets core engine 2.0.3 (the sochdb-grpc-server binary). The
language SDKs version independently (Python 0.5.9, Node.js 0.5.3, Go 0.4.5).
The core engine (Rust workspace, the sochdb crate, the gRPC server, and
the MCP server) is AGPL-3.0-or-later, with commercial licensing available.
The language SDKs (Python, Node.js, Go) are Apache-2.0.
Authentication is off by default
The server starts without authentication. When you do not pass --auth,
the auth interceptor runs in pass-through mode and every request resolves to an
anonymous principal with Read, Write, and ManageCollections
capabilities.
# No auth: every caller is the anonymous principal (Read + Write + ManageCollections)
sochdb-grpc-server --host 127.0.0.1 --port 50051
This is convenient for local development but means anyone who can reach the port can read and write data and manage collections. Turn auth on for any non-loopback deployment:
# Auth enabled: both JWT and API-key checking are turned on
sochdb-grpc-server --host 0.0.0.0 --port 50051 --auth
When --auth is passed, the server enables both JWT verification and
API-key checking, and installs the auth interceptor on every service.
Of the 12 gRPC services, VectorIndexService is the one service registered
without the auth interceptor. It is also the only service raised to a 64 MB
max message size (others use the tonic 4 MB default). If you enable --auth,
be aware the vector index API remains reachable without credentials. Treat the
gRPC port as sensitive and protect it at the network layer (firewall, mTLS, or a
private subnet) regardless of --auth.
The standard gRPC health service (used by Kubernetes probes) is also deliberately left unauthenticated.
RBAC: roles and capabilities
A successful authentication produces a Principal, which carries a set of
capabilities. Capabilities are granted through a role, and roles can be
attached to the principal directly (via a JWT claim) or bound server-side per
namespace.
Roles
There are three built-in roles plus a custom variant:
| Role | Capabilities |
|---|---|
Owner | Admin, Read, Write, ManageCollections, ManageIndexes, ViewMetrics, ManageBackups, ManageUsers (all) |
Editor | Read, Write, ManageCollections, ManageIndexes |
Viewer | Read, ViewMetrics |
Custom { name, capabilities } | An explicit capability list you supply |
The roles are Owner / Editor / Viewer — not Admin / ReadWrite /
ReadOnly. Use these exact names in JWT role claims and role bindings.
Capabilities
The full capability set is:
Admin # wildcard — satisfies every capability check
Read
Write
ManageCollections
ManageIndexes
ViewMetrics
ManageBackups
ManageUsers
Custom(String) # named capability for custom roles
The Admin capability is a wildcard: a principal holding Admin passes any
capability check. That is why Owner (which includes Admin) effectively has
unrestricted access.
How clients present credentials over gRPC
Credentials travel as gRPC metadata headers. Two header forms are accepted:
authorization: Bearer <token>— the preferred form.<token>is interpreted as a JWT when JWT verification is enabled, otherwise as an API key.x-api-key: <key>— a fallback. Internally it is rewritten toBearer <key>before authentication runs.
If no recognized header is present, the request is rejected with
Unauthenticated.
x-api-key is unreachable when JWT is enabledWhen you pass --auth, the server enables JWT verification and API-key
checking at the same time. With JWT verification on, the authenticate path
routes every Bearer token (including one promoted from x-api-key) to the
JWT verifier and never falls through to the API-key hash lookup. So a bare
--api-key is registered but is only actually reachable when JWT verification
is disabled. If you intend to authenticate with API keys, do not also supply a
JWT secret, or use JWTs as your primary credential.
The interceptor pipeline for an authenticated request is:
authenticate (JWT or API key)
-> check_rate_limit (per tenant)
-> inject Principal into request extensions
-> handler reads it via extract_principal(&request)
gRPC metadata examples
- grpcurl
- Python
- Go
- Node.js
# Bearer JWT (preferred)
grpcurl -H "authorization: Bearer ${SOCHDB_JWT}" \
-plaintext 127.0.0.1:50051 sochdb.v1.KvService/Get
# x-api-key fallback (only works when JWT verification is OFF)
grpcurl -H "x-api-key: ${SOCHDB_API_KEY}" \
-plaintext 127.0.0.1:50051 sochdb.v1.KvService/Get
import grpc
# Attach the Bearer token as call metadata on every request.
metadata = (("authorization", f"Bearer {jwt_token}"),)
channel = grpc.insecure_channel("127.0.0.1:50051")
# stub = KvServiceStub(channel)
# stub.Get(request, metadata=metadata)
// Per-RPC metadata: authorization header carrying the Bearer token.
import "google.golang.org/grpc/metadata"
ctx := metadata.AppendToOutgoingContext(
context.Background(),
"authorization", "Bearer "+jwtToken,
)
// resp, err := client.Get(ctx, req)
import * as grpc from "@grpc/grpc-js";
const metadata = new grpc.Metadata();
metadata.set("authorization", `Bearer ${jwtToken}`);
// client.get(request, metadata, (err, resp) => { ... });
JWT authentication (validation only)
The server validates JWTs; it does not issue them.
- Verification uses
jsonwebtokenwith the default validation profile, which is HS256 (HMAC-SHA256). It validatesexp, and optionallyissandaudwhen an issuer/audience is configured. - The verification secret is loaded from the
jwt-secretKubernetes secret or theSOCHDB_JWT_SECRETenvironment variable.
There is no token-issuance API in the server. JWTs must be minted by your identity provider (or by your own service) and signed with the same HS256 secret the server validates against. The server only checks signatures and claims; it never hands out tokens.
Claims the server reads
A validated token produces a Principal whose authentication method is
JwtBearer. The claims are:
{
"sub": "user-123",
"exp": 1760000000,
"iat": 1759990000,
"iss": "https://idp.example.com",
"aud": "sochdb",
"tenant_id": "team-acme",
"role": "Editor",
"capabilities": ["Read", "Write"]
}
roleis one ofOwner/Editor/Viewer. Its capabilities are merged with any explicitcapabilitieslist.tenant_idscopes the principal to a namespace/tenant for per-namespace access checks (see below) and rate limiting.
Configuring the secret
export SOCHDB_JWT_SECRET="a-long-random-hs256-secret-shared-with-your-idp"
sochdb-grpc-server --host 0.0.0.0 --port 50051 --auth
A minimal example of minting a compatible token externally (Python; do this in your IdP/auth service, not in the database):
import time
import jwt # PyJWT
claims = {
"sub": "user-123",
"tenant_id": "team-acme",
"role": "Editor",
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
}
token = jwt.encode(claims, "a-long-random-hs256-secret-shared-with-your-idp", algorithm="HS256")
API keys
When API-key authentication is the active path (JWT verification disabled), keys
are presented via the x-api-key header (or as a Bearer token).
Registering a key
# Register a single API key at startup (requires --auth)
SOCHDB_API_KEY="my-secret-key" sochdb-grpc-server --host 0.0.0.0 --auth
# or pass it as a flag:
sochdb-grpc-server --host 0.0.0.0 --auth --api-key "my-secret-key"
Multiple keys can be supplied through the SOCHDB_API_KEYS environment variable
(comma-separated) or the api-keys Kubernetes secret (newline-delimited). Keys
loaded from secrets are granted Editor capabilities and the default
tenant.
How keys are stored
API keys are never stored in plaintext. Each key is stored as SHA-256(key)
by default, or as HMAC-SHA256(pepper, key) when a pepper is configured via
the SOCHDB_API_KEY_PEPPER environment variable. This is not argon2 —
argon2 is used only for hashing user passwords, never API keys.
export SOCHDB_API_KEY_PEPPER="a-secret-pepper-value"
export SOCHDB_API_KEY="my-secret-key"
sochdb-grpc-server --host 0.0.0.0 --auth
Remember the precedence caveat above: with --auth, JWT verification is on, so
the x-api-key path is only reachable if you do not also provide a JWT
secret.
Per-namespace policy (role bindings)
Beyond the role carried on a principal, the server supports role bindings that grant a role to a principal within a scope:
RoleBinding { principal_id, role, scope }
RoleScope::Global
RoleScope::Namespace(name)
RoleScope::Collection { namespace, collection }
Bindings are attached server-side and resolved per request: the effective capabilities for a principal in a given namespace are the union of all bindings whose scope applies to that namespace.
Namespace access is gated as follows:
- A principal holding
Adminmay access any namespace. - Other principals must match their
tenant_idto the requested namespace, or the namespace must bedefault(or empty).
This lets you, for example, give a principal Editor on the analytics
namespace while leaving it Viewer everywhere else.
mTLS / TLS
Transport security is enabled by supplying a certificate and key. Adding a CA turns on mutual TLS (client-certificate verification).
| Flag | Env var | Effect |
|---|---|---|
--tls-cert | SOCHDB_TLS_CERT | Server cert PEM path (enables TLS when paired with the key) |
--tls-key | SOCHDB_TLS_KEY | Server private key PEM path |
--tls-ca | SOCHDB_TLS_CA | CA cert; presence enables mTLS client-cert verification |
# TLS only (server-authenticated)
sochdb-grpc-server --host 0.0.0.0 --auth \
--tls-cert /etc/sochdb/tls/server.crt \
--tls-key /etc/sochdb/tls/server.key
# Mutual TLS (clients must present a cert signed by the CA)
sochdb-grpc-server --host 0.0.0.0 --auth \
--tls-cert /etc/sochdb/tls/server.crt \
--tls-key /etc/sochdb/tls/server.key \
--tls-ca /etc/sochdb/tls/ca.crt
The TLS provider supports hot-reload: it watches the cert/key files by modification time and reloads them when they change, so you can rotate certificates without restarting the server.
Secrets provider (Kubernetes)
The server can source its secrets from a mounted Kubernetes secret directory or from environment variables, with periodic auto-refresh.
-
--secrets-path(envSOCHDB_SECRETS_PATH) points at a mounted secret directory. Files are refreshed roughly every 30s; the env-var path refreshes roughly every 60s. -
Recognized secret file names in the mount:
jwt-secret # HS256 verification secret
api-keys # newline-delimited API keys
encryption-key # base64, 32 bytes (at-rest encryption key)
tls-cert
tls-key
tls-ca -
Environment-variable overrides:
SOCHDB_JWT_SECRET,SOCHDB_API_KEYS,SOCHDB_ENCRYPTION_KEY.
API keys loaded from the secrets provider are granted Editor capabilities
and the default tenant.
# Mount a Kubernetes secret and let the server read jwt-secret, api-keys, tls-* from it
sochdb-grpc-server --host 0.0.0.0 --auth \
--secrets-path /etc/sochdb/secrets
The engine ships an AES-256-GCM-SIV encryption engine (32-byte key,
nonce-misuse-resistant AEAD) and the secrets provider can load an
encryption-key. However, sochdb-grpc-server has no CLI flag that turns
at-rest encryption on, and the binary's main path does not construct an
encryption engine. Treat at-rest encryption as an available library capability
with server wiring still to come, not as a runtime toggle today.
Rate limiting
The auth interceptor enforces a per-tenant rate limit on every authenticated request:
- 1000 requests/second sustained
- burst of 100
Tenancy is taken from the principal's tenant_id. Requests over the limit are
rejected by the interceptor. These are the built-in defaults; there is no CLI
flag to change them in the current binary.
Audit logging
Audit logging is on by default. Authentication and authorization decisions are recorded so you have a trail of who did what. No flag is required to enable it.
Quick reference
| Concern | Flag / Env | Notes |
|---|---|---|
| Enable auth | --auth | Turns on JWT + API-key checking |
| JWT secret | SOCHDB_JWT_SECRET / jwt-secret | HS256, validation only |
| API key | --api-key / SOCHDB_API_KEY | SHA-256 hashed |
| API keys (many) | SOCHDB_API_KEYS / api-keys | Comma- / newline-delimited |
| API-key pepper | SOCHDB_API_KEY_PEPPER | Switches hashing to HMAC-SHA256 |
| TLS | --tls-cert / --tls-key | PEM paths; hot-reload |
| mTLS | --tls-ca | Adds client-cert verification |
| Secrets mount | --secrets-path / SOCHDB_SECRETS_PATH | K8s secret directory |
| At-rest key | SOCHDB_ENCRYPTION_KEY / encryption-key | Library API; not wired to a server flag |
The PostgreSQL wire-protocol port (--pg-port, default 5433) has no auth
layer: it is simple-query only, cleartext, and trust-auth. It also needs
--pg-data-dir to run real SQL; without it you get an echo placeholder. Keep it
loopback-only unless you have accepted that risk, and never expose it on a
non-loopback --host. See Deploying to Production
for the full port matrix.