Security

How Mortar protects your data and your account, described from the code that runs it. Where a control depends on how Mortar is deployed, the page says so.

This page describes engineering controls. It is not a certification or an audit report. Certifications and independent audits: [[CERTIFICATIONS AND AUDITS]].

Where your data goes

Mortar runs as separate programs: a data plane that serves the API, a control plane that holds accounts and keys, and the website. In Managed Cloud, Mortar runs all of them. In Bring Your Own Cloud, the data plane runs in your environment and Mortar runs the control plane.

Data plane

Managed Cloud: run by Mortar. Bring Your Own Cloud: runs in your environment.

  • Serves the OpenAI-compatible API
  • Receives your prompts and returns completions
  • Keeps conversations in memory only
  • Logs token counts, not prompt or completion text

Control plane

Run by Mortar in both models.

  • Accounts, organisations, members and roles
  • Sessions, and API keys stored only as hashes
  • The audit log
The data plane asks the control plane one question, whether a credential is valid, and sends it nothing else.
Prompts and completions
Handled only by the data plane, which sends them to the model endpoint it is configured with (MORTAR_UPSTREAM) and nowhere else.
What reaches the control plane
The credential a request presents, an API key or the console's session cookie, so it can be verified. The call requires a shared secret of at least 32 random bytes in production. A valid answer is cached for 30 seconds and a rejection for 5.
Conversations
Held in the data plane's memory, belonging to the signed-in user or the API key's organisation, capped at 200 conversations per owner and 1,000 messages per conversation, and gone when the process restarts.
Account data
Stored by the control plane in one SQLite database: your email address, name and avatar URL, and each identity provider's id for your account; organisations, memberships and roles; sessions; hashed API keys, sign-in links and invitations; and the audit log.
The website
Keeps nothing on the server. For a signed-in visitor, it asks the control plane who you are to render each page.

Credentials and sessions

Mortar has no passwords. API keys, sign-in links and invitations come from the operating system's cryptographic random source, are shown or sent once, and are stored only as SHA-256 hashes.

API keys
192 random bits behind an mk_live_ prefix. The full key is shown once, when it is created. The control plane keeps its hash and its first 12 characters, enough to recognise a key in a list and too few to use it.
Each key belongs to one organisation and records when it was last used. Revoking a key takes effect within 30 seconds, the time the data plane may reuse an earlier check.
Sign-in links
256 random bits, stored hashed, valid for 15 minutes and for one use. Two requests racing with the same link cannot both succeed.
Invitations
256 random bits, stored hashed, valid for 7 days and for one use. Inviting the same address again replaces the earlier invitation.
Sessions
The cookie holds a random 192-bit session id and an HMAC-SHA256 signature made with a server secret of at least 32 bytes. Everything else stays on the server: who you are, your organisation and your role are read on every request, so a role change or a disabled account takes effect on the next one.
A session lasts 14 days from your last request. A new session id is issued at every sign-in, and signing out deletes the session on the server.
Comparisons
Session signatures, sign-in state and the internal token are compared in constant time. Keys and links are looked up by their hash, so the secret itself is never compared.

Sign-in with identity providers

Google and Microsoft Entra ID
OpenID Connect. The ID token's signature is checked against the provider's published keys (JWKS), together with its issuer, audience and expiry, and its nonce must match the one this sign-in started with. The authorisation request uses PKCE with S256.
GitHub
OAuth 2.0, which issues no ID token. Your profile and email addresses are read from GitHub's API with the access token it grants.
State
Every sign-in carries a random state value kept on the server and checked on return, so a callback this browser did not start is refused.
Linking accounts
A sign-in joins an existing account only through an email address the provider has verified: Google's email_verified claim, a work or school Entra directory, or GitHub's verified primary address. Personal Microsoft accounts never link by email.
Redirects
After sign-in, the browser is only ever sent back to an address on this site.
Development sign-in
The sign-in that skips identity checks, for local development, stops the service from starting if it is enabled in production.

Transport and browser protections

