Skip to content

Security & tokens

Memoturn’s isolation story has two layers: structural — a profile is its own database file, and no data-plane operation can touch two profiles — and token-based — short-lived Ed25519-signed JWTs that widen authorization only, never the data plane’s reach.

Auth is off by default in dev; the node logs a loud warning at startup. Enable it for any shared deployment:

Terminal window
MEMOTURN_AUTH=on MEMOTURN_PLATFORM_KEY=... memoturnd

With auth on, the posture is fail-closed: the node refuses to start without MEMOTURN_PLATFORM_KEY — there is no generated platform key to leak or lose. It signs per-database JWTs with an Ed25519 key; signing-key precedence is MEMOTURN_AUTH_KEY (base64 PKCS#8 — in production, mounted from a Kubernetes Secret) → a local file under the data dir → object storage (only with MEMOTURN_PERSIST_AUTH_KEY=1, which persists a generated key unencrypted — the mounted Secret is the preferred path) → generate. A multi-replica fleet must share the signing key, or pods reject each other’s tokens. See Configuration for all auth variables.

TokenCoversPosture
Per-database tokenexactly one profile’s databasethe agent posture — an agent is locked to the one profile it serves
Namespace token (ns claim)every profile under one namespace, including its control routesthe orchestrator posture — mint per-profile tokens, checkpoint memories, list profiles
Platform keycontrol-plane operations: provision databases, mint tokensoperators and provisioning code only
Cluster keynode-internal hops (write forwarding)never leaves the cluster

Scopes are read, write, and admin: recall and get need read; ingest, forget, and session-end need write. Tokens are short-lived (default TTL 3600 s).

The cluster key must differ from the platform key — they are separate trust boundaries, and the node refuses to start if they match. Unset, it is derived from the signing key, so it is identical on every node in the fleet with no extra secret to manage.

Terminal window
# per-database token: one agent, one profile
memoturn --platform-key ... token create acme--alice --scope write
# namespace token: every profile under acme — the orchestrator posture
memoturn --platform-key ... token create-ns acme --scope write --ttl 3600

The same mints are available over HTTP (POST /v1/databases/{db}/tokens, POST /v1/namespaces/{ns}/tokens) — see REST API.

Gateways verify JWTs statelessly with the Ed25519 public key — no auth-service round-trip on the data path. Nodes additionally enforce lease epochs on every write, so a stolen-but-valid token still cannot make a zombie writer dangerous (see Consistency).

A profile boundary is a database file. There is no query, filter, join, or recall operation that spans two profiles — the isolation is not a row-level policy that could be bypassed, it is the shape of the storage. A namespace token widens which profiles a caller may address, one at a time; it does not create any cross-profile operation. See Profiles.

A further structural guard: per-tenant object-store prefixes — each database’s segments and manifests live under its own prefix in the bucket.

The SQL escape hatch (POST /v1/db/{db}/sql) is walled in:

  • Reserved tables are unreachable. Everything Memoturn manages inside a database — __memoturn_kv, __memoturn_docs_{collection}, __memoturn_memories* — carries the __memoturn_ prefix and cannot be referenced from user SQL, however the name is quoted.
  • Read tokens cannot mutate. Mutating statements need write scope; a read-scoped token gets 403.
  • No sandbox escapes. ATTACH, VACUUM INTO, and PRAGMA writable_schema are rejected. Benign read-only PRAGMAs (integrity_check, table_info) still work.
  • No transaction control. BEGIN, COMMIT, ROLLBACK, SAVEPOINT, and RELEASE are rejected (trigger bodies excepted): a statement batch is already one atomic unit, and the engine owns transaction boundaries.

Deleting a database — or the profile it backs — writes a deletion tombstone. Write tokens minted before the deletion are rejected with 403 (token revoked: it predates this database's deletion; mint a fresh token), so a stale token cannot resurrect a re-created profile of the same name.

The Helm chart is hardened by default (see Deployment):

  • Secure-by-default pods — non-root (uid 65532), read-only root filesystem, all Linux capabilities dropped, RuntimeDefault seccomp; writable paths are explicit emptyDir mounts.
  • No Kubernetes API access — a dedicated ServiceAccount with no API token mounted.
  • NetworkPolicy — egress locked to DNS, the object store, and (optionally) HTTPS for real S3 and AI providers; ingress restricted to the HTTP port, tightenable with allowExternalIngress and extraIngressFrom.
  • Secrets via Kubernetes Secrets (External Secrets Operator compatible for enterprise). The chart consumes auth.existingSecret (keys PLATFORM_KEY, CLUSTER_KEY, plus AUTH_KEY for multi-replica fleets) and an optional ai.existingSecret for extraction/embedding keys.

TLS at the ingress (cert-manager) and JWT verification at the gateway remain part of the full cell design.

A database’s primary region is chosen at creation. Regions are independent cells — each with its own etcd, gateways, data plane, and regional object-storage bucket — so the cell model maps directly onto residency requirements: a profile created in the EU cell stays in the EU cluster and its EU bucket. The global control plane sits on the provisioning path, never the data path.

Enterprises can enforce data-handling rules per namespace with a governance policy — a JSON document set with the platform key and enforced on every node:

Terminal window
curl -X PUT $MEMOTURN_URL/v1/namespaces/acme/policy \
-H "Authorization: Bearer $PLATFORM_KEY" \
-d '{"policy": {
"retention": {"pitr_secs": 3600},
"memory": {"task_ttl_max_secs": 600, "superseded_max_count": 20},
"ai_egress": {"extract": "deny", "embed": "self_hosted_only"}
}}'
  • Retention caps tighten the node’s point-in-time-recovery windows for every profile under the namespace; memory rules cap task TTLs (clamped at ingest) and age out superseded history and old events automatically.
  • AI egress rules govern the optional AI features per tenant: extract/ask set to deny return a deterministic 403 before any model is called; embed set to deny degrades exactly like an unconfigured embedder (writes succeed, keyword recall keeps working), and self_hosted_only permits embedding only through a self-hosted endpoint (loopback, private-network, cluster-internal, or MEMOTURN_EMBED_SELF_HOSTED_HOSTS).
  • Profiles can tighten, never loosen: PUT /v1/memory/{ns}/{profile}/policy (admin token) accepts only overrides at least as strict as the namespace policy — a loosening override is a 409 naming each offending field. The effective policy is always the strictest of node config, namespace, and profile.
  • Policies live in object storage next to the data they govern and converge on every node within the policy cache window (default 30 s) — no restarts. Egress checks fail closed; retention and TTL clamps never block writes.

