Skip to content

Security logs

This page enumerates the actual log surfaces today: what is recorded, where, in what format, and how to read it. The fragmented today informs what unified auditing should look like (target state).

Today's three log surfaces

1. Bus events

Actors emit events on the bus (emit(eventName, payload)). Subscribers that care can record them. There is no central audit subscriber today; each consumer subscribes to what it needs.

Security-relevant emitters:

ActorEventWhen
system.devicesdevices.added { deviceId }First registration
system.devicesdevices.offline { deviceId, reason }Heartbeat timeout or explicit deregister
host.controldevice.heartbeat { deviceId, runtimeCount, registryRoot? }Each successful heartbeat
host.controldevice.heartbeat.failed { deviceId?, reason }Each failed heartbeat
system.factotumfactotum.credential-stored { provider }Credential added or replaced
system.factotumfactotum.credential-deleted { provider }Credential removed
system.factotumfactotum.credential-refreshed { provider, newExpiresAt }OAuth refresh succeeded
system.factotumfactotum.credential-expired { provider }OAuth refresh failed
system.factotumfactotum.needkey { provider, requestedBy }Lease attempted with no credential present

These events are observable but not persisted by default. A target-state audit subscriber would record them with timestamps and durable storage.

2. Per-runtime log files

<host-home>/logs/runtimes/<runtime-id>/<stream>.log per runtime. These are stdout/stderr from the runtime process. Security-relevant entries appear here for runtimes that include actorLog() calls:

  • system-gateway-http log lines for HTTP auth events (success / fail, body-token mismatch, revoked Host Link rejected)
  • system-auth log lines for principal lookup, OIDC verify, session revoke

Format is per-runtime; there is no schema. Search with journalctl (on systemd-managed Hosts) or grep on the file directly.

3. State-store mutations

The cloud system.auth state file (HostAuthStateStore) is itself a log of sorts: every mutation produces a new save. Inspecting timestamps on the records (createdAt, updatedAt, revokedAt, lastRefreshedAt) gives a partial history.

Specifically:

  • IHostLinkRecord mutations: createdAt, updatedAt, lastRefreshedAt, revokedAt.
  • IHostPublicNamespaceClaim mutations: createdAt, updatedAt.
  • IHostPrincipalRecord mutations: createdAt, updatedAt.
  • revokedSessions[] entries: { jti, principalId, expiresAt }.

This is "the state file is the log." It's incomplete because intermediate values are overwritten, but it captures the most recent mutation per record.

What is NOT logged today

  • Per-op invocation traces. There is no record of "principal X invoked op Y at time T."
  • Failed auth.identity.resolve calls (e.g. invalid cookie). The cookie is rejected and the op returns; no record.
  • NATS subject permission denials. NATS itself logs them server-side (depending on its log level), but Matrix-side actors do not see them.
  • credential.materialize calls. Only the factotum.credential-stored event is emitted on store; lease/materialize is not logged.
  • auth.namespace.claim rejection reasons. The op returns { ok: false, error: '...' } and the call site sees the error; no audit log entry.

A unified audit log would close these gaps. Implementation cost is the schema design; storage cost is per-event size × event rate.

Target state — unified audit log

A system.audit actor (target state) would:

  • Subscribe to all security-relevant bus events.
  • Record auth.identity.resolve outcomes via a subscribe-and-record pattern (the actor adds itself as a subscriber).
  • Persist records to a JetStream KV or stream with operator-bounded retention (e.g. 90 days).
  • Expose audit.search for operators to find specific events.
  • Cross-reference principalId, hostLinkId, deviceId so investigators can pivot.

Design choices to commit:

  • Append-only vs garbage-collected. Append-only is simpler but unbounded.
  • Per-principal vs per-tenant. If per-principal, GDPR-style erasure is cleaner.
  • On-Device vs cloud. Device-side audit is private but lost on unlink; cloud-side is centralized but introduces another cross- boundary surface.

Operator runbooks against current logs

"Did a Device successfully pair?"

bash
matrix invoke system.auth auth.hostLink.list \
  '{"principalId":"p_<sha20>"}'
# Look for the most recent record with status: 'active'.
# createdAt is the pairing timestamp.

"Did this Device's heartbeat fail recently?"

Subscribe to host.control.device.heartbeat.failed events on the bus. There is no historical log; you only see going forward.

bash
# bus subscribe (using whatever bus client you have)
nats sub "<authority-root>.host.control.$events"

"Were any credentials added or removed today?"

Subscribe to system.factotum.$events. Same constraint: forward-only.

"Did anyone access this principal's data without authentication?"

Today: not directly answerable. Inspect HTTP gateway log files for the specific Host:

bash
sudo journalctl -u hivecast-host.service --since "today" \
  | grep -E '(401|403|auth)'

See also

Source: event emitters identified in projects/matrix-3/packages/host-control/src/HostControlActor.ts, projects/matrix-3/packages/system/src/SystemDevicesActor.ts, projects/matrix-3/packages/system-factotum/dist/index.js. State-file mutations in projects/matrix-3/packages/system-auth/src/host-auth.ts.