HTTPS
In production the services refuse to start unless every public origin is https://. TLS ends at nginx; the reference deployment allows TLS 1.2 and 1.3 only and binds the services to loopback addresses behind it.
HSTS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload on every production response.
Session cookie
__Host-mortar.sid in production: Secure, HttpOnly, SameSite=Lax, Path=/ and no Domain, so neither a subdomain nor a plain-HTTP response can set or replace it. See cookies.
Content Security Policy
Every page carries a policy with a fresh nonce: default-src 'none'; scripts and styles only from this origin or with that response's nonce; no 'unsafe-inline' or 'unsafe-eval'; frame-ancestors 'none', base-uri 'none' and object-src 'none'. API responses send default-src 'none'.
The site's own stylesheet and script load with Subresource Integrity (SHA-384) hashes.
Other headers
X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: same-site, Origin-Agent-Cluster: ?1, X-Permitted-Cross-Domain-Policies: none and X-DNS-Prefetch-Control: off.
Cross-site requests
A request that changes something is refused if its Origin is not on the allow-list or the browser marks it cross-site. The JSON APIs also refuse any body that is not application/json, which a form on another site cannot send. CORS answers only allow-listed origins.

Tenant isolation

Which organisation
Your organisation comes from your session on the server, never from a request body or URL, so editing a field cannot point a request at another organisation.
Roles
Membership and role are checked on every request. Managing API keys, members and invitations, and reading the audit log, need the admin or owner role.
Changes
Every change to an organisation's keys or members names that organisation in the database query, so guessing another tenant's key id cannot revoke it.
API keys
A key belongs to exactly one organisation, and requests made with it act for that organisation only.
Conversations
Every lookup goes through the owner the request authenticated as, so another owner's conversation is indistinguishable from one that does not exist.
Rate limits
Counted per API key where one is presented, so one tenant's traffic cannot use up another's allowance from a shared address.

Rate limits

Limits are coarse ceilings against abuse, counted in fixed windows. Over a limit, the answer is 429 Too Many Requests with Retry-After: 60.

What Limit
Inference API600 requests a minute per API key, or per client address without one
Control plane300 requests a minute per client address
Sign-in attempts20 every 15 minutes per client address
Sign-in link emails5 an hour per client address and recipient
Edge proxy, reference deployment50 requests a second per client address, with short bursts

A forwarded client address is believed only when MORTAR_TRUST_PROXY says a proxy is in front, and then only the entry that proxy added, so a caller cannot invent addresses to get fresh allowances.

Logging and audit

Request log
One structured JSON line per request: method, path, status, size, duration and request id. Query strings, headers, cookies and bodies are not logged, so OAuth codes and sign-in link tokens, which travel in query strings, never reach the log.
Request ids
Every response carries an X-Request-Id. Quote it when you report a problem and the matching log line can be found.
Secrets
Session cookies, API keys and the internal token are not logged. Errors return a generic message; the detail stays in the server log.
Completions
The data plane logs token counts, cache reuse and latency for each completion, not the text of prompts or completions.
Client addresses
Logged when a request is rate limited, and recorded with audit events.
Audit log
Organisation changes are recorded with who made them, when, and from which address: API keys created and revoked, members invited, removed or given a new role, invitations accepted or revoked, sign-in identities unlinked, and sign-outs. Admins can read recent events from the control plane's API.

Reporting a vulnerability

Email [[SECURITY CONTACT EMAIL]]. Include the affected URL or endpoint, the steps to reproduce the problem, and the X-Request-Id of a request that shows it, if you have one. We acknowledge reports within [[ACKNOWLEDGEMENT TIME]].

While testing
Use only accounts you own. Do not access or change other people's data, and do not degrade the service for others: no denial-of-service or volume testing.
Disclosure
Give us a reasonable time to fix an issue before you make it public.
Safe harbour
[[SAFE HARBOUR TERMS]]
Machine-readable
/.well-known/security.txt carries these contact details in the RFC 9116 format.