How it works, end to end
This page walks one request through the whole system, then shows what happens when things go wrong. By the end you will understand the full life of a proof-bound request.
The happy path
Step by step, in plain words:
- The caller asks the broker for permission to make a specific call —
GET /ordersfor audienceacme.demo.orders. - The broker checks identity. In the simplest mode it just uses a local software key. On Kubernetes it can verify the caller's SPIFFE/SPIRE X.509-SVID first.
- The broker returns a passport + proof headers. The proof (transcript-v1) is bound to
method
GET, path/orders, the audience, a nonce, the body hash, etc. - The caller sends the real request to the adapter, carrying the passport and proof.
- The adapter resolves trust material and policy for
/ordersfrom local files. - The adapter runs the decision pipeline (order matters — see below).
- Only then does the adapter forward the request to the upstream, adding context headers
like
X-TrustPlane-Subject. The upstream returns business JSON.
The decision pipeline (and why order matters)
The verifier evaluates checks in a deliberate order. Cheap, policy-level denials happen
before replay state is consumed, so an attacker cannot burn a victim's jti by sending
junk:
All the policy checks (route, source, freshness, provenance/context) happen before the
replay Consume. That means a malformed or unauthorized request is rejected without spending
the caller's one-time jti. Replay state is only consumed on an otherwise-valid presentation.
What every failure looks like
Denials return a stable, machine-readable reason. You will see these in adapter JSON responses and audit events:
| What went wrong | Reason code |
|---|---|
| No route in the policy bundle for this method/path | bundle_route_missing |
| No policy bundle loaded at all | bundle_policy_missing |
Bundle too stale for a realtime/bounded route | stale_bundle_fail_closed |
| Unknown/invalid freshness class | bundle_freshness_unknown |
| Issuer not allowed for this route | source_issuer_mismatch |
| Trust domain not allowed | source_trust_domain_mismatch |
| Subject not allowed | source_subject_mismatch |
Key not strong enough (e.g. needs attested_workload) | insufficient_key_binding |
| Required provenance/context missing or wrong | missing_provenance, provenance_mismatch, missing_context, context_mismatch |
| Proof signature malformed / does not verify | invalid_request_proof |
| Request does not match the signed transcript (tampered path/nonce/route) | request_binding_mismatch |
| Same passport/proof presented twice | jti_replay |
(Adapter HTTP) missing Authorization | 401 |
| (Adapter HTTP) wrong audience | 403 |
Where verification can happen
The same passport + proof semantics work at any enforcement point:
That portability is the point: you do not have to rewrite your API to adopt TrustPlane Auth. The most common starting point is the brownfield adapter — covered in depth in Deployment overview.
Local-first, offline-capable
Notice what is not in the happy path: there is no mandatory call to a central server during verification. The verifier only needs public trust material and route policy, both of which are local files. This is why TrustPlane Auth can protect APIs at the edge, in air-gapped environments, and in CI — and why a single bundle file can authorize many issuers and clients.
Next: get hands-on → Quickstart.