Skip to main content

Capability tokens

Every request to mindD carries a signed bearer token in the gRPC metadata key x-mindd-capability (or the HTTP header of the same name). The token encodes who is allowed to do what:

  • tenant, the top-level isolation boundary. Required.
  • agent, an informational id, useful in audit.
  • namespaces, a list of glob patterns (for example kv/scratchpad, kv/tool-*).
  • ops, a list of permitted operations (kv.put, episodic.append, *).
  • exp, the expiration.

Two signature formats are supported: PASETO v4.public (the default) and JWT (HS256 or RS256). Both are pluggable behind a TokenVerifier interface.

Issuing a token

Use mindctl for development. In production the issuer is your IdP; mindD only ever verifies, and never needs a private key.

mindctl token issue \
--tenant acme --agent agent-1 \
--ns 'kv/scratchpad,episodic/events' \
--ops 'kv.get,kv.put,episodic.append,episodic.range' --ttl 1h
FlagDefaultMeaning
--tenantnone, requiredtenant the token acts as
--nsnone, requiredcomma-separated namespace glob patterns
--agentemptyinformational agent id
--opsget,put,delete,scancomma-separated ops
--ttl1htoken lifetime
--formatpasetopaseto or jwt
--secret-key-hex$MINDD_PASETO_SECRET_HEXPASETO signing key
--jwt-secret$MINDD_JWT_SECRETJWT HS256 secret
--jtiemptyoptional token id
  • --ns accepts comma-separated glob patterns. A trailing * matches any suffix: kv/tool-* covers kv/tool-cache, kv/tool-results, and so on. * alone matches every namespace.
  • --ops accepts either the dotted form (kv.put) or a verb-only form (put). * allows all ops.

Note that the default --ops get,put,delete,scan is verb-only, and that mindctl itself has no kv delete or kv scan command. The default exists for convenience with the SDKs; it is not a scope you should ship.

Verb-only ops match across every block

Op matching accepts the bare verb as well as the dotted form, so a token minted with --ops inspect for the lease block also satisfies admin.inspect. admin.inspect is not namespace scoped, so that token gains cross-namespace introspection of every namespace the server serves.

Mint fully-qualified ops (lease.inspect, not inspect) until this is tightened. Tracked as a known limitation for v0.1.0; see Security.

Op names

BlockOps
kvkv.get, kv.put, kv.delete, kv.scan
episodicepisodic.append, episodic.range, episodic.tail, episodic.expire
semanticsemantic.upsert, semantic.search, semantic.delete, semantic.expire
artifactartifact.put, artifact.get, artifact.stat, artifact.delete, artifact.list
leaselease.acquire, lease.renew, lease.release, lease.inspect, lease.list
graphgraph.upsert, graph.get, graph.query, graph.delete
adminadmin.inspect (cross-namespace; not tied to any --ns pattern)

Authority chain

The auth interceptor enforces this on every request:

metadata -> verify signature -> decode scope -> namespace match? -> op match? -> expired?

Failures map to gRPC codes:

CauseCode
Header missingUnauthenticated
Bad signature, malformed tokenUnauthenticated
Token expiredUnauthenticated
Namespace not covered by scopePermissionDenied
Op not in scopePermissionDenied

The capability check happens before the per-block service is invoked, and again inside each service before the driver is touched, which means an unscoped peer never reaches storage even when a real backend would have served the request. That second check is why the token's namespace pattern is the boundary you can rely on most; unlike the policy engine, it applies uniformly to unary and streaming RPCs.

Key rotation

Both verifiers accept a list of trusted public keys, not just one. See Key rotation for the overlap-window flow that never drops in-flight RPCs.

auth:
verifier: paseto
paseto:
public_key_hexes:
- "<new>" # add the new key first
- "<old>" # keep the old one until tokens minted under it expire

mindctl token gen-keypair generates a fresh pair. Do not use the one in this repository's examples: its private half is published with it. See Security.

Tenant isolation

The tenant claim only separates data physically when tenant_isolation: true is set on the server. It defaults to false, in which case every tenant shares one partition and only the namespace pattern separates them. See Tenant isolation.

mTLS adds a second factor

When the gRPC TCP listener runs with client_ca_file and require_client_cert, peers must also present an X.509 cert chained to that CA. Capability tokens still apply on top: mTLS authenticates the peer, the token scopes what they can do. See Transport security.