M mctl-telegram
Security model

Security model

Threat model, cryptographic invariants, Communication Agent controls, and known limitations for this deployment.

Not zero-knowledge (hosted mode). mctl-telegram makes outbound MTProto calls to Telegram on your behalf. The running server handles plaintext Telegram data while processing requests and, when Communication Agent is enabled, decrypts encrypted agent message state for listener/queue processing. You are trusting both the deployment operator and the integrity of this code. For a stronger session-storage model, see Local Bridge mode below.

Reporting vulnerabilities

Email [email protected]. Please do not open public GitHub issues for unpatched vulnerabilities. Acknowledgement target: 72 hours. Fixes are disclosed in the CHANGELOG once a release ships.

End-user support

For product help (setup, disconnect, billing questions): email [email protected]. Do not use the security address for general support.

Trust boundaries

BoundaryTrusted credential / stateLives
Inbound HTTP (ChatGPT / Claude / MCP clients → /mcp) OAuth JWT issued and verified by this server; legacy mode may use api.mctl.ai Bearer header, per request
Outbound MTProto (gotd/td → Telegram) Encrypted per-user session blob telegram_accounts.session_encrypted
Communication Agent listener and queue Per-user listener profile, encrypted message/event rows, queue jobs, conversation and policy state User-scoped Postgres agent tables; active only for listener-enabled profiles

Every database lookup for user content is intended to be scoped to the owning user. Compromise of one account should not expose another account unless the service, key derivation, or deployment boundary itself is compromised.

What the server sees

Communication Agent safety boundary

The model prompt and model output are not trusted as a security boundary. The server-side listener, state machine, policy engine, approval flow, and Telegram send gate remain authoritative.

Enabling the listener does not bypass the existing ALLOW_SEND, scope, per-account, and per-peer send gates.

Communication Agent storage and retention

Local Bridge mode

When an account is set to mode='local', its MTProto session lives on the user's machine instead of the hosted server:

What logs and audit rows never persist

This guarantee applies to logs and audit rows. Communication Agent message/action tables intentionally store encrypted content as described above.

Known coupling: shared-hmac-legacy mode

AUTH_MODE=shared-hmac-legacy validates JWTs using secret material shared with an external authorization service. This coupling does not apply to the default local-jwt mode.

Cryptographic invariants

Send gate (defense in depth)

On production tg.mctl.ai, ALLOW_SEND=false is the safe default. Real Telegram sends through send_message or send_media require all of:

  1. Server flag ALLOW_SEND=true.
  2. Identity has telegram:messages:send scope.
  3. telegram_accounts.send_enabled = true for that operator.
  4. The per-peer send rate limit is not exhausted.

Any failed condition returns a dry-run result (sent=false) with a dry_reason; no send RPC reaches Telegram. A denied media call does not fetch its URL or decode its base64 payload.

Authentication-required mode

Rate limiting

The default per-identity token bucket is 30 requests/minute with the same burst. Write paths additionally use per-peer limits so one recipient cannot be targeted up to the global ceiling. Public and well-known endpoints are limited at the ingress layer.

Session TTL

Tamper-evident audit log

Audit rows form a SHA-256 hash chain using prev_hash and entry_hash. You can ask the server to verify the chain:

GET /api/account/audit/verify
{
  "ok": true,
  "verified": 42
}

A non-OK response includes first_bad_id and a reason. Rows created before hash chaining are skipped.

Prompt-injection content boundary

Read tools wrap Telegram message bodies in <telegram-content origin="telegram" peer="redacted" untrusted="true">…</telegram-content> and escape literal closing tags inside message text. This is a signalling boundary for the MCP client, not a substitute for the server-side Communication Agent policy engine.