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.