Read a denial

A gateway denial is a 403 problem document:

Gateway 403 body
{ "type": "https://mutualtrust.io/problems/route_not_permitted", "title": "This workload is not permitted to call this route or method.", "status": 403, "code": "route_not_permitted", "request_id": "req_7b2e9d4a1f6c3e8b5a0d", "tls": "verified", "policy_revision": 2 }
  • tls is the separate TLS verdict: verified, failed or absent.
  • policy_revision is the revision the gateway enforced (null if it has none). The x-mt-policy-revision header carries epoch.revision.
  • The same decision is recorded on a signed receipt visible to the API owner, so both sides can look at the same evidence.

Gateway deny reasons

Checks run in this order; the first failure wins: policy present → policy fresh → client certificate → trusted issuer → exactly one workload URI for this environment → clientAuth EKU → not expired → serial not revoked → path unambiguous → grant and route.

Gateway deny reasons
CodeMessage shownCauseFix
allowedNoneThe request was authorized (shown on receipts).None needed.
uninitializedThis gateway has not received a signed policy yet; all protected routes are denied.The gateway has never applied a manifest (not paired, or cannot reach the API since start).Check pairing and outbound HTTPS to api.mutualtrust.io. Look for manifest_fetch_failed or manifest_rejected in the gateway log.
manifest_expiredCached policy expired and the control plane is unreachable; failing closed.No successful refresh for more than 5 minutes 30 seconds.Restore connectivity; the next successful refresh re-enables traffic. Check the gateway clock.
no_client_certificateNo client certificate presented. Configure your client with the enrolled certificate and key.No certificate presented, or (with "tls":"failed" in the body) the chain did not verify against the trust bundle (wrong space, expired chain, or missing issuing CA in the presented chain).Send chain.pem (leaf + issuing CA) and key.pem. If TLS failed, re-enroll against this service’s space.
tls_verification_failedThe client certificate chain did not verify against this service’s trust bundle.Reserved for chain failures reported separately from authorization; gateways report chain failures as no_client_certificate with "tls":"failed".As above.
unknown_issuerThe certificate was issued by a CA this service does not trust.The chain verified, but the issuing CA is not in this service’s signed trust list.Enroll against the service you are calling.
malformed_sanThe certificate does not carry exactly one valid workload identity.Zero or several workload URIs, a malformed URI, or a URI for another environment.Use a certificate issued by Mutual Trust for this environment.
missing_client_auth_ekuThe certificate is not valid for client authentication.The leaf lacks the clientAuth extended key usage.Use the Mutual Trust workload certificate, not a server certificate.
certificate_expiredThe client certificate has expired; renew it.not_after has passed.Run mt renew; if it reports certificate_expired, re-enroll.
certificate_revokedThe client certificate was revoked.The serial is in the deny set: revoked individually or by grant withdrawal.Re-enroll if your grant is still active; otherwise contact the API owner.
grant_revokedAccess for this workload was withdrawn by the API owner.The grant was withdrawn.Contact the API owner (the service contact is shown in your grant view).
grant_suspendedAccess for this workload is temporarily suspended by the API owner.The grant is suspended.Contact the API owner.
grant_expiredThe access grant for this workload has expired.The grant’s expires_at passed.Ask the API owner for a new invitation.
no_matching_grantNo active grant authorizes this workload for this service.Valid identity, but no rule for this principal and issuer on this service.Check you are calling the right service with the right workload.
route_not_permittedThis workload is not permitted to call this route or method.A live grant exists but its scope does not cover this method and path.Ask the API owner to change the scope; check the method (HEAD needs GET).
ambiguous_routeThe request path is ambiguous and was refused.Encoded slashes or dots, backslashes, dot or empty segments.Send a normalized path.

Other gateway responses

