Five-minute sandbox quickstart
Play both sides of a partner relationship in the sandbox: Northstar owns an Orders API, Harbor calls it. You finish with one verified allow, one verified deny, and a withdrawal that the gateway acknowledges.
Prerequisites
1. Create a space
Open app.mutualtrust.io, choose Start sandbox, then Try the Orders API. No card is needed. The portal calls POST /v1/sandbox/try-orders-api, which creates two clearly labelled sample organizations (Northstar (sample) as the API owner and Harbor (sample) as the partner), plus a sandbox trust space with its own private root and issuing CA, and an Orders API service with three route templates:
| Template | Methods | Path |
|---|---|---|
| Read orders | GET | /orders/* |
| Submit orders | POST | /orders |
| Admin export (restricted) | GET | /admin/* |
The portal shows the sandbox trust fingerprint (SHA-256 of the space’s root certificate). Sandbox roots carry SANDBOX TEST in their subject so they can never be mistaken for production.
2. Start the sample API and gateway
Install the mt CLI. The install script verifies the release signature and checksum before it installs anything (packages and other options):
curl -fsSL https://mutualtrust.io/install.sh | shirm https://mutualtrust.io/install.ps1 | iexStart the sample Orders API with the enforcer in front of it. mt gateway runs the built-in enforcer in proxy mode on https://localhost:8443:
mt gateway --api https://api.mutualtrust.ioThe gateway generates its own P-256 key, registers it and prints a pairing code:
Mutual Trust quickstart control plane https://api.mutualtrust.io
sample Orders API on http://127.0.0.1:9001 (rejects calls that bypass the gateway)
enforcer embedded (mt enforcer proxy)
Pairing code: K7QM-4TXZ
Approve it in the portal (Services > your service > Pair gateway), or open:
https://app.mutualtrust.io/#/pair?code=K7QM-4TXZ
gateway key thumbprint 3nUq0b2s9kTq7m1x…In the portal, as Northstar, open Orders API → Pair gateway, enter the code and a name. The code is valid for 15 minutes. On approval the gateway collects its scoped credential once, pins the space’s policy-verification key, fetches its first signed policy manifest and acknowledges the revision it applied. The service readiness checklist now shows Gateway paired and Gateway acknowledged the current policy revision.
3. Authorize the caller
- As Northstar, open Orders API → Invite partner. Choose Harbor (sample) and the routes
GET /orders/*andPOST /orders. Leave Admin export out: that is the route you will be denied on. - Switch to Harbor (sample) and accept the scoped invitation. Give the workload a name; in the sandbox it binds a sandbox identity. Accepting creates an active grant.
- Still as Harbor, choose Approve enrollment. The portal issues a single-use sandbox enrollment token (
mtenr_…, valid 30 minutes) and shows a one-time command.
mt enroll --token mtenr_… --api https://api.mutualtrust.ioEnrolling workload via https://api.mutualtrust.io
1. attestation accepted: challenge for enr_01jcqb5q2h4j6k8m0n2p4q6r8s
2. generated P-256 key locally (private key never leaves this machine)
3. certificate issued serial 5c1e9a7b3d2f48a6b0c4e8d1f3a5b7c9
principal urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g
expires 2026-11-15T15:31:30Z
renew after 2026-11-15T07:27:12Z
files ~/.config/mutualtrust/workload/{cert,chain,key,root}.pemThe CLI generated the key locally, sent a CSR plus a signature over the server’s challenge, and wrote the certificate to the workload directory of its home (~/.config/mutualtrust on Linux, ~/Library/Application Support/MutualTrust on macOS, %ProgramData%\MutualTrust on Windows). The portal now shows the workload, its issuer, routes, owner and expiry. Details: Enrollment.
4. Send two requests
mt call https://localhost:8443/orders # 200 ALLOWED
mt call https://localhost:8443/admin/export # 403 DENIED (route_not_permitted)200 ALLOWED GET https://localhost:8443/orders
request req_3f9a1c7e5b2d8a4c6e0f · policy revision 1.2
{"orders":[{"id":"ord_1001","status":"staged","lines":3}],"caller":"urn:mutualtrust:workload:sandbox:org_01jcqa7m3a2b7c9d4e6f8g1h3j:wl_01jcqb3p1y2z4a6b8c0d2e4f6g"}
403 DENIED GET https://localhost:8443/admin/export
request req_7b2e9d4a1f6c3e8b5a0d · policy revision 1.2
{"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}The gateway answers with x-mt-request-id and x-mt-policy-revision (epoch.revision) headers. Both requests produce signed receipts that appear in the portal with the gateway, grant, time and policy revision. TLS verification and authorization are separate verdicts on each receipt: the denied call above has tls: verified and authz: deny, which is what makes it a valid denied control for the service’s readiness checklist.
The origin refuses bypass
5. Change access
As Northstar, open the Harbor grant and choose Withdraw with a reason (for example contract_ended). The portal shows the effective control-plane time, the intended policy revision and how many gateways must acknowledge it. Repeat the first request:
mt call https://localhost:8443/orders
403 DENIED GET https://localhost:8443/orders
request req_9c4e1a7f3b5d2e8a6c0b · policy revision 1.3
{"type":"https://mutualtrust.io/problems/certificate_revoked","title":"The client certificate was revoked.","status":403,"code":"certificate_revoked",…}Withdrawal revokes every live leaf for the grant, so the gateway reports certificate_revoked (the deny-set check runs before the grant check). The operation view lists each gateway as pending, delivered, applied or offline (pending); a disconnected gateway is never shown as applied. An online gateway refreshes policy every 15 seconds, so the acknowledgment typically arrives within seconds. Details: Withdraw access.
Keep it running: renewal
mt renew --watch # in the foreground
mt service install renew # or as a service: systemd, launchd or a Windows serviceLeaves last 24 hours. The CLI renews at about two-thirds of the validity period (the server adds ±5% jitter), proves key continuity with the current key, and confirms the reload with the new key before the old credential is retired locally. Add --on-renew "systemctl reload nginx" (after -- for mt service install) when a server reads the files. See Renewal.
Troubleshooting
| Denial code | Meaning and fix |
|---|---|
no_client_certificate | No usable client certificate was presented, or its chain did not verify against this service’s trust bundle (the response then shows "tls":"failed"). Use mt call, or pass ~/.mt/workload/chain.pem and key.pem to your client. |
unknown_issuer | The chain verified but its issuing CA is not in this service’s signed trust bundle. Re-enroll against the right service. |
route_not_permitted | Your grant does not include this route and method. Ask the API owner to change the scope. |
grant_revoked / certificate_revoked | Access was withdrawn. Contact the API owner shown in the portal. |
manifest_expired / uninitialized | The gateway has no fresh signed policy and fails closed. Check that the gateway can reach api.mutualtrust.io. |
Every error includes a request ID; quote it if you contact support. The full list is in Troubleshooting.
Next steps
- Concepts: the model behind what you just did.
- Gateways: put enforcement in front of your real API.
- Invite a real partner and enroll with OIDC workload identity.