See Configuration and the CLI.

With audit.enabled in the namespace policy, every node records an append-only, per-namespace audit stream in object storage — durable, immutable objects, deliberately outside branching so a rewind can never erase the trail, and surviving even deletion of the profiles it describes.

  • What’s recorded: memory mutations (ingest, extract, forget, session end) with txid and counts; every AI egress with provider, model, endpoint, item/byte counts, and duration — including denials; token minting, policy changes, and database deletion. Reads (recall, get, ask) are recorded only with audit.include_reads.
  • Metadata only: events never contain memory content, transcripts, or credentials. Each event carries actor attribution — a non-reversible hash of the credential plus its scope and claims — so an auditor can correlate “same token” without the stream ever holding material that grants access.
  • Reading the trail: GET /v1/namespaces/{ns}/audit with time/action/profile/outcome filters and cursor pagination — platform key, or a namespace admin token for its own stream, so enterprises pull their trail without holding the platform key. The CLI exports ranges as JSONL: memoturn audit export acme --from 7d --action ai. --outcome denied.
  • Bounds: emission is non-blocking and off the write path; a crash loses at most one flush window (MEMOTURN_AUDIT_FLUSH_MS, default 2 s; orderly shutdowns drain). audit.retention_secs bounds the stream itself. For tamper evidence, pair the stream with bucket-level object lock.

Forget hides a memory; erasure proves it’s gone — including from point-in-time-recovery history. POST /v1/memory/{ns}/{profile}/erasures targets one memory, a topic’s whole supersession chain, or a session:

Terminal window
curl -X POST $MEMOTURN_URL/v1/memory/acme/alice/erasures \
-H "Authorization: Bearer $TOKEN" \
-d '{"topic_key": "user.home-address", "type": "fact"}'
# 202 { "erasure_id": "ers_…", "status": "pending", "txid": 412, "grace_until": … }
  • Immediately: the rows, search entries, and vectors are hard-deleted with secure_delete page zeroing, and the post-erasure state is durably shipped before the request returns.
  • After the grace window (erasure.grace_secs in the policy, default 24 h — the undo window), the node rewrites object-storage history: every restorable snapshot and segment below the erasure point is dereferenced and physically reclaimed. Completion is bounded by grace + maintenance cadence, never the 30-day snapshot tier.
  • Then it proves it: object keys encode their transaction ids, so absence is verifiable by listing. A completed erasure carries a signed Ed25519 receipt — target, erasure point, and the verification evidence — checkable offline against the cluster’s public key.
  • Honest blockers: a named checkpoint pinning older history, or a branch that may still hold the data as live content, flips the erasure to blocked with the offenders named — never silently violated. Receipts scope their claim to object storage (the source of truth); node-local caches are transient and converge.
  • erasure.purge_on_forget: true upgrades every plain forget into a tracked erasure (the coupon id rides the Memoturn-Erasure-Id response header). Poll GET .../erasures/{id} or memoturn memory erasures for the receipt; erasure requests and completions land in the audit stream.
  • Without MEMOTURN_ETCD, a node that looks multi-node — auth on, or a non-loopback MEMOTURN_ADVERTISE — refuses to start unless MEMOTURN_SINGLE_NODE=1: the in-process lease table cannot enforce single-writer across nodes.
  • Per-tenant encryption keys wrapped by cloud KMS are planned for enterprise — see Roadmap.