Skip to main content

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.

Versions

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

License

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.

VectorIndexService is not behind auth

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:

RoleCapabilities
OwnerAdmin, Read, Write, ManageCollections, ManageIndexes, ViewMetrics, ManageBackups, ManageUsers (all)
EditorRead, Write, ManageCollections, ManageIndexes
ViewerRead, ViewMetrics
Custom { name, capabilities }An explicit capability list you supply
Role names

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 to Bearer <key> before authentication runs.

If no recognized header is present, the request is rejected with Unauthenticated.

x-api-key is unreachable when JWT is enabled

When 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

# 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

JWT authentication (validation only)

The server validates JWTs; it does not issue them.

  • Verification uses jsonwebtoken with the default validation profile, which is HS256 (HMAC-SHA256). It validates exp, and optionally iss and aud when an issuer/audience is configured.
  • The verification secret is loaded from the jwt-secret Kubernetes secret or the SOCHDB_JWT_SECRET environment variable.
Tokens are minted externally

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"]
}
  • role is one of Owner / Editor / Viewer. Its capabilities are merged with any explicit capabilities list.
  • tenant_id scopes 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

Hashing

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 Admin may access any namespace.
  • Other principals must match their tenant_id to the requested namespace, or the namespace must be default (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).

FlagEnv varEffect
--tls-certSOCHDB_TLS_CERTServer cert PEM path (enables TLS when paired with the key)
--tls-keySOCHDB_TLS_KEYServer private key PEM path
--tls-caSOCHDB_TLS_CACA 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 (env SOCHDB_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
At-rest encryption is a library API, not a server flag

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

ConcernFlag / EnvNotes
Enable auth--authTurns on JWT + API-key checking
JWT secretSOCHDB_JWT_SECRET / jwt-secretHS256, validation only
API key--api-key / SOCHDB_API_KEYSHA-256 hashed
API keys (many)SOCHDB_API_KEYS / api-keysComma- / newline-delimited
API-key pepperSOCHDB_API_KEY_PEPPERSwitches hashing to HMAC-SHA256
TLS--tls-cert / --tls-keyPEM paths; hot-reload
mTLS--tls-caAdds client-cert verification
Secrets mount--secrets-path / SOCHDB_SECRETS_PATHK8s secret directory
At-rest keySOCHDB_ENCRYPTION_KEY / encryption-keyLibrary API; not wired to a server flag
Other ports have their own (weaker) security posture

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.