Security
How Trellaris keeps the credentials it holds β people's sessions, connector tokens, API keys, webhook secrets and your organisation's own secrets β and the standards each mechanism follows. The design decision behind all of it is recorded as ADR-041.
At a glance
| Credential | Where it lives | How it is protected |
|---|---|---|
| Your sign-in session | A session record on the server, and an encrypted, HttpOnly cookie in your browser | 15-minute access tokens signed ES256; refresh tokens checked against the session, rotated on every use, with reuse detection; 30-minute idle and 12-hour absolute limits enforced by the server |
| Connector tokens (Google, Microsoft, ServiceNowβ¦) | The refresh token in the vault; the access token only in the cache, for its own lifetime | Envelope-encrypted and bound to their owner; renewed on use; revoked at the provider when you disconnect |
| Your secrets and your organisation's | The vault | Envelope-encrypted; owned (personal, or org-level for admins); every read audited |
| API keys | A SHA-256 hash | Shown once; scoped; expire (90 days by default, at most a year); carry a checksum |
| Webhook URLs | A SHA-256 hash of the token | Shown once; rotatable; signing secrets encrypted in the vault |
| Platform keys | Azure Key Vault in the cloud | The vault's key-encryption key never leaves Key Vault; the process refuses to start on development defaults |
Sessions and tokens
However you sign in β company SSO, Google, GitHub β Trellaris opens a session on the server. The session record is the authority: tokens are only ever as valid as the session they belong to.
Access tokens
- JSON Web Tokens (RFC 7519) signed with ES256 β ECDSA on P-256 with SHA-256 (RFC 7518 Β§3.4) β and valid for 15 minutes.
- Each token names its signing key with a
kid, the key's JWK thumbprint (RFC 7638). The public keys are published as a JWK Set (RFC 7517) at/.well-known/jwks.json. A service that only needs to check tokens β the MCP gateway, a remote agent receiving an on-behalf-of token β reads that set and can never mint one. - The algorithm is fixed, not negotiated, and every token is checked for its issuer, expiry and type; a token signed with the old shared secret (HS256) is refused. This follows the JWT Best Current Practices (RFC 8725).
- Each access token carries its session id. Once the session ends β sign-out, an idle or absolute limit, a detected copy β the token is refused on its next request, not when it would have expired.
- Rotating the signing key keeps the previous public key in the JWK Set, so tokens already issued keep verifying until they expire.
Refresh tokens
A refresh token is opaque and checked against its session on every use. Nothing replayable is stored: the token carries the session id, a generation number and an HMAC (RFC 2104) under a key held β encrypted β in the session record.
- Rotation. Every refresh issues the next token and retires the one presented.
- Reuse detection. If an older token is ever presented again, it has been copied. The session ends and every copy with it, and the event is audited. This is the refresh-token protection the OAuth 2.0 Security Best Current Practice recommends (RFC 9700 Β§4.14.2).
- No false alarms from your own tabs. Two tabs refreshing in the same instant present the same token; within a 60-second grace window the second receives the same new token rather than being treated as a thief.
- Server-side limits. A session idle for 30 minutes, or 12 hours old, is ended at its next refresh whatever the browser believes, and a person whose account has been deactivated cannot refresh at all.
- Switching organisation ends the current session and opens a new one for the other organisation, so signing out always ends what your browser holds.
The browser never holds a platform token
The web app keeps the tokens in an encrypted Auth.js session cookie marked
HttpOnly, Secure and SameSite=Lax
(RFC 6265 and its successor draft), which page scripts cannot read. Every API
call goes through the app's own /api route, which attaches the
token on the server. A script injected into a page therefore has no
token to steal. This is the backend-for-frontend pattern described in the IETF
guidance on OAuth for browser-based applications.
- Writes authenticated by the cookie must come from the app's own origin: a
request the browser labels cross-site (via
Sec-Fetch-SiteorOrigin) is refused. - A Content-Security-Policy limits where pages can load
scripts from and send data to, forbids framing the app, and blocks plugins.
The app also sends
X-Content-Type-Options: nosniff, a strict referrer policy and a permissions policy. - The idle logout in the browser is still there, but it is no longer the only control β the server enforces its own.
The vault
Every credential Trellaris stores β in its own table or inside another component's configuration β is sealed and opened through one piece of code.
Envelope encryption
- Each value is encrypted with its own random 256-bit data key using AES-256-GCM, an authenticated encryption scheme (NIST SP 800-38D; the AEAD interface of RFC 5116).
- The data key is wrapped by a key-encryption key. In the cloud that key is an RSA key in Azure Key Vault, and the wrapping is RSA-OAEP with SHA-256 (RFC 8017) performed inside Key Vault: the application asks Key Vault to wrap and unwrap, authorised by its workload identity, and never holds the key. A copy of the database together with the application's environment is not enough to read anything.
- Unwrapped data keys are cached in memory for a few minutes, so Key Vault is not asked on every read.
Bound to where it belongs
Every value is encrypted with associated data naming what it is for, which organisation it belongs to, and exactly where it is stored. A ciphertext copied into another row, another organisation or another purpose fails to decrypt instead of quietly opening as someone else's secret.
Rotation
Each value records which version of the key-encryption key wrapped it. After a
rotation, new values use the new version while old ones still open, and a
daily re-seal task re-wraps everything under the current version. The same
task upgraded every value stored before this design. Administrators can see
counts by purpose and key version β never values β at
GET /secrets/inventory.
Who may touch a secret
| Kind | Who manages it | Example |
|---|---|---|
| Personal | Its owner only | An API token you use for an MCP server you registered; your passenger details for the Travel Desk |
| Organisation | Org admins only | A connector's OAuth client secret |
| Platform-managed | Nobody, through the API β only the component that owns it | Your connector tokens; a remote agent's credentials |
- A connector's client secret is read only from organisation level, so a member's personal secret with the same name can never stand in for it.
- You can point an MCP server β whose address you choose β only at a secret you manage. Otherwise registering a server you control could deliver somebody else's secret to it.
- Every time a secret is decrypted, the audit log records which secret, for what purpose, and for which run β never the value.
Connector tokens
Connectors act as the person who connected them, under a delegated OAuth 2.0 grant (RFC 6749) obtained with PKCE (RFC 7636) and sent to providers as a bearer token (RFC 6750).
- Only the refresh token is stored durably. The access token β an hour of your identity β is kept only in the cache, encrypted, for its own lifetime. A grant that cannot be renewed keeps its access token, because it is all there is.
- Renewed when used. An expiring token is renewed at the moment an agent needs it; if a provider still answers 401, Trellaris renews once and retries once.
- One renewal at a time. Providers that rotate refresh tokens (RFC 9700 Β§4.14.2) invalidate the old one on use, so renewals of the same connection are serialised across every server, and each renewal is saved the moment it succeeds.
- An outage is not a disconnection. A timeout or a provider
error leaves the connection active and retries later. Only a refused grant β
the OAuth errors
invalid_grant,invalid_clientorunauthorized_client(RFC 6749 Β§5.2) β marks it as needing reconnection, and tells you in your inbox. - Disconnecting revokes at the provider. Where the provider offers the standard revocation endpoint (RFC 7009) β Google, Salesforce, Zoho, Box β the grant is revoked there as well, and the stored tokens are destroyed. This also happens when an account is deprovisioned through SCIM.
API keys and webhooks
- API keys are high-entropy random values stored only as a SHA-256 hash and shown once. New keys expire after 90 days unless you choose 30, 180 or 365. Each ends with a checksum, so a mistyped or truncated key is refused before any lookup, and a leaked key can be recognised by a scanner without asking the platform.
- Webhook URLs contain a random token that is stored only as a hash and shown once, when the automation is created. If you lose it, issue a new URL from the automation; the old one stops working immediately.
- Webhook signatures use HMAC-SHA256 (RFC 2104) over the request body. The signing secrets β for incoming webhooks and for outgoing deliveries β are stored encrypted in the vault and never returned by the API.
Refusing unsafe configuration
Every signing and encryption secret has a development default so the platform runs with no configuration on a laptop. Outside development those defaults would be published keys, so the API and the worker refuse to start without a token-signing key, with the default master key as the vault's key-encryption key, or with the default exchange secret β and say which setting is missing.
Standards referenced
| Standard | Title | How Trellaris uses it |
|---|---|---|
| RFC 7519 | JSON Web Token (JWT) | Access, agent and on-behalf-of tokens |
| RFC 7515 | JSON Web Signature (JWS) | How those tokens are signed |
| RFC 7518 | JSON Web Algorithms (JWA) | ES256 (Β§3.4) as the only accepted algorithm |
| RFC 7517 | JSON Web Key (JWK) | The published key set at /.well-known/jwks.json |
| RFC 7638 | JWK Thumbprint | Each signing key's kid |
| RFC 8725 | JWT Best Current Practices | A fixed algorithm, issuer and type checks, no shared-secret fallback |
| RFC 6749 | The OAuth 2.0 Authorization Framework | Connector consent and renewal; the error codes (Β§5.2) that decide whether a failed renewal is final |
| RFC 7636 | Proof Key for Code Exchange (PKCE) | Every connector consent flow |
| RFC 6750 | OAuth 2.0 Bearer Token Usage | How connector and platform tokens are presented |
| RFC 7009 | OAuth 2.0 Token Revocation | Revoking a grant at the provider on disconnect or deprovisioning |
| RFC 9700 | Best Current Practice for OAuth 2.0 Security | Refresh-token rotation with reuse detection (Β§4.14.2); serialised renewal for rotating providers |
| RFC 2104 | HMAC | Refresh-token integrity; webhook signatures (HMAC-SHA256) |
| RFC 5116 | An Interface for Authenticated Encryption | AES-256-GCM with associated data binding each value to its place |
| NIST SP 800-38D | Galois/Counter Mode (GCM) | The vault's authenticated encryption |
| RFC 8017 | PKCS #1 v2.2 | RSA-OAEP key wrapping in Key Vault |
| RFC 5869 | HKDF | Deriving development-only keys, and the local key-encryption key when no keyring is configured |
| RFC 5958 | Asymmetric Key Packages (PKCS #8) | The format of the token-signing private key |
| RFC 6265 | HTTP State Management (cookies) | The HttpOnly, Secure session cookie (SameSite from its successor draft) |
| RFC 6797 | HTTP Strict Transport Security | Sent by the ingress in the cloud deployments |
| RFC 9457 | Problem Details for HTTP APIs | Every refusal, e.g. auth.session_ended, secret.forbidden |
| W3C CSP Level 3 | Content Security Policy | Limits on script sources, connections and framing |
| W3C Fetch Metadata | Fetch Metadata Request Headers | Refusing cross-site writes (Sec-Fetch-Site) |
| IETF draft | OAuth 2.0 for Browser-Based Applications | The backend-for-frontend pattern: no token in the browser |