Other gateway responses
ResponseMeaning
502 {"code":"origin_unavailable"}The request was allowed but the gateway could not reach your origin.
401 {"error":"origin_requires_gateway"}From the quickstart sample origin: a request reached the origin without the gateway’s verified principal header. Your real origin should behave the same way.
Connection closedThe gateway drains connections when policy goes stale, when a principal loses all rules, and after 5 minutes of connection age. Clients should reconnect.

Manifest rejections

If a gateway refuses a manifest it keeps its current policy, logs manifest_rejected: <code>, and (except for scope errors) sends a signed rejected acknowledgment with that code.

Manifest rejection codes
CodeMeaning
jws_alg_rejectedThe JWS algorithm is not ES256.
jws_kid_mismatchSigned with a key other than the one pinned at pairing.
jws_typ_mismatchWrong JWS typ (must be mt.manifest.v1+jws).
jws_signature_invalidThe signature does not verify.
wrong_scopeTenant, environment, service, gateway, epoch or revision do not match the pinned target or the embedded policy.
expiredReceived after exp + 30 s.
not_yet_validiat is more than 30 s in the future; check the gateway clock.
hash_mismatchThe embedded policy does not hash to policy_hash.
downgradeLower epoch or revision than the gateway’s high-water mark.
replay_conflictSame epoch and revision as applied, but a different hash.

API problems

Common API problems
StatusCodeTypical fix
401unauthenticatedSign in again, or check the bearer credential (revoked, expired, wrong host for cookies).
403forbiddenMissing role, missing CSRF header on a cookie session, or a production action as a sandbox guest. The title says which.
404not_foundWrong ID, or a resource in an organization you are not a member of.
409invalid_transitionThe resource is in a state that does not allow this action.
412revision_mismatchRe-read the grant and retry with its current revision in If-Match.
415unsupported_media_typeSend Content-Type: application/json.
422validation_failedCheck errors[] for the failing JSON pointer.
428precondition_required / idempotency_key_requiredAdd If-Match or Idempotency-Key.
429rate_limitedWait for Retry-After seconds.
503variousA dependency is unavailable (Retry-After: 5). Existing certificates keep working.

The full list is the error catalogue.

Request IDs

  • API: every response has an X-Request-Id header, and problem bodies include request_id. On the hosted service it is req_ followed by the edge ray ID.
  • Gateway: every response has x-mt-request-id (req_ + 20 hex characters); the same ID is on the receipt and is forwarded to your origin.
  • CLI: mt prints the request ID with every error and every call.
  • Audit entries record the API request_id and an operation_id, so support can trace a request from the edge to the database transition.

Health and readiness

Control plane health
curl -sS https://api.mutualtrust.io/healthz {"ok":true,"deployment":"sandbox"}

GET /v1/services/{id} returns a readiness checklist. Each step shows exactly what is missing:

Service readiness steps
StepPasses when
trust_anchorThe space’s private root exists.
gateway_pairedAt least one gateway is paired with a pinned policy key.
policy_appliedA gateway acknowledged the current policy revision.
allow_receiptAn allowed request was observed.
deny_receiptA denied control request with verified TLS was observed.

Gateway status in the same response is current, pending (connected, not on the latest revision), offline (no check-in for 5 minutes), never_connected or disabled, with policy_age_seconds since its last acknowledgment.

Frequently seen problems

Frequently seen problems
SymptomLikely cause
Partner blocked right after renewalThe client kept the old key with the new certificate, or still presents the predecessor after it expired. Reload both chain.pem and key.pem together, then confirm reload.
Renewal returns 404The proof was signed with the wrong key or over the wrong CSR digest or timestamp.
Enrollment 422 “Proof of possession failed”The proof is DER-encoded or signs something other than proof_input. Use raw r‖s, base64url.
OIDC enrollment 401Token older than one hour, audience mismatch, or sub differs from the binding.
Every request manifest_expiredGateway lost outbound access or its clock is far off.
Invitation accept 403You are signed in as a member of a different organization, or as a sandbox guest on a production invitation.