Files
buzz/report.md
Jordan Mecom cee58c3653 wip
2026-07-06 14:04:27 -04:00

1134 lines
50 KiB
Markdown

# Buzz Security-Relevant Architecture Report
This report explains Buzz from a security review point of view. It is based on
the current code in this repository, including work in progress areas such as
managed agents, relay membership, workspace switching, Git over the relay, and
multi-tenant direction.
The short version: Buzz is a Nostr-backed collaboration system. Almost every
user action becomes a signed Nostr event. The relay verifies signatures, checks
membership, writes events to Postgres, fans them out over WebSockets, and sends
selected content to search, audit, workflow, media, Git, or audio subsystems.
The main security boundaries are Nostr private keys, relay-level membership,
channel membership, event kind validation, and the process boundary around
agents.
## Code Layout
The security-relevant code is spread across the relay, shared Nostr logic,
clients, agent harness, and supporting services.
Relay and core:
- `crates/buzz-relay/src/main.rs` starts the relay, enforces production key
requirements, and wires services together.
- `crates/buzz-relay/src/config.rs` reads environment configuration and
service secrets.
- `crates/buzz-relay/src/state.rs` holds shared runtime state, caches, queues,
the relay keypair, and service clients.
- `crates/buzz-relay/src/connection.rs` handles WebSocket connection lifecycle,
frame limits, heartbeats, and handler concurrency.
- `crates/buzz-relay/src/handlers/auth.rs` implements NIP-42 WebSocket auth.
- `crates/buzz-relay/src/handlers/event.rs` handles incoming WebSocket `EVENT`
messages and live fanout.
- `crates/buzz-relay/src/handlers/req.rs` and
`crates/buzz-relay/src/handlers/count.rs` implement query and count behavior.
- `crates/buzz-relay/src/handlers/ingest.rs` is the main persistent event ingest
path used by WebSocket and HTTP.
- `crates/buzz-relay/src/handlers/side_effects.rs` applies channel membership,
channel metadata, deletion, discovery, and notification side effects.
- `crates/buzz-relay/src/handlers/command_executor.rs` handles command events
such as workflow creation, workflow approval, and direct message commands.
- `crates/buzz-core/src/kind.rs` defines the event kind registry.
- `crates/buzz-core/src/verification.rs` verifies Nostr event IDs and Schnorr
signatures.
- `crates/buzz-core/src/filter.rs` implements filter matching and per-event
reader authorization.
- `crates/buzz-auth/src/nip42.rs` and `crates/buzz-auth/src/nip98.rs` implement
NIP-42 and NIP-98 authentication.
Data and supporting services:
- `crates/buzz-db/src/*` contains Postgres data access for events, channels,
members, API tokens, workflows, relay members, and user state.
- `migrations/0001_initial_schema.sql` defines the main storage model.
- `crates/buzz-search/src/lib.rs` configures Typesense search.
- `crates/buzz-audit/src/*` records audit log entries.
- `crates/buzz-media/src/*` validates and stores uploads in S3-compatible
storage.
- `crates/buzz-relay/src/api/media.rs` exposes Blossom-style media upload and
download endpoints.
- `crates/buzz-relay/src/api/bridge.rs` exposes HTTP `/events`, `/query`,
`/count`, and workflow webhook endpoints.
Clients and identity:
- `desktop/src-tauri/src/app_state.rs` creates and stores the desktop user
Nostr key.
- `desktop/src-tauri/src/commands/identity.rs` exposes signing, import, export,
and auth event commands to the Tauri frontend.
- `desktop/src-tauri/src/commands/workspace.rs` applies the active relay
workspace to the backend.
- `desktop/src/features/workspaces/*` stores workspace metadata in frontend
storage and forces React remounts during workspace switching.
- `mobile/lib/shared/workspace/*` stores mobile workspace configuration,
including the mobile user private key.
- `mobile/lib/shared/auth/auth_provider.dart` authenticates the mobile client
against the relay.
Agents and developer tools:
- `desktop/src-tauri/src/managed_agents/*` creates, stores, launches, stops, and
logs managed agents.
- `crates/buzz-acp/src/*` is the ACP harness that bridges Buzz events to an AI
agent process.
- `crates/buzz-agent/src/*` is a minimal ACP-compatible agent.
- `crates/buzz-dev-mcp/src/*` is the local developer MCP server. It exposes
shell and file-editing tools to agents.
Other important feature areas:
- `crates/buzz-workflow/src/*` executes workflow definitions.
- `crates/buzz-relay/src/api/git/*` implements Git smart HTTP, object storage,
and push policy checks.
- `crates/git-credential-nostr/src/lib.rs` signs Git HTTP credentials with a
Nostr key.
- `crates/git-sign-nostr/src/lib.rs` signs Git commits and tags with Nostr keys.
- `crates/buzz-core/src/pairing/*`, `crates/buzz-pair-relay/src/lib.rs`, and
`crates/buzz-pairing-cli/src/main.rs` implement device pairing.
- `crates/buzz-relay/src/audio/*` implements huddle audio rooms.
- `crates/buzz-proxy/src/*` provides Nostr compatibility proxy behavior.
## System Model
Buzz uses Nostr as the event format and identity model. A user or agent has a
Nostr keypair. The public key is the account identifier. The private key signs
events.
The relay is the central service. It accepts WebSocket and HTTP requests,
verifies events, checks authorization, writes accepted events to Postgres, and
fans events out to live subscribers. Redis is used for pub/sub and presence.
Typesense is used for search. S3-compatible storage is used for media and Git
object data.
The relay is not a gossiping Nostr relay. It does not replicate events to other
relays as a normal Nostr network relay would. The security model is closer to a
single workspace service that uses Nostr events as signed records.
The important runtime boundaries are:
- Relay process boundary: all normal clients and agents must go through relay
validation.
- User key boundary: whoever controls a user's Nostr private key can act as
that user.
- Agent key boundary: a managed agent has its own Nostr key. The desktop app
can delegate owner context to it with a NIP-OA auth tag.
- Channel membership boundary: private channel reads and writes depend on
membership and role checks.
- Relay membership boundary: when enabled, the relay only admits configured
relay members or delegated agents whose owners are relay members.
- Local machine boundary: managed agents and the dev MCP server can run local
commands. That is intentionally powerful and should be treated like local code
execution.
## Nostr Machinery
### Event Identity And Signatures
Nostr events contain a public key, kind, timestamp, tags, content, ID, and
signature. The event ID is derived from the event body. The signature is a
Schnorr signature over that ID.
The relay verifies both the event ID and the signature in
`crates/buzz-core/src/verification.rs`. The persistent ingest path in
`crates/buzz-relay/src/handlers/ingest.rs` rejects events that fail signature
verification. The WebSocket event handler in
`crates/buzz-relay/src/handlers/event.rs` also rejects events whose `pubkey`
does not match the authenticated connection, except for specific proxy and gift
wrap cases.
This is a strong property: accepted application events are attributable to the
Nostr key that signed them. The sharp edge is also simple: key compromise is
account compromise.
### Event Kinds
All known event kinds are registered in `crates/buzz-core/src/kind.rs`. Unknown
kinds are rejected by the relay ingest path. This is good because it keeps the
relay from silently storing unexpected signed data with unclear semantics.
Some important kinds:
- NIP-42 auth: `22242`
- NIP-98 HTTP auth: `27235`
- Blossom upload auth: `24242`
- Gift wrap: `1059`
- Channel messages and NIP-29 group events
- Channel metadata and discovery events: `39000`, `39001`, `39002`
- Membership notifications: `44100`, `44101`
- Presence and typing ephemeral events: `20001`, `20002`
- Agent observer frames and agent-specific events
- Workflow, approval, Git, and relay administration events
The relay has several kind-specific guardrails in
`crates/buzz-relay/src/handlers/ingest.rs`: relay-only events are rejected from
clients, HTTP-only or WebSocket-only restrictions are enforced, delete events
must be structurally valid, encrypted engram/reminder envelopes are checked for
basic shape, and channel-scoped kinds must carry valid channel scope.
### WebSocket Auth: NIP-42
WebSocket connections authenticate with NIP-42. The relay sends a random
challenge. The client signs an auth event with the challenge and relay URL. The
relay verifies the signature, the challenge, the relay tag, and timestamp
freshness in `crates/buzz-auth/src/nip42.rs`.
The connection-level auth handler is in `crates/buzz-relay/src/handlers/auth.rs`.
After NIP-42 succeeds, the relay can also check:
- Pubkey allowlist, if configured.
- Relay membership, if `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`.
- NIP-OA delegation, if `BUZZ_ALLOW_NIP_OA_AUTH=true`.
When relay membership is enabled, `RELAY_OWNER_PUBKEY` bootstraps the owner and
`BUZZ_RELAY_PRIVATE_KEY` is required at startup. This is enforced in
`crates/buzz-relay/src/main.rs`.
### HTTP Auth: NIP-98
HTTP requests use NIP-98 signed auth events. Verification is in
`crates/buzz-auth/src/nip98.rs`; bridge route handling is in
`crates/buzz-relay/src/api/bridge.rs`.
The auth event binds the signer, URL, method, timestamp, and optionally a
payload hash. Buzz clients generally include a payload hash for normal JSON
requests. The verifier accepts requests without a payload hash if the auth event
does not include one. That is allowed by the NIP, but it is risky for
state-changing non-streaming requests because the body is not cryptographically
bound to the signature.
The HTTP bridge has a replay cache for NIP-98 event IDs. Git HTTP intentionally
uses a different tradeoff, described later.
### Scopes, Tokens, And Real Authorization
Buzz has a `Scope` model in `crates/buzz-auth/src/scope.rs`. API tokens can be
stored with scoped permissions in the `api_tokens` table. Those token records
are used by paths such as production media upload.
For normal Nostr-authenticated users, the relay generally grants the known
application scopes after authentication and then relies on relay membership,
channel membership, ownership checks, and per-kind validation for the real
authorization decision. That is important during review: a scope check may be
necessary, but it is rarely sufficient by itself.
This also means new features should not treat "authenticated Nostr user" as
"authorized user." They need an explicit policy for the event kind, channel,
global record, owner, or resource being accessed.
### NIP-OA Delegation
Buzz uses NIP-OA-style auth tags to connect an agent key to a human owner key.
The helper is in `crates/buzz-sdk/src/nip_oa.rs`. The owner signs an auth tag
that names the owner pubkey, the agent pubkey, conditions, and a signature. The
relay can verify that tag when `BUZZ_ALLOW_NIP_OA_AUTH=true`.
This is how a managed agent can be admitted when relay membership is enabled and
the owner is a relay member. The agent still signs events with its own key. The
auth tag proves that the owner delegated that agent.
The sharp edge is that `BUZZ_AUTH_TAG` is a credential. It is not the owner's
private key, but when paired with the agent private key it can carry owner
context in parts of the system that trust NIP-OA. If conditions gain more
semantics, expiry, or scope rules, those rules need to be enforced everywhere
the tag is accepted.
### Encrypted Nostr Payloads
Some Buzz data is encrypted at the Nostr payload layer. Gift wraps, engrams,
event reminders, observer frames, and pairing messages all use encrypted content
or encrypted envelopes in different ways.
The relay can validate envelope shape, route by tags, and enforce read gates,
but it usually cannot read the encrypted plaintext. The metadata is still
visible: kind, pubkey, created timestamp, tags, event size, and routing
relationships. NIP-44 conversation keys are derived in memory from Nostr keys;
they are not stored as independent long-lived keys in the relay.
The important review rule is that encryption does not remove the need for relay
authorization. Encrypted global events still need p-gates, author gates,
indexing exclusions, and live fanout filtering.
### Channel Scope
Buzz uses the NIP-29 `h` tag for channels. Channel data is not scoped by `e`
tags. `h` tags and `channel_id` are checked throughout the relay.
Important code:
- `crates/buzz-relay/src/handlers/ingest.rs` resolves channel scope and checks
membership before storing most channel events.
- `crates/buzz-relay/src/handlers/side_effects.rs` applies role checks for
admin actions.
- `crates/buzz-relay/src/handlers/req.rs` checks channel access before
registering subscriptions or returning historical results.
- `crates/buzz-core/src/filter.rs` performs final filter matching and
per-event reader authorization.
Private channel reads and writes require membership. Open channels are
deliberately readable and writable by nonmembers, subject to relay-level auth
and any token restrictions. If relay membership is disabled, an arbitrary
authenticated Nostr identity can interact with open channels.
### Global Events And P-Gated Events
Some events are global instead of channel-scoped. Examples include profiles,
gift wraps, user state, agent engrams, reminders, and some Git or workflow
events.
Global events are a security sharp edge because they are not protected by
channel membership. The relay compensates with kind-specific checks:
- Gift wraps and DM visibility records are p-gated. Queries must include a
`#p` tag that matches the authenticated user.
- Agent engrams must be requested by the author or by the user named in `#p`.
- Author-only kinds, such as event reminders, can only be queried by the author.
- Search excludes gift wraps, DM visibility records, and author-only records.
The relevant checks live in `crates/buzz-relay/src/handlers/req.rs`,
`crates/buzz-relay/src/handlers/event.rs`, and
`crates/buzz-core/src/filter.rs`.
One sharp edge: global-only kinds are stored with `channel_id = NULL`, but the
raw signed tags are still present. `crates/buzz-core/src/filter.rs` treats
explicit `h` tags as authoritative during filter matching. A malformed or
surprising global event with an `h` tag can therefore match a channel-style
filter even though it is not stored as a channel event. This should not leak
private channel data by itself, because the event is global already, but it can
confuse automation, counts, or UI assumptions.
### Live Fanout And Queries
Subscriptions are registered after auth and access checks in
`crates/buzz-relay/src/handlers/req.rs`. The subscription registry in
`crates/buzz-relay/src/subscription.rs` keeps channel and global subscriptions
separate. Channel subscriptions do not receive global events, and global
subscriptions do not receive channel events.
Live fanout also has a backstop in `crates/buzz-relay/src/handlers/event.rs`.
Private channel fanout is filtered by visibility and membership caches before
events are sent. Sensitive global kinds are filtered by recipient. Search
results are also post-filtered after Typesense returns matches.
This is one of the stronger parts of the system: authorization is checked both
before registering interest and before sending results.
### Replaceable Events, Deletions, And Side Effects
Nostr has replaceable and parameterized replaceable event kinds. Buzz handles
replacement behavior during ingest. It also has kind-specific validation for
deletions, edits, reactions, admin events, forum votes, and workflow commands.
Side effects are in `crates/buzz-relay/src/handlers/side_effects.rs`. This is
one of the most important files to review because it determines who can add or
remove members, change channel metadata, archive channels, delete groups, and
emit relay-signed discovery or notification events.
Good behavior in this area:
- Last-owner removal is guarded.
- Private channel admin changes require a member with sufficient role.
- Channel archive and visibility changes are restricted.
- Deletion has author and owner/admin checks.
- Open channel joins are allowed, but private channel joins are not open.
The important policy decision is that open channels are intentionally open to
nonmember writes. That is not a bug, but it must be clear in product and
deployment docs.
## Feature Review
### Relay, Auth, And Membership
The relay is the main enforcement point. It verifies Nostr signatures, checks
auth state, checks relay membership when enabled, checks channel membership, and
enforces per-kind rules.
Attack surface:
- WebSocket clients can send auth, event, req, close, and count messages.
- HTTP clients can use `/events`, `/query`, `/count`, media endpoints, Git
endpoints, and workflow webhooks.
- Redis pub/sub messages are trusted enough to fan out events between relay
instances.
- Postgres stores plaintext event content and tags for normal events.
- Typesense stores searchable plaintext for indexed events.
Good things:
- WebSocket frame size is bounded in `crates/buzz-relay/src/connection.rs`.
- There is handler concurrency control and heartbeat cleanup for dead clients.
- Unknown event kinds are rejected.
- Event content size and timestamp drift are bounded.
- Relay membership mode fails startup if required owner or relay key config is
missing.
- Sensitive global kinds are p-gated or author-gated.
Risky things:
- Development defaults are permissive. `BUZZ_REQUIRE_AUTH_TOKEN=false`,
`BUZZ_REQUIRE_RELAY_MEMBERSHIP=false`, permissive CORS defaults, and the
hardcoded development relay key are acceptable for local development but
dangerous if deployed accidentally.
- The relay has no first-class tenant ID in the schema. The isolation model is
relay membership plus channel membership, not tenant membership.
- Redis, Postgres, Typesense, and S3 are all inside the trusted service
boundary. A compromise of any of them exposes meaningful data.
### Channels, Messages, Threads, Reactions, And Deletions
Messages are signed Nostr events scoped to channels with `h` tags. Threads are
represented with NIP-10-style relationships. Replies update materialized thread
counters. Reactions and deletions are kind-specific events.
Attack surface:
- Malformed tags can try to confuse thread, channel, or deletion semantics.
- Message content is plaintext unless the specific event kind uses encryption.
- Open channels allow writes by nonmembers.
- Delete events can be used to hide or remove content if policy checks are
wrong.
Good things:
- Channel membership is checked before normal private channel writes.
- Delete events require exactly one target tag and then ownership or role
authorization.
- Reactions have deduplication logic.
- Archived channel checks block writes after archive.
- Thread counter behavior is centralized enough to review.
Risky things:
- Any new message-like kind must remember to use `h` tags and the same
membership checks.
- Any new reply insertion path must update thread counters.
- Global event kinds carrying accidental `h` tags can surprise channel-style
filters.
### Search
Search uses Typesense through `crates/buzz-search/src/lib.rs`. The relay indexes
accepted events and uses Typesense for NIP-50-style search. Query results are
post-filtered by the relay.
Attack surface:
- Typesense receives plaintext searchable content for indexed event kinds.
- Search filters must stay aligned with relay authorization rules.
- Search queue pressure can drop indexing work.
Good things:
- Gift wraps, DM visibility records, and author-only records are excluded from
indexing.
- Search results are post-filtered after Typesense returns them.
- Accessible channel IDs are added to search filters.
Risky things:
- Typesense must be protected like a primary data store because it contains
sensitive channel text for indexed kinds.
- Dropped indexing work can create security-adjacent confusion: users may think
absence from search means absence from the system.
### Audit
Audit logging is handled by `crates/buzz-audit/src/*` and queued from the relay.
The audit path records accepted events and selected service actions.
Attack surface:
- Audit integrity depends on Postgres and the relay process.
- Queue send failures or worker failures can create gaps if not monitored.
Good things:
- Audit writes are separated from the main event path with backpressure.
- The audit service uses a hash-chain style design.
Risky things:
- This is not a tamper-proof external audit system. A database administrator or
a relay host compromise can still affect the audit store.
- The security review should check whether queue and worker failures are
surfaced strongly enough for production operations.
### Media Upload And Download
Media upload uses Blossom-style signed upload auth. The relay validates the
auth event before accepting the body. Files are validated by content type and
stored by hash in S3-compatible storage. The code is mainly in
`crates/buzz-relay/src/api/media.rs` and `crates/buzz-media/src/*`.
Attack surface:
- Uploaders can attempt oversized files, active content, media parser bugs, or
content type confusion.
- Download URLs are unauthenticated once the hash URL is known.
- S3 credentials and bucket policy are part of the trusted boundary.
Good things:
- Upload auth is verified before reading the body.
- Upload auth binds the expected SHA-256 hash through the `x` tag.
- Image, video, and generic file validation reject many active content types.
- Generic files are served as attachments with defensive headers.
- The response content type comes from the validated sidecar metadata, not from
untrusted request headers.
- Range reads are capped and stream from storage.
Risky things:
- `GET /media/...` and `HEAD /media/...` are unauthenticated. Media URLs are
bearer-by-knowledge. Anyone with the URL can fetch the object.
- Media privacy depends on unguessable hashes and controlled sharing, not on
per-request auth.
- Production mode depends on correct `X-Auth-Token` and S3 configuration.
- S3 buckets must not be public.
### Desktop Client And Workspaces
Desktop is a Tauri app. It holds the user's signing key in the backend process
and exposes signing commands to the frontend.
Important code:
- `desktop/src-tauri/src/app_state.rs`
- `desktop/src-tauri/src/commands/identity.rs`
- `desktop/src-tauri/src/commands/workspace.rs`
- `desktop/src/features/workspaces/workspaceStorage.ts`
- `desktop/src/features/workspaces/useWorkspaceInit.ts`
Attack surface:
- A compromised frontend or webview can ask the Tauri backend to sign events.
- Today it can also call `get_nsec` and export the user's private key.
- Local files in the app data directory contain identity and managed agent
secrets.
- Workspace switching changes relay URL and remounts React state, but it does
not create a separate per-workspace identity by default.
Good things:
- The desktop frontend migration strips old `nsec` values out of localStorage.
- The main user key is stored in `identity.key`, not localStorage.
- `identity.key` writes are atomic.
- On Unix, `identity.key` is written with mode `0600`.
- Corrupt `identity.key` files are quarantined rather than overwritten.
- Workspace switching resets known module-level caches in
`desktop/src/features/workspaces/useWorkspaceInit.ts`.
Risky things:
- `identity.key` is an unencrypted Nostr private key on disk. It is protected by
filesystem permissions, not by the OS keychain.
- `desktop/src-tauri/src/commands/identity.rs` exposes `get_nsec`. This means an
XSS or compromised frontend can exfiltrate the full user private key, not only
request signatures.
- `apply_workspace` accepts an optional `nsec` over IPC. The current frontend
passes `undefined`, but the command surface still supports changing the
signing key.
- Desktop workspaces are mostly relay URL profiles. The desktop identity is
global unless explicitly replaced through env or IPC.
### Mobile Client
Mobile stores workspaces using Flutter secure storage. The workspace JSON can
include `nsec`, and auth uses that key to sign NIP-42 events.
Important code:
- `mobile/lib/shared/workspace/workspace_storage.dart`
- `mobile/lib/shared/workspace/workspace.dart`
- `mobile/lib/shared/auth/auth_provider.dart`
Attack surface:
- Device compromise or app compromise exposes the mobile Nostr key.
- Workspace JSON contains the key material needed to act as the user.
Good things:
- Mobile uses platform secure storage rather than normal localStorage.
- Auth failures can remove invalid or restricted workspaces.
Risky things:
- Secure storage improves local at-rest protection, but the app still needs the
plaintext key while running.
- The mobile workspace model can hold different keys per workspace, unlike the
current desktop default. That difference should be intentional and documented.
### Managed Agents
Managed agents are launched by the desktop app. Each managed agent has its own
Nostr keypair. The desktop stores the agent private key and passes it to the
agent process as environment variables.
Important code:
- `desktop/src-tauri/src/managed_agents/types.rs`
- `desktop/src-tauri/src/managed_agents/storage.rs`
- `desktop/src-tauri/src/managed_agents/runtime.rs`
- `desktop/src-tauri/src/managed_agents/env_vars.rs`
- `desktop/src-tauri/src/commands/agents.rs`
- `crates/buzz-acp/src/*`
The flow is: desktop creates an agent record, generates an agent Nostr keypair,
creates an owner delegation auth tag, stores both in the managed agent JSON
store, and later starts the ACP harness. The harness receives the agent command,
agent args, MCP command, relay URL, agent private key, and optional auth tag in
environment variables. The harness connects to Buzz as the agent, listens for
events it is allowed to respond to, and invokes the configured AI agent process.
Attack surface:
- The agent process can sign as the agent.
- The agent receives `BUZZ_PRIVATE_KEY` and `NOSTR_PRIVATE_KEY`.
- The agent may receive `BUZZ_AUTH_TAG`, which proves owner delegation in
places that accept NIP-OA.
- User-provided environment variables and commands affect the child process.
- Agent stdout and stderr are written to local log files.
Good things:
- Agents use separate keys from the human owner.
- The owner key is not passed to the managed agent.
- Reserved environment variables are stripped from user-provided env.
- Malformed environment keys are rejected, including keys containing `=`.
- Agent process cleanup uses process groups where supported.
- Logs are rotated.
Risky things:
- `managed-agents.json` contains agent private keys and auth tags in app data.
The write path is atomic, but it does not set explicit `0600` permissions the
way `identity.key` does.
- Any tool available to the agent can read its own private key from the
environment unless the tool is deliberately isolated.
- `BUZZ_AUTH_TAG` is a delegated credential. If an attacker has both the agent
key and auth tag, they can act as that delegated agent wherever the relay
accepts the owner delegation.
- ACP permission defaults should be reviewed carefully. The ACP config supports
powerful modes, including bypass-style permissions, and the managed agent
posture should match the user's expectation for local command execution.
### Developer MCP Server
The developer MCP server exposes local shell and file tools to agents. This is
not a sandbox. It is a controlled way to give an agent access to the developer's
machine.
Important code:
- `crates/buzz-dev-mcp/src/shell.rs`
- `crates/buzz-dev-mcp/src/paths.rs`
- `crates/buzz-dev-mcp/src/shim.rs`
Attack surface:
- The shell tool runs arbitrary `bash -c` commands.
- File tools can read and edit paths outside the repo.
- The shell child environment intentionally includes `BUZZ_PRIVATE_KEY` so the
`buzz` CLI works.
- Git signing helpers are configured with a temporary keyfile.
Good things:
- Command duration, output size, and artifact size are bounded.
- Process groups are killed on timeout or cancellation where supported.
- The shim removes `NOSTR_PRIVATE_KEY` and uses a `0600` temporary keyfile for
Git helpers.
- The session temp directory is created with restrictive permissions.
Risky things:
- This is local code execution by design. Any agent with this MCP server can
read files, run commands, and exfiltrate secrets available to its process.
- There is no path containment in the file tools.
- `BUZZ_PRIVATE_KEY` remains available to shell commands.
### Workflows
Workflows are YAML or JSON definitions executed by the workflow engine. They can
run conditions, add reactions, call webhooks, and wait for approvals.
Important code:
- `crates/buzz-workflow/src/executor.rs`
- `crates/buzz-relay/src/handlers/command_executor.rs`
- `crates/buzz-relay/src/webhook_secret.rs`
- `crates/buzz-relay/src/api/bridge.rs`
- `crates/buzz-db/src/workflow.rs`
Attack surface:
- Workflow definitions can call outbound webhooks.
- Webhook triggers expose an unauthenticated HTTP endpoint protected by a
workflow secret.
- Conditions are evaluated from user-controlled expressions.
- Approval tokens control resumable workflow execution.
Good things:
- Webhook outbound calls reject private and reserved IPs after DNS resolution.
- The HTTP client pins the resolved IP and disables redirects.
- Webhook response bodies are capped.
- Condition expression length is bounded and evaluation has a timeout.
- Workflow webhook secrets are generated with random UUID v4 values and compared
in constant time.
- Workflow approval tokens are stored as SHA-256 hashes.
Risky things:
- Workflow webhook secrets are stored inside the workflow definition in
Postgres under `_webhook_secret`. They are stripped from normal responses and
returned once at creation, but DB compromise exposes them.
- The webhook endpoint also accepts `?secret=...`. Headers are preferred, but
query parameters are often logged by proxies.
- Workflow outbound webhooks can exfiltrate data by design. That means workflow
creation and editing permissions matter.
- The command executor notes that command event idempotency and domain mutation
writes are not always in a single transaction. That can produce event/mutation
divergence in failure cases.
### Git
Buzz implements Git smart HTTP backed by object storage and Nostr auth. All Git
HTTP routes require NIP-98 auth. Push policy is enforced by a pre-receive hook
callback.
Important code:
- `crates/buzz-relay/src/api/git/transport.rs`
- `crates/buzz-relay/src/api/git/policy.rs`
- `crates/buzz-relay/src/api/git/store.rs`
- `crates/buzz-relay/src/api/git/cas_publish.rs`
- `crates/git-credential-nostr/src/lib.rs`
- `crates/git-sign-nostr/src/lib.rs`
Attack surface:
- Git clients stream packfiles to the relay.
- The Git credential helper signs NIP-98 credentials.
- Push policy depends on hook callback integrity.
- Object storage holds repository data.
- Git subprocesses are launched by the relay.
Good things:
- Repo IDs are validated: owner must be a lowercase 64-char pubkey, and repo
names are limited.
- Git subprocess environments are cleared and rebuilt with minimal variables.
- The push policy hook uses HMAC-SHA256 over a canonical payload.
- Hook callbacks have a short freshness window.
- Push policy checks repo ownership, channel binding, archive state, roles, and
branch rules.
- Object publication uses content-addressed storage and compare-and-swap style
manifest pointers.
Risky things:
- Git auth intentionally signs the repo-root URL, not the exact Git service URL,
because Git credential protocol does not pass query strings cleanly.
- Method checking is intentionally relaxed because the same credential is reused
across GET and POST.
- Packfile bodies are not covered by a NIP-98 payload hash because they are
streamed.
- Git auth does not use the generic NIP-98 replay cache. The design relies on
short timestamp freshness, URL binding, TLS, and push policy.
- A captured Git HTTP credential can likely be replayed briefly for the same
repo within the freshness window.
- Local repo name reservation currently has a cross-instance caveat. Separate
relay instances with separate disks can race to grant the same display name.
### Pairing
Pairing lets one device transfer secret material to another device using a QR
payload, ephemeral keys, encrypted events, and a short authentication string.
Important code:
- `crates/buzz-core/src/pairing/crypto.rs`
- `crates/buzz-core/src/pairing/qr.rs`
- `crates/buzz-core/src/pairing/session.rs`
- `crates/buzz-pair-relay/src/lib.rs`
- `crates/buzz-pairing-cli/src/main.rs`
Attack surface:
- The QR URI contains a 32-byte session secret.
- Pairing events are encrypted, but the sidecar relay can see metadata.
- Device users must compare the short authentication string.
- Pairing payloads can contain private key material.
Good things:
- Pairing uses ephemeral keys and HKDF-derived session state.
- Transcript hashes are checked.
- Short authentication string confirmation is required before payload acceptance.
- Pairing session timeout is short.
- Pairing event signatures and IDs are verified.
- Pairing relay has tight limits: short TTL, small frames, small per-connection
budgets, deduplication, and no persistence.
- Secret buffers use zeroization in several places.
Risky things:
- The QR code is a secret. A screenshot, camera, or screen share can expose the
session secret.
- SAS confirmation is a human security step. If users skip or misunderstand it,
pairing can be attacked.
- Some temporary plaintext copies can still exist because serde and NIP-44
internals allocate strings.
- The pairing relay is intentionally unauthenticated and should be exposed only
in the intended narrow deployment shape.
### Huddle Audio
Huddle audio is a WebSocket audio relay for channel rooms.
Important code:
- `crates/buzz-relay/src/audio/handler.rs`
- `crates/buzz-relay/src/audio/room.rs`
- `crates/buzz-relay/src/audio/wire.rs`
Attack surface:
- Audio clients authenticate over WebSocket.
- The relay receives audio frames and forwards them to room peers.
- Telemetry such as audio level is client-provided.
Good things:
- Huddle connections use NIP-42 auth.
- Relay membership and channel membership are checked.
- Room size, frame size, text control frames, heartbeat, and peer admission are
bounded.
- Room version is pinned.
- Audio level telemetry is treated as untrusted and clamped.
Risky things:
- Audio is relayed through the server. It is not end-to-end encrypted at the
application layer.
- The relay can observe room metadata and audio frame traffic.
- Any future moderation or trust decision must not rely on client-supplied audio
level telemetry.
### NIP-28 Proxy Compatibility
The proxy provides compatibility with clients that expect NIP-28 behavior. It
maps NIP-28-style interactions to Buzz channel behavior.
Important code:
- `crates/buzz-proxy/src/*`
- `NOSTR.md`
Attack surface:
- The proxy has its own server key, salt, admin secret, and API token config.
- It creates deterministic shadow identities for compatibility behavior.
- Private channels require the proxy server pubkey to be a member.
Good things:
- Proxy config keeps the compatibility layer separated from the core direct
relay path.
- Private channel behavior depends on explicit proxy membership.
Risky things:
- Shadow identity derivation and proxy secrets are security-sensitive. If the
salt or proxy server key changes, identity continuity and trust assumptions
change.
- Proxy admin secrets and API tokens need production-grade secret storage.
### Multi-Tenancy And Workspaces
Today, Buzz has workspace switching in the desktop app and relay-level
membership in the relay. It does not yet have a first-class tenant ID that is
carried through every table, query, cache, search index, S3 key, and pub/sub
topic.
Important code:
- `migrations/0001_initial_schema.sql`
- `crates/buzz-db/src/relay_members.rs`
- `crates/buzz-relay/src/handlers/relay_admin.rs`
- `desktop/src/features/workspaces/useWorkspaces.tsx`
- `desktop/src/features/workspaces/workspaceStorage.ts`
- `desktop/src-tauri/src/commands/workspace.rs`
Current behavior:
- Relay membership is global to a relay.
- Channel membership is per channel.
- Events have optional `channel_id`, but no `tenant_id`.
- Global events are shared within the relay.
- Desktop workspaces are mostly relay URL profiles with local metadata.
- Desktop switches workspace by changing relay URL state and remounting the
React app subtree.
Attack surface:
- Any shared relay deployment uses one database realm unless tenant scoping is
added.
- Global events, relay members, API tokens, workflows, media, search, audit,
Redis pub/sub, and S3 keys all need a tenant story before multi-tenant hosting
can be considered isolated.
- Client-side workspace switching can accidentally retain module-level caches
unless each singleton is reset.
Good things:
- Workspace switching already has an explicit reset path in
`desktop/src/features/workspaces/useWorkspaceInit.ts`.
- Relay membership mode provides a useful coarse admission control for private
deployments.
- Channel access checks are centralized enough that tenant scoping can be added
deliberately.
Risky things:
- Without a tenant ID, "multi-tenant" means multiple users or workspaces inside
one relay security domain, not hard tenant isolation.
- Typesense indexes, Redis channels, S3 object keys, audit logs, API tokens, and
workflow state must all be scoped if a single relay serves multiple tenants.
- Desktop uses one default identity across workspaces. That may be correct for a
user moving between relays, but it is not the same as per-tenant identity
separation.
## Keys And Secrets
This section lists the important keys and secrets, where they live, and what
happens if they are compromised.
| Key or secret | Where it is stored or passed | Used for | Compromise impact |
| --- | --- | --- | --- |
| Human Nostr private key, desktop | `{app_data_dir}/identity.key`; `BUZZ_PRIVATE_KEY` env can override; exportable through `get_nsec` | Signs user events, NIP-42 auth, NIP-98 auth, observer control events | Full account compromise for that Nostr identity |
| Human Nostr private key, mobile | Flutter secure storage workspace JSON, including `nsec` | Signs mobile user events and auth | Full account compromise on mobile identity |
| Human Nostr private key, CLI and Git tools | `BUZZ_PRIVATE_KEY`, `NOSTR_PRIVATE_KEY`, or Git config `nostr.keyfile` | CLI event signing, Git credentials, Git object signatures | Full account compromise for that key |
| Managed agent private key | `desktop` app data under `agents/managed-agents.json`; child env as `BUZZ_PRIVATE_KEY` and `NOSTR_PRIVATE_KEY`; dev MCP temp keyfile for Git | Signs agent events and auth | Attacker can act as the managed agent |
| NIP-OA auth tag | Managed agent record and child env `BUZZ_AUTH_TAG`; also Git config path in some tools | Proves an agent is delegated by an owner key | With the agent key, attacker can act as delegated agent where NIP-OA is trusted |
| Relay private key | `BUZZ_RELAY_PRIVATE_KEY` env; hardcoded dev fallback only when auth token is not required | Signs relay/system/discovery/notification events | Attacker can forge relay-signed system behavior and damage trust in relay events |
| Relay owner pubkey | `RELAY_OWNER_PUBKEY` env and `relay_members` table | Bootstraps relay owner membership | Wrong value gives owner control to wrong pubkey |
| API tokens | Raw token returned to caller; SHA-256 hash stored in `api_tokens` | Scoped API access, especially media upload in production | Raw token grants its configured scopes until revoked or expired |
| Workflow webhook secret | Stored inside workflow definition as `_webhook_secret`; returned once at creation; sent as `X-Webhook-Secret` or `?secret=` | Authenticates external webhook triggers | Anyone with the secret can trigger that workflow |
| Workflow approval token | Raw token generated by workflow engine; SHA-256 hash stored in DB | Approves or denies waiting workflow steps | Hash leak is less useful than raw token, but approval authorization still needs review around token delivery |
| Blossom upload auth event | Short-lived signed event in HTTP `Authorization: Nostr ...` | Authorizes one upload hash | Replay within validity window may upload the same hash if other checks allow it |
| S3 media credentials | `BUZZ_S3_ACCESS_KEY`, `BUZZ_S3_SECRET_KEY`, `BUZZ_S3_ENDPOINT`, `BUZZ_S3_BUCKET` | Media object storage | Attacker can read or write stored media if bucket policy also allows it |
| S3 Git credentials | `BUZZ_GIT_S3_*` or fallback S3 env vars | Git object and manifest storage | Attacker can read or tamper with Git backing storage depending on bucket permissions |
| Typesense API key | `TYPESENSE_API_KEY` env | Search indexing and querying | Attacker can read indexed plaintext content or tamper with search results |
| Database URL and credentials | `DATABASE_URL` env | Primary Postgres access | Attacker can read and mutate almost all application state |
| Redis URL and credentials | `REDIS_URL` env | Pub/sub, presence, cache coordination | Attacker can inject or observe live coordination data depending on Redis access |
| Git hook HMAC secret | `BUZZ_GIT_HOOK_HMAC_SECRET` env, random dev fallback | Authenticates pre-receive hook callbacks to relay policy endpoint | Attacker can forge hook policy callbacks |
| Proxy server key, salt, admin secret, API token | Proxy env vars documented in `NOSTR.md` | NIP-28 compatibility behavior and proxy administration | Attacker can impersonate proxy behavior or administer proxy depending on secret |
| Pairing session secret | Encoded in `nostrpair://...secret=...` QR URI | Establishes pairing session | Anyone who sees it can participate in the pairing attempt unless SAS confirmation stops them |
| Pairing ephemeral private keys | In memory during a pairing session | Establishes encrypted pairing messages and SAS | Compromise during the short session can attack that pairing |
| NIP-44 derived conversation keys | Derived in memory from Nostr private keys | Encrypts and decrypts NIP-44 payloads | Compromise allows plaintext access for that derived conversation context |
## Security Strengths
These are the design and implementation choices that look good from a security
review point of view.
- Normal actions are signed by user or agent Nostr keys.
- Event IDs and signatures are verified before storage.
- Unknown kinds are rejected.
- NIP-42 auth uses random challenges, relay binding, and timestamp freshness.
- NIP-98 auth verifies URL, method, timestamp, signature, and payload hash when
present.
- WebSocket frames, event content, historical limits, media sizes, workflow
expressions, webhook responses, and audio frames have bounds.
- Channel and global subscriptions are kept separate.
- Private channel live fanout is filtered again at send time.
- Sensitive global kinds are p-gated or author-gated.
- Search results are post-filtered after Typesense.
- Gift wraps and other sensitive global kinds are excluded from search.
- Relay membership mode fails startup if required production secrets are absent.
- Desktop no longer stores the user `nsec` in localStorage.
- Desktop user key writes use atomic writes and `0600` permissions on Unix.
- Mobile uses secure storage for workspace secrets.
- Managed agent environment handling strips reserved variables and rejects
malformed env keys.
- The dev MCP shim removes `NOSTR_PRIVATE_KEY` and uses a `0600` temporary
keyfile for Git helpers.
- Workflow outbound webhooks have SSRF defenses: private IP rejection, DNS
pinning, no redirects, timeout, and response body cap.
- Git subprocess environments are hardened.
- Git push policy callbacks use HMAC and a short freshness window.
- Pairing uses ephemeral keys, transcript binding, SAS confirmation, and a
tightly limited sidecar relay.
- Huddle audio has auth, membership checks, frame bounds, room caps, and
untrusted telemetry handling.
## Security Sharp Edges
These are the areas I would prioritize in a security review.
1. Desktop private key exposure
`desktop/src-tauri/src/commands/identity.rs` exposes `get_nsec`, and the
desktop key is stored unencrypted in `identity.key`. Filesystem permissions
help, but a frontend compromise can export the key directly. For a desktop
app that renders remote or semi-trusted content, this is one of the highest
risk areas.
2. Managed agent private key storage
Managed agent records contain agent private keys and auth tags. They are
stored in JSON in the app data directory and passed to child processes in env
vars. This is simple and useful, but it means any local process compromise or
agent tool compromise can steal an agent identity.
3. Agent tool authority
`buzz-dev-mcp` is intentionally powerful. It can run shell commands and
access files outside the repo. This should be described to users as local
code execution, not as a restricted tool sandbox.
4. Multi-tenancy is not hard isolation yet
The schema does not carry a tenant ID. Relay membership and channel
membership are useful, but they are not the same thing as tenant isolation.
Before hosting multiple tenants on one relay, tenant scope needs to be added
to Postgres queries, Typesense indexes, Redis pub/sub, S3 object keys, audit
logs, workflow state, API tokens, media, Git storage, caches, and relay admin
state.
5. Development defaults are unsafe for production
The relay defaults are convenient for local development. In production, the
dangerous settings are no relay membership, no auth token requirement,
permissive CORS, dev Typesense and S3 credentials, and the hardcoded dev
relay key fallback.
6. Git NIP-98 replay tradeoffs
Git auth intentionally relaxes URL, method, body hash, and replay behavior
because of Git smart HTTP constraints. The design may be acceptable, but it
should be explicitly reviewed as a replay window protected by TLS and short
freshness, not as full NIP-98 request binding.
7. NIP-98 payload hash is optional
The generic NIP-98 verifier accepts missing payload hashes. For non-streaming
state-changing routes, Buzz should consider requiring payload binding unless
there is a specific reason not to.
8. Media URLs are bearer-by-knowledge
Media download is unauthenticated. That is a valid product choice if media
hashes are treated as share links, but it should not be mistaken for private
media authorization.
9. Workflow secrets are stored in database definitions
Workflow webhook secrets are stripped from normal responses, but they are in
Postgres. Query-param secrets are also supported. Header-only secrets and
stronger secret storage would reduce accidental leakage.
10. Typesense contains sensitive plaintext
Typesense should be treated as sensitive infrastructure. It can contain
indexed channel content even when Postgres access is otherwise controlled.
11. Global event `h` tag behavior is surprising
Global-only events can still contain signed `h` tags. Because filter matching
respects explicit tags, these events can match channel-style filters even
though they are stored globally. That is not necessarily a data leak, but it
is a confusing edge that can affect automation.
12. Relay/system key blast radius
The relay private key signs system events, discovery events, and membership
notifications. A relay key compromise lets an attacker forge relay-authored
system behavior. It should be stored and rotated like a production signing
key, not like a normal config value.
## Suggested Review Priorities
For a first security review pass, I would focus on these areas in order.
1. Desktop key handling and Tauri command exposure
Review `get_nsec`, `import_identity`, `sign_event`, observer encryption
commands, frontend trust assumptions, CSP, remote content rendering, and
whether the private key can move to OS keychain-backed signing without direct
export to the frontend.
2. Managed agent launch and local tool model
Review managed agent key storage permissions, auth tag storage, env passing,
logs, command configuration, ACP permission defaults, and how clearly the UI
communicates local command authority.
3. Relay authorization invariants
Review `ingest.rs`, `req.rs`, `event.rs`, `side_effects.rs`, and
`filter.rs` together. The key question is whether every event kind has clear
rules for who can write it, who can read it, whether it is channel or global,
whether search can index it, and whether live fanout matches historical
query behavior.
4. Multi-tenant design
Decide whether a "tenant" is a relay, an org inside a relay, a workspace, or
something else. If tenants share one relay process and database, add tenant
scope explicitly before treating the deployment as isolated.
5. Git auth and push policy
Review the Git NIP-98 exception model, replay window, credential helper
behavior, hook HMAC, object storage CAS behavior, and repo name reservation
across multiple relay instances.
6. Workflow and media exfiltration
Review who can create workflows, where webhook secrets appear, whether query
secrets can be removed, what data workflows can send out, and whether media
URL privacy matches product expectations.
7. Production configuration hardening
Add deployment checks or docs that make production requirements hard to miss:
relay private key, relay membership, strict CORS, real Typesense key, private
S3 bucket, strong Git hook secret, TLS, Redis/Postgres network isolation, and
secret rotation paths.
## Things To Know While Reviewing Code
- The most important relay files are `ingest.rs`, `event.rs`, `req.rs`,
`side_effects.rs`, and `filter.rs`. Read them as a set.
- Do not assume a Nostr kind is safe because it is signed. Signatures prove who
authored the event; they do not say the author was allowed to perform the
action.
- Do not assume global events are harmless. Global events need their own read
gates and indexing rules.
- Do not assume desktop workspaces are tenants. They are local relay profiles
unless backed by server-side tenant isolation.
- Do not assume agents are sandboxed. The default developer tool model gives
agents local machine power.
- Do not assume encrypted content hides metadata. Gift wraps, engrams,
reminders, observer frames, and pairing events still expose kinds, pubkeys,
timestamps, tags, and routing metadata.
- Do not assume relay-signed events are user intent. They are relay assertions,
and their trust depends on the relay private key and relay implementation.
- Do not assume media hashes are private authorization. They are unguessable
object references, but anyone who learns the URL can fetch the object.