Security
This page lists only things that are implemented and tested. It is a description of the code, not a certification. The service is in early access (see status): there is no SOC 2 report, no published penetration test and no bug bounty.
Authentication#
| Area | What happens |
|---|---|
| API keys | Generated from 24 random bytes, prefixed am_. Only a SHA-256 hash is stored, so the secret is shown once at creation and cannot be recovered. Keys can be limited to a pod or inbox and to named permissions. |
| Dashboard passwords | Hashed with scrypt (N=2^15, r=8, p=1) and a per-user salt. A password change rotates the session. |
| Dashboard sessions | Opaque session ids stored hashed, in a cookie that is HttpOnly and SameSite=Lax, and Secure in production. Every state-changing session request needs a CSRF token and a same-origin Origin. API-key requests are not cookie based, so CSRF does not apply to them. |
| Two-step verification | TOTP (RFC 6238) with single-use recovery codes, which are stored hashed. Codes cannot be replayed. See two-step verification and login security. The dashboard shows the setup secret and an otpauth link, not a QR code. |
| Login protection | Failed logins are throttled per email and IP with a capped, growing delay so strangers cannot lock you out from elsewhere. Sign-ups, verification emails and agent sign-ups are rate limited too. |
| Email verification | Sending and creating API keys are locked until the owner email is verified. Agent-created accounts are send-locked until a human verifies a 6-digit code (24 hour expiry, 10 attempts). |
| Reserved names | Addresses such as postmaster@ and noreply@ on the shared domain cannot be registered. |
Isolation between customers#
- Every query is scoped to your organization. Resources outside your key's scope answer
404 not_found, never403, so their existence is not revealed. - A pod-scoped key reaches only that pod. An inbox-scoped key reaches only that inbox and cannot create inboxes, pods or domains. A key can never be given more permissions than the key that creates it.
- Organization-level lists are not reachable with pod or inbox scoped keys.
- The hosted MCP endpoint runs each tool call as an ordinary request carrying your own key, so permissions, scoping, plan limits and isolation are exactly the REST ones.
- An independent read-only review checked isolation across threads, messages, drafts, lists, pods, domains, keys, search and the WebSocket and found no cross-tenant access; it did find and we fixed permission gaps on webhook, events, metrics and suppression routes.
Rate limits#
All limits are counted in memory per server process. Defaults:
| Limit | Default |
|---|---|
| Requests per IP | 600 per minute |
| Requests per API key | 1200 per minute |
/auth/* requests per IP | 30 per minute |
| Open-tracking pixel per IP | 3000 per minute (separate bucket) |
| Sends per inbox | 100 per rolling 24 hours |
| Recipients per message | 50 |
Limited requests get 429 rate_limited and a Retry-After header. Because the counters live in memory they reset when the server restarts and are not shared across servers; the service currently runs as a single server.
Webhooks#
- Every delivery is signed. Headers
svix-id,svix-timestampandsvix-signaturecarry an HMAC-SHA256 overid.timestamp.body, keyed with yourwhsec_secret. Verify the raw body and reject timestamps more than five minutes old. A valid signature can be replayed inside that window, so dedupe onsvix-id. See webhooks. - The signing secret is returned only when the webhook is created. Get, list and update never include it. Custom header values are write-only.
- Webhook URLs must be public
httpsURLs. Delivery resolves DNS inside the connection, blocks private, loopback, link-local and metadata addresses (so DNS rebinding does not help), does not follow redirects, times out after 10 seconds and caps the response it reads. Delivery logs record only coarse error classes. - Attachments fetched by
urlgo through the same guard: publichttpsonly, 10 MB per file and 25 MB in total.
Data handling#
- Where it lives. Mail and account data are stored in one SQLite database on a persistent disk attached to the service's single server. There is no region choice and no multi-region copy. The server refuses to start in production without a persistent disk, so a deploy cannot silently wipe accounts.
- Backups. The database is copied online every 6 hours by default (
VACUUM INTO) and the newest 14 copies are kept. The copies are written to the same disk. Copying them somewhere else is an operator task that is not automated by the code, so treat the disk as the failure domain until that is in place. A restore tool checks the backup with an integrity check before replacing the live database. - Logs. Request logs are JSON lines. Passwords, tokens, secrets, authorization headers and
api_keyquery values are redacted. MCP request logs record the JSON-RPC method and tool name only, never arguments or mail content. - Open tracking. Off unless requested per message. The pixel stores an open count and first and last open time. It never stores the IP address or user agent. See open tracking.
- Attachments. Downloads are served with
Content-Disposition: attachment,X-Content-Type-Options: nosniffandContent-Security-Policy: sandbox. The dashboard never renders email HTML.
Transport and browser headers#
HSTS is sent in production. Responses carry X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer and a restrictive Content-Security-Policy. CORS is closed unless the operator lists origins. The /mcp endpoint refuses any request that carries an Origin header.
Reviews and known gaps#
Two independent reviews (Phase 3 and Phase 4) produced fixes with regression tests, among them server-side request forgery through attachment and webhook URLs, missing permission checks, sign-up takeover, quota counting per recipient, a metrics query that could stall the server, and a prompt-injection exfiltration path in the LangChain toolkit. The log is docs/SECURITY.md in the repository. Items we know about and have not fixed:
- Rate limits, login throttles and sign-up limits are in memory (single server, reset on restart).
- Custom
headerson send are passed through, soReply-ToandReturn-Pathcan be overridden by the sender.Fromcannot. - Mail received over the built-in SMTP receiver, if an operator enables it, carries no SPF or DKIM verdict. Production inbound arrives through the email provider's receiving.
- Billing provider calls have been tested against simulated providers, not live test accounts yet.
- Backups are not copied off the server by the application.
Reporting a problem#
A public security contact address has not been published yet. Until it is, report issues through the support contact you were given when you signed up. Do not put secrets in a report.