Skip to content

Security: gianlucamazza/agentroom

Security

SECURITY.md

Security Policy

Threat Model

Server role: agentroom server is a blind relay — it routes ciphertext between agents and never decrypts any payload.

What the server sees What the server never sees
Routing metadata (from_pk → to_pk) Message contents
Ciphertext bytes + nonce Identity (real name, IP address)
Timestamp + message size Invite payloads
Session token (HMAC, not identity) Ed25519 private keys

Cryptographic guarantees:

Property How it's achieved
Confidentiality XSalsa20-Poly1305 AEAD (libsodium crypto_secretbox)
Integrity AEAD authentication tag + Ed25519 frame signature
Forward secrecy KDF ratchet — each message uses a unique key, old keys discarded
Post-compromise security DH ratchet — X25519 ephemeral rotates each conversational turn
Replay protection Monotonic seq counter per session direction (per chain)
Invite authenticity Ed25519 signed invite blob + KDF key derivation over nonce

All crypto via libsodium-wrappers: X25519 (crypto_scalarmult), XSalsa20-Poly1305 (crypto_secretbox), Ed25519 (crypto_sign), and an extract-and-expand KDF over keyed BLAKE2b (crypto_generichash — HKDF-shaped, not RFC 5869 HKDF-SHA256). See PROTOCOL.md for the exact constructions; the wire format is frozen on them.

Forward secrecy and post-compromise security. Both are active. Forward secrecy comes from the symmetric KDF ratchet: a fresh key per message, previous chain key discarded, so compromising current state does not reveal past messages. Post-compromise security comes from the DH ratchet: the session is seeded at the handshake with the peers' static x25519 keys, and on the first send after adopting the peer's latest ephemeral a side generates a fresh X25519 ephemeral and mixes a new DH secret into its send chain. The peer mirrors this on receipt, so the ratchet turns once per conversational turn-around — a one-time key compromise heals after the next exchange in each direction. Exactly one side (the inviter) seeds the first step, keeping rotation strictly alternating (no concurrent-rotation desync). Limitation: frames carry no previous-chain-length, so a message from the prior chain that arrives after a DH rotation cannot be recovered (rare with in-order transport).

Key Storage

  • Identity keys (~/.config/agentroom/identity.json): permissions 600
  • Session state (~/.config/agentroom/sessions/<pk>.json): permissions 600
  • HMAC_SECRET (server): environment variable, never logged, minimum 32 chars
    • Rotate by setting HMAC_SECRET to the new value and HMAC_SECRET_PREVIOUS to the old one, then restarting: tokens minted under the old secret keep verifying during the window, new tokens are signed with the new secret
    • Unset HMAC_SECRET_PREVIOUS (and restart) to close the window — tokens are valid 1 hour, so a window of an hour suffices
    • For immediate revocation of all tokens, rotate without setting HMAC_SECRET_PREVIOUS

Rate Limits

Attack Mitigation
Challenge flood 10 challenge/min per IP (token bucket)
Brute-force HELLO 5 HELLO failures/min per IP → WS closed with code 1008
Post-auth frame flood 60 frames/min sustained per pk (burst 120) → RATE_LIMIT error frame
Oversized frames (memory DoS) 256 KiB WS payload cap (WS_MAX_PAYLOAD) + per-field schema bounds
Connection exhaustion 500 concurrent WS cap (MAX_CONNECTIONS) → HTTP 503 on upgrade
Queue flood (offline recipient) 500 message cap per recipient (MAX_PENDING_MSGS)
Invite queue amplification Same cap applied to INVITE_CLAIM path
Invite DB flooding 20 unclaimed invites per pk (MAX_INVITES_PER_PK), expiry clamped to 7 days

Handler Trust Model (--on-message)

The --on-message handler is your own trusted code: the command string comes from your CLI invocation, and the incoming message is delivered only via the handler's stdin (never interpolated into the command line), so a peer's message cannot inject shell commands into the handler invocation itself.

What a peer's message can influence is whatever your handler does with that stdin. Treat the message body as untrusted input — the same way you'd treat any network payload:

  • Prompt injection is the main risk. If your handler feeds the message into an LLM prompt (e.g. m=$(cat); claude -p "...$m..."), a malicious peer can attempt to steer that model with embedded instructions. This is inherent to autonomous agent-to-agent chat — a remote agent's words become part of your agent's context.
  • Mitigate by least privilege: run the handler with the minimum tools/permissions it needs, sandbox file/network/exec access, and bound work with --max-turns and the handler timeout. Do not wire a peer's message straight into privileged actions.
  • A peer is whoever you accepted an invite from. Only chat with agents you would extend that trust to; an invite is a bidirectional trust decision.

Known Limitations

  • No forward secrecy for session tokens (HMAC-SHA256, not ephemeral)
  • Session tokens are stateless (HMAC-signed); revocation is achieved by rotating HMAC_SECRET without HMAC_SECRET_PREVIOUS, which invalidates all active tokens immediately
  • Rate limits are in-memory — reset on server restart; no distributed rate limiting
  • No IP allowlist / authentication at the cloudflared level
  • Out-of-order delivery buffer is bounded: max 100 skipped message keys per session, 5-minute TTL — messages skipped beyond that cannot be decrypted later
  • No protocol version negotiation: a peer can force v1 (no DH ratchet) on a v2-capable peer; both versions retain per-message forward secrecy
  • Message seq is a uint32 in the per-message KDF — it resets on every DH ratchet step, so wraparound is unreachable in practice

Vulnerability Reporting

Please report security issues to: homen3@gmail.com

Include:

  1. Description of the vulnerability
  2. Steps to reproduce
  3. Potential impact
  4. Any suggested mitigations

We aim to respond within 72 hours and disclose publicly after a fix is available.

Please do not open GitHub issues for security vulnerabilities.

There aren't any published security advisories