Skip to content

Topic claims

A "topic claim" is permission over NATS subjects. NATS subjects are the wire-level addresses; controlling who can publish/subscribe to which subjects is the most fundamental authorization gate in this system.

NATS account permissions (today)

In operator/JWT mode, NATS users belong to NATS accounts. Each user gets a JWT that declares its publish and subscribe permissions:

text
user JWT permissions:
  pub:  "<authority-root>.>"          (publish anywhere under this root)
  sub:  "<authority-root>.>"          (subscribe to anything under this root)
        "_INBOX.>"                    (own reply-inbox)
        "$JS.API.>"                   (JetStream API)

The principal's NATS account holds these permissions; per-Device user JWTs inherit them, scoped by the link's authorityRoot. The helper that builds these is rootScopedNatsUserPermissions() in @open-matrix/nats-auth.

authorityRoot is e.g. space.spc-7f3a9b2c (canonical for new Spaces) or COM.NIMBLETEC.RICHARD-SANTOMAURO (legacy email-derived). Subjects under that root are addressable by anyone authorized into that account.

Cross-account isolation

Different principals get different NATS accounts. The permissions are:

  • principal A's user can pub/sub <root-A>.> — yes
  • principal A's user can pub/sub <root-B>.> — no, different account

Account boundaries are enforced by NATS itself; this is not a Matrix contract. As long as accounts are correctly provisioned, no JWT-level trick can cross them.

Cross-account exports/imports (target state)

Some system.* subjects need to be reachable from any account (e.g. system.heartbeat for cross-Space discovery). Operator/JWT mode supports per-subject exports/imports: account A exports specific subjects, account B imports them with explicit grant.

The historical parentRootMap mechanism (in old daemon/src/services/NatsSidecar.ts) implemented this for system.* cascades. The new operator/JWT configuration requires explicit export/import per subject. See projects/matrix-3/packages/docs/content/security/nats-account-isolation.md.

Topic-claim attenuation (target state)

A capability token (target state) might further attenuate a Device's NATS permissions:

account permissions: pub/sub  <root>.>
device-link scope:    pub/sub  <root>.devices.<deviceSlug>.> + <root>.chat.>

The device-link scope would be intersected with the account permissions before the user JWT is issued. Today this attenuation does not happen at the user-JWT level; the device user has the full account-level permissions.

Subject-naming convention

Subjects follow the actor wire format from CLAUDE.md § "Bus addressing":

<authority-root>.<mount>.$<facet>
COM.NIMBLETEC.RICHARD-SANTOMAURO.system.llm.$inbox
COM.NIMBLETEC.RICHARD-SANTOMAURO.chat.$events
COM.NIMBLETEC.RICHARD-SANTOMAURO.$reply.<correlationId>

NATS permissions are wildcard-based:

PermissionAllows
<root>.>any subject under <root>
<root>.system.>any system actor
<root>.system.devices.>only system.devices and its subtree
_INBOX.>reply inbox (per NATS convention)

Restricting pub more narrowly than sub is the typical pattern: a caller may need to subscribe to many subjects but only publish to its own.

Heartbeat token vs NATS user JWT

These are two different credentials and easily confused:

Heartbeat tokenNATS user JWT
Used byhost.control against HTTP endpointNATS client at connect
Formatopaque base64url stringNATS JWT with permissions
Verified byauth.hostLink.verifyHeartbeat (cloud)NATS server (operator/JWT)
Scopeone Host Linkone NATS account / authority root
Rotated byauth.hostLink.credentials.refreshsame op

A leaked heartbeat token gives attackers the ability to post heartbeats (and only heartbeats) for one Host Link until rotation. A leaked NATS user JWT gives attackers full subject permissions of the principal's account until the JWT expires (24h). The latter is more serious; rotation cadence reflects this.

What the actor framework gates

The actor framework (@open-matrix/core) gates op invocations by the actor's static accepts declaration. A caller that can reach system.devices.$inbox (NATS subject permission allows it) can attempt any op declared in accepts (devices.list, devices.register, …) but cannot invoke an undeclared op — the framework rejects unknown ops.

This is op-name gating, not principal-and-capability gating. Any caller with the right NATS subject access can attempt any declared op. Combined with NATS account isolation, this is "any caller in your account can invoke any of your actors' ops." Within an account, no further gates.

Browser-NATS specifics

Browser clients connect to NATS via WebSocket (/nats-ws, same-origin). They authenticate using a short-lived bus token (5 min) issued by HostSessionService.createBusToken(). The NATS server (per the nats-hub.conf) maps this token to the principal's account.

no_auth_user (the unauthenticated browser identity) gets minimal permissions — typically just enough to participate in the auth-rotation ceremony. Real authentication elevates the connection to the principal's account.

See also

  • Capability tokens — would be the wire format for attenuation.
  • Security realms — how the system-security package's realm model relates.
  • Namespace ownership — naming the roots that subjects live under.
  • projects/matrix-3/packages/docs/content/security/nats-account-isolation.md — canonical NATS spec.

Source: @open-matrix/nats-auth (rootScopedNatsUserPermissions, scopedJwtCredentialsFromAccountSeed); projects/matrix-3/packages/system-auth/src/host-auth.ts (storeNatsAccount, getNatsAccount).