Skip to content

Security: mutonby/agentring

SECURITY.md

Security policy

Reporting a vulnerability

Please report vulnerabilities privately, not in public issues.

Use GitHub private vulnerability reporting: the repository's Security tab → Report a vulnerability.

Include what is affected (relay, daemon, Android app, deploy files), the version or commit, steps to reproduce and the impact you expect. We aim to acknowledge reports within a few days. Please give us reasonable time to fix the issue before disclosing it.

Supported versions

Only the latest release (and the default branch) gets security fixes. Release APKs (agentring-X.Y.Z.apk) are signed with the agentring release key, SHA-256 certificate fingerprint afa35da7bed28a94b62693f7b4c184646b45c7855a51c3f8809283ae96889e3c. The android-preview pre-release is a test build of the default branch signed with the public debug key (see below).

Threat model

agentring is self-hosted: you run the relay, LiveKit and the daemon. One user, one agent, one or more paired Android phones; there are no accounts. The hosted version runs the same relay in hub mode, many isolated tenants in one process (docs/cloud.md): there, one tenant must not be able to affect another.

What the relay stores and can see

The relay keeps a SQLite database in its data directory (DATA_DIR/relay.db):

Table Contents
meta relay_id, the device JWT signing secret and, when generated by the relay, the ak_ API key (also written to DATA_DIR/api_key, mode 600)
devices device id, name, platform, app version, FCM token, per-device push_secret, created / last seen / revoked times
messages full text of every message in both directions, reply links, read times
calls, call_participants call id, direction, state, reason, priority, agent name, timestamps, who ended it and why, per-phone state
events the event log (every persisted event, including message text and call reasons)
pairing_tokens SHA-256 hashes of pairing tokens only

Messages, calls and events are not deleted automatically. Anyone with access to the relay host or its data volume can read all of them and impersonate the agent. The relay never handles call audio: media goes through LiveKit.

Credentials

  • API key (ak_…): the daemon's credential. It grants full agent access (send messages, place calls, list and revoke devices, read the event log). The relay compares SHA-256 digests in constant time. On the daemon it is stored in ~/.agentring/.env (mode 600, directory mode 700). Rotate it by setting a new AGENTRING_API_KEY on the relay.
  • Device JWT: HS256, issued at pairing, long-lived. Every request re-checks the device row, so agentring devices revoke <device_id> takes effect immediately: REST calls return 401, open WebSockets are closed with code 4401, and if the phone is on the open call that call ends and its room closes. A join token minted just before stays valid for up to 120 s but finds no room, because deployments run LiveKit with room.auto_create: false. On the phone the JWT and push_secret are AES-GCM encrypted with a non-exportable Android Keystore key.
  • Agent tokens (at_…, hub mode): short-lived agent credentials, at most one hour. They can't create pairing tokens or revoke devices, and a socket opened with one closes when it expires.
  • Pairing tokens: single-use, expire after at most 10 minutes, stored hashed. Rotating the API key deletes the unused ones. The QR code carries the token in clear: treat it like a password while it is valid.

Push (FCM)

Pushes go through Google's Firebase Cloud Messaging using your own Firebase project. Each payload is data-only and carries relay_id and sig = HMAC-SHA256(push_secret, event_json) with a per-device secret; the app drops any payload whose relay id or signature does not match. This stops forged pushes, but payloads are signed, not encrypted: Google can read them. Pushed events include message text from the agent (truncated to fit FCM's 4 KB limit), call reasons and the agent name. LiveKit tokens are never sent over FCM.

The Firebase service-account JSON lets its holder send pushes to your devices; keep it private.

Transport

  • The relay refuses to start with a non-https public URL unless AGENTRING_INSECURE_DEV=1.
  • The app rejects http:// relay URLs and ws:// LiveKit URLs unless the host is private: localhost, *.local, ::1, 10/8, 127/8, 172.16/12, 192.168/16 and 100.64/10.
  • The LiveKit webhook (/v1/livekit/webhook) checks LiveKit's signature and rejects any request that came through the public reverse proxy (X-Forwarded-For present).

Calls and LiveKit

  • One room per call (call_<call_id>, at most 4 participants). The relay mints a token per participant, valid for that room only and for 120 s (the join window). The caller gets its token in the POST /v1/calls response; a phone gets its token only from accept / join.
  • Media is encrypted in transit (WebRTC DTLS-SRTP) but not end-to-end: the LiveKit server you run can access call audio.
  • The LiveKit API secret can mint tokens for any room; the relay requires it to be at least 32 characters.
  • LiveKit must run with room.auto_create: false (deploy/livekit.yaml): the relay creates each room, so a token that outlives its call can't bring the room back.

Voice AI provider

With --ai (or the agentring.voice library) the daemon streams the call's audio (everyone on the call, mixed) to the voice model provider — currently Google's Gemini Live API — together with the instructions, tool definitions, tool results and transcripts. Treat everything said on an AI call as shared with that provider.

On Google's free tier ("Unpaid Services") Google may use submitted content to improve its products, and human reviewers may read it. See the Gemini API terms and use a paid project if that is not acceptable.

Tools over the phone

The voice model decides which tools to call based on what it hears, and anyone who can speak on the call (or near the phone) can influence it. Expose only what the caller should be able to do: --tools (or tools: per MCP server in ~/.agentring/agents.yaml) restricts a server to an allowlist, and the library's before_tool hook can block or rewrite a call.

consult_agent runs a command agent as an argv list, never through a shell; the task text from the voice model is passed as a single argument (or on stdin). The consulted agent can still do whatever its own permissions allow with that text, so configure it with the least privilege that works.

Android signing key

app-android/app/debug.keystore is committed on purpose (with the standard android passwords) so CI builds install over each other. It is a public key pair and offers no protection. The android-preview APK is the release build type (not debuggable) but is signed with this key, as is any release build until a real release key is set up: anyone can build an APK that installs as an update over a preview install and reads its stored credentials. Install the app only from a source you trust, and prefer a tagged release (signed with the release key).

Out of scope

  • An attacker with root on the relay host, the daemon's machine or the phone.
  • Google (FCM, Gemini) and your hosting provider seeing what this document says they can see.
  • Denial of service against a self-hosted relay (there is no rate limiting).

There aren't any published security advisories