Bundle policy
The verifier needs two kinds of local input: trust material (which public keys to trust) and
route policy (what each route requires). The policy bundle (trustplane-bundle-v1) holds
the per-route rules: which sources are allowed, the minimum key binding, and — crucially —
how fresh the bundle must be for that route. Freshness is what lets high-risk routes fail
closed when policy might be out of date, while low-risk routes keep working offline.
How it works
Section titled “How it works”- Built by
trustplane bundle build(inpkg/bundle); local-only, no hosted services. - Uses the
trustplane-bundle-v1envelope and validates required fields, key-binding classes, freshness classes, and route source rules. - Freshness classes:
| Class | Behavior | Stale deny |
|---|---|---|
realtime |
Requires a current bundle view | stale_bundle_fail_closed |
bounded |
Allows age up to max_staleness_seconds |
fail-closed after the window |
offline-ok |
Explicitly allows stale/offline use (alias offline_ok normalized) |
never (by design) |
- Unknown class →
bundle_freshness_unknown;boundedwithout a positive max → fail-closed. - Route resolution in the adapter:
--route-idmode: a single configured route.--route-policy-mode requestmode: match the actual request method/path against routemethod+path_template, derive the matchedroute_id. Exact paths win over conservative single-segment{name}templates.- Missing policy →
bundle_policy_missing; unknown route →bundle_route_missing(both before replay consume).
- File-backed realtime freshness: when the adapter reads
--policy-bundle, arealtimeroute is considered current only within--policy-bundle-freshness-window(default5s) after the file mtime, then fails closed.bounded/offline-okkeep their normal semantics.
flowchart TD
REQ["GET /orders"] --> MATCH{"route match?<br/>(route-id or request mode)"}
MATCH -- no --> M1["bundle_route_missing"]
MATCH -- yes --> FRESH{"fresh enough for<br/>freshness_class?"}
FRESH -- "realtime, window elapsed" --> F1["stale_bundle_fail_closed"]
FRESH -- "bounded, age > max" --> F1
FRESH -- "offline-ok" --> OK["continue to source + binding checks"]
FRESH -- "within window" --> OK
Build a policy bundle
Section titled “Build a policy bundle”Build a policy bundle from an authoring config:
trustplane bundle build --config bundle.config.json --out trustplane.bundle.jsonThe multi-anchor example authors the hosted-demo GET /orders route:
trustplane bundle build \ --config cmd/trustplane-cli/testdata/bundle-build/acme-demo.config.json \ --out trustplane.bundle.jsonA route entry, showing freshness + a source rule:
{ "route_id": "acme.demo.orders.read", "method": "GET", "path_template": "/orders", "freshness_class": "bounded", "max_staleness_seconds": 300, "allowed_sources": [ { "issuer": "https://issuer.acme.demo/external-jwks", "trust_domain": "acme.demo.external", "subject_exact": "external:jwks:hosted-demo-caller", "required_key_binding": "software" } ]}The provider/gateway demo enforces this end to end (route selection, freshness, deny reasons):
make demo-provider-gatewayWhat to notice
Section titled “What to notice”- The bundle is the policy. There is no central policy server in the request path — the adapter decides from the mounted file.
- Freshness is a per-route risk knob: make
GET /ordersboundedfor resilience, but make a money-moving routerealtimeso it fails closed the moment policy goes stale. trustplane bundle buildemits a deterministic skeleton. Sign reviewed trust and policy outputs withtrustplane bundle signbefore production-style adapter loading.
Next steps
Section titled “Next steps”- Signed bundle lifecycle — sign, verify, refresh, and remove trust and policy material safely.