Identity and access

Passkeys are the primary way to sign in to Mutual Trust, for customers and for Sectigo staff. Organizations that want central control can add enterprise SSO through OIDC. Customer and staff sign-in are separate applications with separate audiences and sessions.

Documentation menu

Passkeys

A passkey is a WebAuthn credential held by your device, browser or password manager. The private key never leaves it, sign-in is bound to app.mutualtrust.io so it cannot be phished from a look-alike site, and there is no password to reuse or reset.

  • Create an account. On app.mutualtrust.io, choose Create account, enter your name and email, and confirm with your device. Your first passkey is created with the account.
  • Sign in. Choose Sign in with a passkey and confirm with your fingerprint, face, device PIN or security key.
  • Add and remove passkeys. In your account settings, add a passkey for each device you use and remove any you no longer trust.

The user is identified by their Mutual Trust account, not by the passkey, so adding or removing a passkey never changes organization memberships or roles.

Recovery codes

When you create an account with a passkey, Mutual Trust shows ten one-time recovery codes, once. Save them somewhere other than the devices that hold your passkeys, such as a password manager or a printed sheet. Only a hash of each code is stored, so nobody at Mutual Trust can read them back.

  • Lost every passkey? Choose Lost your passkey? Use a recovery code on the sign-in page and enter one code. Letter case, spaces and hyphens do not matter.
  • A code opens a restricted session. It lasts 15 minutes, has no organization access and can do one thing: register a new passkey. The session ends as soon as the passkey is registered; sign in with the new passkey to continue, then remove the lost passkeys from My profile.
  • Each code works once. My profile > Recovery codes shows how many are left. Generate new codes issues a fresh set of ten and invalidates every earlier code; it is a high-risk change, so it needs a sign-in from the last 15 minutes.
  • Passkey accounts only. Accounts that sign in through organization single sign-on recover through their identity provider.

Redeeming codes is rate limited per network address. API: POST /auth/passkey/recovery {"code":"mtrc_…"} (app host, same origin), GET /v1/me/recovery-codes for the count, and POST /v1/me/recovery-codes to regenerate.

Deployment sign-in (OIDC)

For organizations that manage access centrally, the portal is also an OIDC relying party. On the hosted service the identity provider is a Cloudflare Access for SaaS OIDC application, which in turn federates your enterprise IdP. Any standards-compliant OIDC provider works the same way.

Access for SaaS application
NameMutual Trust portal sign-in
TypeSaaS, auth type OIDC
Redirect URIhttps://app.mutualtrust.io/auth/callback
Grant typeAuthorization code with PKCE
Scopesopenid email profile
Session duration12 hours
PolicyAllow any authenticated user. Mutual Trust authorizes per organization membership after sign-in.

Configure the control plane (mt-api) with:

mt-api sign-in settings
SettingValue
OIDC_ISSUERThe application’s issuer, for Access: https://<team>.cloudflareaccess.com/cdn-cgi/access/sso/oidc/<client_id>
OIDC_CLIENT_IDThe application’s client ID.
OIDC_CLIENT_SECRETThe client secret (confidential clients; shown once in the Zero Trust dashboard).
Store the settings as Worker secrets
printf '%s' 'https://<team>.cloudflareaccess.com/cdn-cgi/access/sso/oidc/<client_id>' | npx wrangler secret put OIDC_ISSUER --name mt-api printf '%s' '<client_id>' | npx wrangler secret put OIDC_CLIENT_ID --name mt-api npx wrangler secret put OIDC_CLIENT_SECRET --name mt-api

Leave them unset on a deployment that offers passkey sign-in only; /auth/login then answers 503 oidc_unconfigured.

The sign-in flow

  1. GET /auth/login?return_to=/… fetches the issuer’s discovery document (cached for an hour; its issuer must match), stores a hashed state with a PKCE verifier and nonce for 10 minutes, and redirects with code_challenge_method=S256.
  2. GET /auth/callback consumes the state (single use), exchanges the code at the token endpoint with the verifier, and verifies the ID token.
  3. The user is identified by the pair (issuer, subject); email and name are used for display only. A new session cookie __Host-mt_session is issued and the browser returns to return_to (same-site paths only).

