Multi-tenancy in Pion¶
Short version: Pion offers two multi-tenant models.
- One
pion-serverprocess per tenant — the canonical deployment wherever hard resource isolation matters (memory, CPU, WAL, blast radius). - Per-connection tenant binding (
--tenant NAME=PASSWORD) — in-process namespace isolation for high-density hosting of many small tenants. Every key a tenant connection touches is transparently prefixed with its namespace; commands outside a fail-closed allowlist are rejected.
The --ns-prefix flag is only a cooperative namespace guard, not an isolation boundary — do not rely on it to keep tenants apart.
What --ns-prefix actually does¶
--ns-prefix tenant_a makes the KV.PREFIX.* and V.* command family require the client-supplied namespace to start with the byte string tenant_a. It is a startswith check applied at a handful of call sites (src/commands/kv_prefix.mojo).
What it does not do:
- It does not gate plain
GET/SET/HSET/FT.*/ stream / pub-sub commands. Those share one per-worker keyspace regardless of--ns-prefix. - It does not bind a namespace to a connection. Any client may pass any namespace value; a client that names its namespace
tenant_a...passes the check.
In other words, --ns-prefix guards against accidental cross-namespace access (a typo, a misconfigured client), not against a motivated or hostile tenant. Treat it as a soft convention, not a security control.
The isolation model: one process per tenant¶
Pion is shared-nothing per worker and cheap to run (--profile kv ≈ 50 MB/worker). The supported way to isolate tenants is to give each tenant its own process:
pion-server -p 1974 --requirepass "$TENANT_A_PW" --profile kv -w 4 --independent-workers # tenant A
pion-server -p 1975 --requirepass "$TENANT_B_PW" --profile kv -w 4 --independent-workers # tenant B
Each process has a private keyspace, WAL, HNSW graph, and (with --requirepass) its own authentication boundary. Route each tenant's clients to their own port. This is the model pion-lmcache/MULTITENANT.md describes.
Authentication¶
Set --requirepass <password> to require AUTH <password> on every connection before any other command is served (both the RESP port and the binary port+1 fast lane, which uses a 0x37 AUTH frame). Without it, the server accepts unauthenticated commands from anyone who can reach the port — bind to loopback or a trusted network in that case.
Per-connection tenant binding (--tenant)¶
Real in-process tenant isolation. Enable with repeatable --tenant NAME=PASSWORD flags; --requirepass is required and becomes the admin credential:
pion-server -w 4 --independent-workers --requirepass "$ADMIN_PW" \
--tenant "acme=$ACME_PW" --tenant "globex=$GLOBEX_PW"
- Binding.
AUTH acme <password>(orHELLO 2 AUTH acme <password>) binds the connection to tenantacme.AUTH <admin-password>binds it as admin (unprefixed keyspace, full command set). There is no anonymous path: until a connection binds, every command is rejected with-NOAUTH. - Transparent namespacing. Every key a tenant connection touches is invisibly prefixed with
acme:— clients need no changes. Tenant names are restricted to[A-Za-z0-9_-]{1,64}, so:can never appear in a name and the name→prefix map is prefix-free: tenant A cannot forge a key in tenant B's namespace even by sending a key that literally containsB:(it becomesA:B:...). - Deny-by-default allowlist. Tenant connections may run the keyed KV / hash / list / set / zset / bitmap / stream(XADD-family) / geo / HLL families,
KEYS/SCAN(filtered to the tenant's namespace, prefix stripped), TTL commands,MULTI/EXEC/WATCH, and connection-scope commands (PING,ECHO,INFO,CLIENT, ...). Everything else —EVAL*/SCRIPT/FUNCTION(runtime-computed keys can't be gated),FLUSHALL/FLUSHDB,CONFIG/DEBUG/SAVE/CLUSTER,FT.*/vector/AI.*/KV.*(ANN neighbor sets are prefix-blind; vector isolation needs per-tenant indexes),SUBSCRIBE/PUBLISH(channels are a separate namespace),SORT,XREAD, numkeys-form multi-key commands (ZUNIONSTORE,BITOP,LMPOP, ...) — is rejected with-NOPERM. Fail-closed at the command level. - Multi-key safety. Allowed multi-key commands (
MSET,DEL,RENAME,COPY,SMOVE,LMOVE,S*STORE, ...) are safe by construction: every key argument is rewritten, so all of them land in the caller's own namespace. - Performance. Tenant-bound connections are served entirely on the slow path (~345K ops/s P=1 per worker); the default/admin path is byte-for-byte untouched, so the KV gate baselines are unaffected. Namespacing cost is paid only by namespaced connections.
- Binary lane (
port+1). The0x37AUTH frame accepts only the admin credential; tenants cannot use the binary lane (fail-closed; tenant binding for the binary lane is future work).
Caveats¶
- Per-worker keyspace scatter. Pion workers own private keyspaces and connections are distributed by
accept()competition, so a tenant's keys are spread across per-worker maps.KEYS/SCANsee only the current worker's slice — existing Pion semantics for every client, but visible under a tenant banner. Use-w 1(the default) if a tenant needs a completeSCANview. - Shared resources. Tenants in one process still share the worker's memory, CPU, WAL, and snapshot files (noisy-neighbor and blast-radius remain). Where that matters, use one process per tenant (below).
WATCHversions are bumped by fast-path mutations only; slow-path writes (which includes all tenant writes) do not bump them, soWATCH-based optimistic locking is not currently conflict-detecting for tenant connections.
Regression test: tests/test_tenant_isolation.py.