Skip to content

Rotate credentials

Rotation is replacing a Device's authentication material without breaking its identity. Two artefacts can be rotated:

  • The device-scoped NATS user JWT (24h TTL)
  • The heartbeat bearer token

Rotating these is normal hygiene. It is also the right answer when an existing token is suspected leaked, when a long-lived Device crosses the JWT TTL, or when the cloud credential store changes (e.g. account seed rotated).

The op is declared in system-auth/src/index.ts:266-271:

auth.hostLink.credentials.refresh
  hostLinkId?: string
  hostId?: string
  principalId: string

Effect:

  1. HostLinkStore.refresh() bumps updatedAt / lastRefreshedAt.
  2. HostLinkStore.rotateHeartbeatToken() mints a new bearer token, stores its SHA-256 hash, returns the plaintext exactly once.
  3. @open-matrix/nats-auth mints a fresh device-scoped NATS user JWT + nkey seed, scoped to the link's authorityRoot, expiring in 24h.
  4. The new credentials are returned in a DeviceSetupExchangeResponse-shaped payload (same shape as redeem).

The old NATS user JWT continues to work until its exp claim is reached (NATS does not eagerly invalidate). The old heartbeat token is immediately invalid, because the cloud only accepts the new heartbeatTokenHash.

Headless rotation today

There is currently no dedicated CLI subcommand for "rotate this Device's credentials in place." The supported paths are:

Path A — re-pair (current canonical method)

bash
hivecast logout --local-only --home /tmp/matrix-home
hivecast login --device --cloud https://hivecast.ai --home /tmp/matrix-home

This produces a fresh Host Link (revoking the previous one), fresh heartbeat token, fresh NATS credentials. The Device's hostId / installId / deviceSlug are preserved (the cloud reuses them). User visible result: the Device "re-connects" with the same name and slug.

Path B — bus-level call (advanced, today)

bash
matrix invoke system.auth auth.hostLink.credentials.refresh \
  '{"hostLinkId":"hostlink_<id>","principalId":"p_<sha20>"}'

The result includes the new credentials map. Today, applying that map to the Device's local files requires writing the response into the correct credential files at the right paths and modes. There is no helper that does only-rotate-don't-re-pair on the Device side. Target state is a hivecast rotate-credentials subcommand that wraps this. For now: re-pair is the operational path.

Path C — automatic refresh (target state)

host.control could detect heartbeat 401/403 and trigger a re-pair automatically. Today it only emits device.heartbeat.failed and continues trying. Auto-rotation is target state.

When rotation is automatic

The Device's NATS user JWT has a 24h TTL (DEVICE_NATS_CREDENTIAL_TTL_SECONDS = 86_400). On a long-lived linked Device, rotation should happen at least every 24 hours to keep NATS auth working. In practice today: re-pair when NATS auth starts failing.

Why heartbeat token rotation matters separately

The heartbeat bearer token is the credential that proves "I am the Device that owns this Host Link" to the cloud /_auth/host-link/heartbeat endpoint. Rotating it:

  • caps the blast radius if the token leaks
  • makes "lost laptop" cleanup safer (revoke + new pairing on a new Device produces fresh material; the old material is dead)
  • keeps the token forward-secrecy assumption alive

Today's hash-only storage (no plaintext kept cloud-side) means a leaked cloud-side state file does not reveal the token. But rotating still reduces window of validity.

Verification

After re-pairing (Path A):

bash
hivecast link-status --home /tmp/matrix-home
# expect: linked: true, fresh linkedAt, hostConfigLinked: true

# Force a heartbeat
matrix invoke system.devices devices.status '{}'
matrix invoke system.devices devices.list '{"principalId":"p_<sha20>"}'
# expect: source: 'merged', status: 'online'

If link-status still shows the old link or the heartbeat fails, the re-pair did not complete cleanly. Check <host-home>/credentials/hivecast-link.json mtime to confirm it was rewritten.

What does NOT change on rotation

  • hostId (stable Device identity)
  • installId (stable per-install identity)
  • hostName (mutable, but not changed by rotation alone)
  • deviceSlug (stable management projection key)
  • principalId, spaceId, authorityRoot, publicNamespace, routeKey

What changes:

  • New hostLinkId (path A: re-pair) or same hostLinkId (path B: in-place refresh)
  • New heartbeatTokenHash
  • New NATS user JWT + seed
  • lastRefreshedAt, updatedAt

See also

Source: projects/matrix-3/packages/system-auth/src/host-auth.ts (HostLinkStore.refresh(), rotateHeartbeatToken(), recordNatsUserPublicKey()); op declarations in projects/matrix-3/packages/system-auth/src/index.ts:266-271.