How ID tokens are verified

  • Signature with RS256 or ES256 using the issuer’s JWKS (cached 10 minutes), selecting the key by kid.
  • iss equals the configured issuer (ignoring a trailing slash); aud includes OIDC_CLIENT_ID.
  • exp not passed and nbf reached, each with 30 seconds of clock skew.
  • nonce equals the value stored for this login.

The same verifier, with an approved issuer’s JWKS URI and the workload binding’s audience and subject, checks OIDC workload tokens.

Your organization's identity provider

Each organization can connect its own OpenID Connect identity provider (Okta, Microsoft Entra ID, Google Workspace, Ping, Auth0 or any standards-compliant provider). Members whose email domain the organization routes to it sign in there with authorization code and PKCE; identities are bound by (issuer, subject).

  1. Verify the email domain under Organization, Verification (a DNS TXT record). Only verified domains of your own organization can be routed, and a domain routes to one identity provider.
  2. In your provider, register a web application with the redirect URI https://app.mutualtrust.io/auth/sso/callback, scopes openid email profile and the authorization code grant.
  3. Under Organization, Single sign-on, or PUT /v1/organizations/{id}/sso, enter the issuer URL, the client ID, the client secret (optional: omit it for a public client that uses PKCE only) and the domains. This is a high-risk change and needs a fresh sign-in.
  4. Sign in once through the provider: the connection records its first successful sign-in.

How Mutual Trust protects the connection:

  • The issuer must be an https URL on a public host. Discovery (/.well-known/openid-configuration), the JWKS and the token request never follow redirects, are bounded in size and time, and every endpoint the discovery document names must itself be public https. The document's issuer must equal the configured one.
  • The client secret is sealed with AES-256-GCM under a key that exists only as a Worker secret, bound to your organization and connection. It is never stored in plaintext and never returned; the API reports only client_secret_set.
  • The ID token must assert an email in a domain you routed to that provider (and not email_verified: false); anything else is refused. The state is single use and bound to the browser that started the sign-in.

On the sign-in page, Continue with your organization asks for a work email, looks up the domain (GET /auth/sso/discover?email=) and goes to your provider (GET /auth/sso/start). Passkey sign-in stays available for everyone else.

SSO-only sign-in and break-glass owners

An owner can enforce SSO-only sign-in (POST /v1/organizations/{id}/sso/enforcement). Sessions that did not come through your provider then carry no roles in your organization; GET /v1/me lists the organization under sso_required. Owners you designate as break-glass keep passkey sign-in, so a provider outage cannot lock the organization out. Enforcement needs a tested connection and at least one break-glass owner, and the person enabling it must keep access. Changing the issuer or client ID turns enforcement off until the new connection is tested. API credentials are machine identities and are not affected; revoke them separately if you need to.

SCIM provisioning

Your provider can create, update and deactivate members over SCIM 2.0 (RFC 7643 and RFC 7644) at https://api.mutualtrust.io/scim/v2.

  • Token. An owner creates a bearer token under Organization, Provisioning (SCIM) (POST /v1/organizations/{id}/scim/tokens, fresh sign-in). It starts with mtscim_, is shown once and stored only as a hash. Rotate it (the old one stops working at once) or revoke it at any time. A token reaches only its own organization.
  • Users. /Users supports create, read, replace (PUT), PATCH (Okta and Entra ID styles, including "Replace" and string booleans), deactivate (active: false) and delete; filters userName eq, externalId eq and id eq; startIndex and count pagination (at most 200 per page); weak ETags with If-Match on writes. A provisioned person is bound to their provider subject at their first SSO sign-in.
  • Groups. /Groups supports the same operations, member add and remove (including members[value eq "..."]), displayName eq filters and excludedAttributes=members.
  • Roles. You map group display names to roles (PUT /v1/organizations/{id}/scim/role-mappings). Group membership grants the mapped roles; leaving the group removes them. Roles assigned by hand are left alone. SCIM never grants or removes the owner role.
  • Deactivation works like removing a member: the person's SCIM roles go, and their sessions, API credentials and audit export rights in the organization are revoked in the same transaction.

Errors use the SCIM error schema (urn:ietf:params:scim:api:messages:2.0:Error) with scimType values such as uniqueness, invalidFilter and invalidSyntax. GET /scim/v2/ServiceProviderConfig describes what is supported.

Owner recovery

