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.
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).
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.
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.
- 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 newAGENTRING_API_KEYon 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 withroom.auto_create: false. On the phone the JWT andpush_secretare 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.
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.
- The relay refuses to start with a non-
httpspublic URL unlessAGENTRING_INSECURE_DEV=1. - The app rejects
http://relay URLs andws://LiveKit URLs unless the host is private:localhost,*.local,::1,10/8,127/8,172.16/12,192.168/16and100.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-Forpresent).
- 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 thePOST /v1/callsresponse; a phone gets its token only fromaccept/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.
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.
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.
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).
- 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).