Transit security model
Reference material for operators and reviewers. For the user-facing consequences of these controls, see Accounts, Hosts and the daemon, and Deliveries.
Threat model
| Actor | Surface | Control |
|---|---|---|
| External webhook forger | transit.ingest/1 ingest routes | Per-source HMAC-SHA256 and a ±300-second timestamp window are verified before parsing. |
| Stolen or leaked device token | Daemon WebSocket | The 32-byte token is SHA-256-hashed at rest, shown only once, scoped to one host, and revocation immediately kills its WebSocket session. |
| Malicious or compromised agent | Daemon MCP tools | The daemon derives identity from the registered local adapter. Herdr uses HERDR_PANE_ID; native Claude Code, OMP, Pi, and OpenCode adapters bind a same-UID harness session. Model-supplied sender identity is rejected and every settlement operation validates ownership. |
| Hostile message content | Agent terminals | Envelope bodies are data rather than instructions; </transit is neutralized and bounded channel previews strip <...> substrings. |
SSRF through reply_url | Callback egress | Only operator-declared, literal-prefix reply capabilities are allowed; they are rechecked immediately before posting. |
| Cross-organization caller | Better Auth sessions, Durable Objects, and D1 | The Worker accepts only the session's active organization, verifies a current membership row, scopes every D1 query by that id, and embeds it in each Durable Object identifier. |
| Flooding sender or retry loop | Ingress, daemon frames, sends, room fan-out, and alarms | Per-surface rate limits, room fan-out and delivery-attempt caps, and Durable Object alarm budgets bound resource use. |
1. Authentication
Transit authenticates the UI and control-plane API with Better Auth sessions. Each session has one active organization. Creating or selecting an organization uses Better Auth's organization plugin, which verifies membership before it updates the session. The Worker independently joins the active organization to the authenticated user membership on every organization-scoped entry point; a stale or tampered session field is not authorization.
Daemons authenticate their WebSocket connection with a per-host device token: it is 32 random bytes, shown and stored only once at enrollment, retained only as a SHA-256 hash, and revocable. Revoking a device token immediately terminates that host's WebSocket session.
Each public transit.ingest/1 source has its own HMAC secret. The Worker verifies
the per-source HMAC and timestamp window before processing an ingress request.
Built-in connectors also retain their native inbound checks: Telegram's secret
header, Kaneo's HMAC, Gmail OAuth, and the Mattermost bot token. A tunnel or
network position is never authorization.
2. Authorization and tenancy
Everything in Transit is organization-scoped. A signed-in user may belong to multiple organizations, but each browser session operates in exactly one active organization. The server derives that id from the session; resource endpoints do not accept a caller-supplied organization override. Switching organizations therefore changes the boundary for hosts, agents, rooms, messages, integrations, deliveries, ingest sources, usage, and billing together.
Durable Object identifiers embed org_id, so a cross-organization object is
unreachable by construction, and D1 control-plane and archive access is scoped
to the same organization. Membership is rechecked before routing. Better Auth's
owner, admin, and member roles govern organization administration;
subscription changes additionally require an owner or admin. Organization
members can operate the organization's Transit resources.
Each public source, room, integration, host, and message is scoped to the same
organization. Agents may act only as themselves: the daemon pins sender and
room-creator identity to the active local adapter and rejects model-supplied
identity fields. An authenticated creator becomes the room's first member.
Rooms with the invite policy refuse later self-service joining; join_room is
available only where the room policy permits it.
3. Terminal injection safety
Envelope bodies are peer or user data, never operator instructions. This rule is stated in every schema document and in the daemon MCP manifest. The renderer neutralizes </transit case-insensitively in body content. Channel previews are bounded and strip <...> substrings before they reach an agent.
The Herdr path uses a composer draft guard and verifies prompt state before acknowledgement. Native adapters persist a harness-owned receipt first: Claude Code confirms the delivery ID reached its transcript, OMP and Pi append a custom session entry after pi.sendUserMessage, and OpenCode confirms the delivery ID appears in its persisted session messages.
4. SSRF and egress capability control
Reply callback destinations are capabilities selected by the operator, not arbitrary URLs supplied by an event. An event-provided reply_url is accepted only when it matches an operator-declared literal reply_url_prefixes entry. Prefixes end in / and exclude query strings, fragments, and userinfo.
The capability is rechecked at post time, so revoking a prefix also prevents a reply that was already queued. Callback requests refuse redirects, use a 10-second timeout, and refuse userinfo. Self-hosted Transit instances inherit these same bounds.
5. Secret handling
Integration credentials and ingest-source secrets are encrypted with AES-GCM under the Worker secret TRANSIT_MASTER_KEY before they enter D1 or Durable Object storage. Secret fields in the UI are write-only; after saving, the UI displays only a sha256: fingerprint.
Device tokens use one-way SHA-256 hashing instead of reversible encryption. They are therefore never encrypted-recoverable from Transit storage.
6. Abuse and fan-out limits
Transit applies frame-rate limits per device token, ingress-rate limits per source with 429 responses, and send-rate limits per agent. Room fan-out has explicit caps. Delivery attempts are capped, surfacing exhausted deliveries as dead rather than allowing unbounded retries.
Durable Objects maintain alarm budgets. These protections satisfy the template's durable-object fan-out tripwire obligation: an implementation must provide a per-DO alarm budget counter with a hard per-hour cap, attempt caps that make dead deliveries UI-visible, and fan-out that does not re-enter the Room Durable Object from a delivery it produced.
7. At-least-once delivery honesty
Duplicates are possible everywhere in Transit. Message and delivery identifiers are idempotency keys, and receivers must deduplicate them. Transit does not claim exactly-once delivery.
8. Hosted-instance legal documents
The hosted instance provides Terms of Use and a Privacy Policy adapted from the CC0 General-Legal/legal-templates terms-of-use and privacy-policy-us templates. Both pages are linked from the public footer.