Back to Home

Trust

Security

gork.email holds mail for autonomous processes, so the security model is built around one assumption: every credential and every byte of content is attacker-controlled until proven otherwise. This page describes what we actually do — not what an audit deck would say.

The short version

Keys are hashed at rest and scoped to the minimum they need. Every request is filtered to one organization. Webhooks are signed and replay-resistant. Inbound mail is signature-verified. Received content is treated as hostile and sanitized before it is ever stored or rendered. There is no path that skips these.

01.Credentials are never stored

API keys are shown exactly once, at creation. We keep a salted SHA-256 hash and nothing else — a database dump does not yield a usable key. Every comparison runs in constant time, so a timing side-channel cannot recover a key byte by byte.

Webhook signing secrets can be rotated at any time; rotation takes effect on the next delivery and the previous secret stops verifying immediately.

02.Least privilege, enforced per request

A key carries an explicit scope list, drawn from a closed vocabulary: inboxes, messages, threads, webhooks, keys, domains, suppressions and organization. A read-only key cannot send; a send-only key cannot read a key list or rotate a secret. Wildcards are supported but must be requested explicitly.

Keys can be bound to a single inbox, which narrows them further: that key sees only that inbox's messages, threads and attachments, and is refused on every other inbox.

03.Tenant isolation on every query

Multi-tenancy is enforced in the application layer: the caller's organization is resolved from their membership on every request and applied as a filter to every database query. There is no code path that reads a customer row without an organization filter.

An inbox-bound key adds a second filter on top, so a scoped key cannot reach its neighbours even inside the same organization.

04.Signed webhooks and verified ingress

Every webhook delivery carries an HMAC-SHA256 signature in X-Gork-Signature over a canonical string that binds the method, path, organization, user, timestamp and a hash of the body. Signatures are compared in constant time and rejected outside a freshness window, which is what makes a captured request non-replayable against a different route.

Inbound mail is accepted only from a verified Amazon SES receipt: the SNS envelope's signature is validated against the certificate named in the message, and the topic ARN must match. Unsigned envelopes are refused unless the worker is explicitly configured to allow them, and that switch is read from the environment — never from the payload, so it cannot be smuggled in by a sender.

05.Idempotent, retryable, observable

Sends and webhook deliveries are idempotent. Supplying an Idempotency-Key makes a retry replay the original result instead of delivering a second copy, so an agent that times out and retries cannot double-send.

Queues retry with exponential backoff and dead-letter anything that cannot succeed. Every send carries an X-Request-Id that is stamped on logs and on the webhook payload, so one message can be followed from your API call through to delivery.

06.Zero trust on email content

Message content is hostile input. We strip scripts and event handlers, sanitize HTML, cap payloads at 15MB, and guard against nested multipart and decompression bombs. Nothing in a received message is ever executed or fetched on our behalf.

Public and unauthenticated surfaces are rate limited per IP, and the sign-in flow is gated by Turnstile. The auth surface refuses to start in production without its CAPTCHA secret rather than silently running unprotected.

Reporting a vulnerability

If you find a security issue, email security@gork.email with reproduction steps. We will acknowledge within 2 business days and keep you updated until it is resolved. Please do not test against other customers’ data or run automated scans against the production API.

Security-relevant changes are recorded in the changelog. Our current component list is on the subprocessors page.