When every owner of an organization is locked out, a person at the organization signs in (passkey or IdP) and opens Recover organization ownership (POST /v1/owner-recovery-requests) with a verified domain of the organization and a contact attestation: name, title, email, optional phone and a statement.

  1. Fresh DNS evidence. The request returns a new TXT record (mutualtrust-owner-recovery=<token> at _mutualtrust-challenge.<domain>). Publish it and check it (POST /v1/owner-recovery-requests/{id}/check); both DNS-over-HTTPS resolvers must agree, within 72 hours of the request.
  2. Two staff members. One Mutual Trust staff member reviews the evidence and calls the contact back; a second, independent one approves.
  3. Claim. The named identity, and nobody else, claims the owner role with a fresh sign-in within 48 hours (POST /v1/owner-recovery-requests/{id}/claim).

Every step is recorded in the organization's audit log and in the staff access log. Current owners see open requests under Organization, Owner recovery and can cancel one: an owner who can still sign in is proof the organization is not locked out.

Step-up for high-risk changes

High-risk changes (production grant withdrawal, scope widening, member removal, privileged API credentials, production space and service creation and the others listed in Organization and roles) need proof that the person at the keyboard signed in recently.

  • Passkey accounts confirm in place: the portal asks for the passkey (POST /v1/me/step-up/options, then POST /v1/me/step-up) and keeps the session.
  • Organization (OIDC) accounts sign in again through the identity provider and come back to the same page.
  • The window is 15 minutes at most. A space, service, tenant or platform approval policy can shorten it with step_up_max_age_minutes; nothing can lengthen it.
  • Sandbox guest sessions cannot step up and cannot make production changes.
  • API credentials cannot step up. They make high-risk production changes only when created with high_risk_allowed, which needs a stepped-up session at creation and expires within 30 days.
Step-up errors
CodeStatusMeaning
step_up_required403The session signed in too long ago, or cannot step up. The problem carries high_risk_action, action_label, max_age_seconds and authenticated_at. Re-authenticate and retry the same request.
high_risk_credential_required403An API credential without high_risk_allowed tried a high-risk change. Make the change in the portal, or create a credential with the flag.

Staff console

admin.mutualtrust.io is a separate Worker (mt-admin). It never reads customer cookies, and customer sessions or API credentials are not accepted there.

Staff passkeys

Sectigo staff sign in to the console with a passkey. A one-time bootstrap registers the first staff passkey for a new deployment; after that, staff add and remove their own passkeys from the console. Staff passkeys and sessions are separate from customer accounts.

Cloudflare Access with MFA

The console also sits behind a self-hosted Cloudflare Access application that admits only named staff with MFA.

Self-hosted Access application
NameMutual Trust staff console
Domainadmin.mutualtrust.io
Session duration8 hours
Policy“Named staff with MFA”: include the named staff emails; require authentication method mfa (prefer hardware keys / WebAuthn).
App launcherHidden
mt-admin settings
SettingValue
ACCESS_TEAM_DOMAINYour Zero Trust team domain, for example <team>.cloudflareaccess.com.
ACCESS_AUDThe application audience (AUD) tag.

If either is unset, every staff request is refused with 401: the console fails closed.

How Access assertions are verified

  • Every staff request must carry Cf-Access-Jwt-Assertion.
  • Header alg must be RS256 with a kid; keys come from https://<ACCESS_TEAM_DOMAIN>/cdn-cgi/access/certs (cached 5 minutes, refetched once on an unknown kid for key rotation).
  • aud includes ACCESS_AUD; iss equals https://<ACCESS_TEAM_DOMAIN>; exp and nbf hold with 60 seconds of skew.
  • An email claim is required, so Access service tokens can never act as staff.
  • The email must map to an active staff member; their roles (support, trust operations, PKI key custodian, platform operations, billing operations and approval, finance, incident commander, partner management) gate each action.
  • MFA is enforced by the Access policy; the Worker trusts only assertions minted for this application’s AUD.

Operator setup

Sectigo operators (and self-hosters) can create both applications idempotently with the repository script, which prints the secret commands to run and never stores secrets itself:

Create the Access applications
CLOUDFLARE_API_TOKEN=… CLOUDFLARE_ACCOUNT_ID=… STAFF_EMAILS="a@example.com,b@example.com" \ node scripts/cloudflare-access-setup.mjs

The API token needs Access: Apps and Policies: Edit and Access: Organizations: Read. Zero Trust must already be set up for the account.