Security model
A valid certificate chain alone never authorizes a call. Every request is checked against a fresh, signed, target-scoped policy at your gateway, and every change is recorded with per-gateway evidence.
Trust boundaries
- Your API traffic stays inside your ingress. Mutual Trust receives configuration, enrollment requests, signed acknowledgments and verification receipts, never request bodies, headers or payloads.
- Workload private keys are generated and kept on partner machines. Mutual Trust never collects them; it sees CSRs and signatures.
- Each trust space has its own private root and issuing CA. There is no automatic trust across customers; the explicit grant is the only cross-organization edge, and a partner sees only its side of it.
- The signer is a separate deployment reachable only through an internal service binding, with its own caller secret. It independently re-checks every issuance intent before using its key provider and never falls back to software when a hardware provider fails.
- Customer, automation, staff and signer traffic use separate principals and audiences: portal sessions only on
app.mutualtrust.io, bearer credentials onapi.mutualtrust.io, Cloudflare Access assertions onadmin.mutualtrust.io.
Threat model
| Threat | Controls |
|---|---|
| A stolen client certificate without its key | TLS proof of possession; keys never leave the workload. |
| A stolen workload key | Short (24 h) leaves; revoke the leaf (key_compromise), enforced per request via the signed deny set; re-enroll with a fresh attestation. |
| A certificate from another tenant or space | Gateways trust only the issuing CAs in their own service’s signed policy and authorize on (principal, issuer) pairs. |
| A forged, replayed or downgraded policy | ES256 JWS with a key pinned at authenticated pairing; target binding; 5-minute expiry; monotonic (epoch, revision) high-water mark persisted across restarts; same-revision hash check. |
| A gateway cut off from the control plane | Cached policy is honoured only until expiry + 30 s, then protected routes fail closed; connections are drained and capped at 5 minutes. |
| Path confusion and identity-header spoofing | Ambiguous paths are refused, including encoded separators, dot segments, path parameters (;) and double encodings; the gateway strips inbound x-mt-* and client-certificate headers in any spelling (hyphen or underscore) and method or URL override headers such as X-HTTP-Method-Override and X-Original-URL, sets its own headers after hop-by-hop processing, and forwards a verified principal only over the protected origin path. |
| Invitation interception or reuse | One-time, hashed, time-limited secrets bound to the intended organization; atomic single-use acceptance. |
| Enrollment replay | Single-use 5-minute challenges bound to identity, environment, grant and public key; single-use sandbox tokens; OIDC tokens limited to one hour of age, exact iss/aud/sub, keys only from approved JWKS URIs on public https hosts, fetched without following redirects. |
| Mis-issuance | The server derives every certificate field; the signer re-checks tenant, environment, grant, issuer, key version, algorithm, profile and the exact TBS digest, and re-derives the leaf profile from the database (the grant’s own workload URI as the only SAN, the allocated serial, the service’s issuing CA, the profile’s extensions only, and a validity within 24 hours, the grant and the issuer); the result is verified before delivery and only one certificate can be committed per intent. |
| Cross-site request forgery | Host-only __Host- cookie with SameSite=Lax plus a per-session CSRF token on every write. Organization sign-in binds its OIDC state to the browser that started it, so a callback link from someone else’s login cannot sign you in. |
| Over-privileged automation | API credentials are scoped to one organization and a subset of the creator’s roles, re-intersected with the creator’s current roles on every request, expiring, and revocable; credentials cannot mint credentials. |
| Staff misuse | Separate identity (Cloudflare Access with MFA), narrowly scoped roles, read-only by default, customer-approved expiring support grants, an append-only access log, and dual control for key destruction and refunds. |
| Payment-event forgery | Stripe signatures verified on raw bytes with a 5-minute tolerance; entitlement re-read from Stripe, never inferred from browser redirects. |
Gateway verification rules
- The gateway holds a fresh manifest (≤ 5 minutes after issue, plus a 30-second clock allowance), correctly scoped to its tenant, environment, service and gateway, and not a replay or downgrade. Otherwise: deny
uninitializedormanifest_expired. - The client presented a certificate whose chain verifies to an issuing CA in the current signed trust list.
- The leaf carries exactly one workload URI SAN for this environment and the clientAuth EKU, and is within validity.
- The leaf’s serial is not in the manifest deny set for its issuer.
- The request path normalizes unambiguously.
- An active, unexpired grant maps (principal, issuer) to the normalized route and HTTP method.
- Otherwise: deny. The check runs on every request, including resumed and keep-alive sessions. TLS success and authorization are recorded as separate verdicts.
Tenant isolation
- Records, objects, signed manifests and policy caches are scoped by tenant and environment; tables use tenant-qualified foreign keys, and authorization is checked at the repository boundary.
- Inaccessible resources return
404so their existence is not disclosed. - Background jobs re-resolve the service under the event’s tenant before acting and discard untrusted hints.
- Receipts that name a grant from a different service are stored without the grant reference.
- Every state change, its audit entry and its outbox event commit atomically; a zero-row guarded update rolls back the whole batch.
Key custody
| Key | Custody |
|---|---|
| Workload keys | Generated and held by the partner workload. Never sent to Mutual Trust. |
| Gateway keys | Generated by the gateway; only the public key is registered at pairing. Used to sign acknowledgments and receipts. |
| Issuing CA keys | Production: hardware-protected, non-exportable keys (EC_SIGN_P256_SHA256) provisioned with Sectigo PKI Operations, accessed with short-lived federated credentials and an allowlist of exact key versions. Sandbox: software-backed keys wrapped (AES-GCM) in signer-only storage. |
| Root keys | Production: ceremony-controlled under a separate PKI operator principal; the online service cannot sign roots. |
| Manifest-signing keys | Separate per-space key, used only for policy manifests and bound to the space’s services. |
The signer validates the provider’s public-key fingerprint before signing, checks CRC32C integrity both ways with Cloud KMS, verifies every signature locally, and refuses CA or non-clientAuth output on the issuance path. Key destruction requires two different PKI key custodians.
Sandbox and production
Sandbox credentials are issued from dedicated test roots labelled SANDBOX TEST and are meant for evaluation and integration work. Production trust spaces are provisioned with Sectigo PKI Operations on hardware-protected keys. The two environments never share issuers, keys or credentials: production deployments refuse software signing keys and reject sandbox chains. See Sandbox and production.
Control-plane interruption
Gateways refresh signed policy every 15 seconds and keep enforcing the last valid policy for up to 5 minutes (plus a 30-second clock allowance). After that, protected routes fail closed. Long-lived connections are drained when policy becomes stale or a principal is denied, and every connection has a maximum age of 5 minutes. When the signer is unavailable, new issuance stops but existing leaves keep working while policy is fresh; because manifests are signed by the same service, a prolonged signer outage also stops policy refresh and gateways then fail closed.
Evidence
A last applied revision is evidence about one gateway only, never evidence of enforcement everywhere. The portal and API show configured, delivered, applied and verified as distinct states, and list offline gateways as pending. Receipts and acknowledgments are signed by the gateway key, so they can be attributed to a specific paired gateway.
Staff access controls
- Staff use a separate application at
admin.mutualtrust.iobehind Cloudflare Access with an MFA policy; it never reads customer cookies. See Identity and access. - Every staff request is logged (identity, action, tenant, stated purpose, request ID) before it runs; if logging fails the request is refused. The log is append-only.
- Views are read-only by default. Writes require an explicit reason and a role: support cannot change payment methods (there is no endpoint), finance roles cannot change trust or grant scope.
- Support actions on a tenant (
audit.detail,invitation.resend) need an unexpired, customer-approved support grant naming that staff member. - Emergency issuer suspension is limited to incident commanders and trust operations, requires a reason and is flagged for post-incident review.
- Credits and refunds need a second, different approver; key destruction needs a second, different custodian.
- Every staff write appears in the customer’s audit log with
actor_type: "staff".
Data handling
- Collected: organization and member names and emails, configuration, CSRs and issued certificates, audit events, and receipts (gateway, verdicts, principal, serial, method, normalized route, time).
- Never collected: private keys, request or response bodies, full TLS traces, or credential tokens (secrets are stored only as hashes).
- Adoption metrics are pseudonymous cohorts and milestone names, without personal data or tokens.
- Proposed retention: decision receipts 30 days, administrative audit 12 months.