Trust anchors
A real API has many callers from different identity sources: an external SaaS partner
with a software/JWKS key, an in-cluster workload with a SPIFFE SVID, an EC2 box, an n8n runner.
A single global trust domain can’t express that. allowed_sources lets one route authorize
multiple distinct sources — each with its own issuer, trust domain, subject, and required key
binding.
And because trust is data, not code, you can add a new client by publishing trust
material — no application change, no adapter image rebuild. That is the “add a caller without
redeploy” property, delivered by trustplane bundle merge-source.
How it works
Section titled “How it works”Multi-anchor route authorization
Section titled “Multi-anchor route authorization”Each route’s allowed_sources is a list of source rules. A source rule names:
issuertrust_domainsubject_exactorsubject_prefixrequired_key_binding(signer class minimum)- optional
provenance_policyandcontext_policy
Source authorization runs before replay consume, with stable denials:
| Deny | Meaning |
|---|---|
source_issuer_mismatch |
Issuer not in any allowed source |
source_trust_domain_mismatch |
Trust domain not allowed |
source_subject_mismatch |
Subject (exact/prefix) not allowed |
insufficient_key_binding |
Key class below the route’s minimum |
missing_provenance / provenance_mismatch |
Source provenance policy unmet |
missing_context / context_mismatch |
Source context policy unmet |
Bundle-level provenance_policy applies to every accepted source; source-local policies
compose as additional constraints (they don’t replace it). context.max_txn_value is treated
as a numeric ceiling: at/below allows, above or malformed denies with context_mismatch.
Non-destructive merge (trustplane bundle merge-source)
Section titled “Non-destructive merge (trustplane bundle merge-source)”Appends a client public key to trust material and a matching source rule to a route, while
preserving existing issuers, keys, route groups, routes, and source rules by default.
Duplicate key IDs or matching selectors are rejected unless you pass --replace-existing.
flowchart LR TM0["trust material<br/>(existing keys)"] --> MERGE["trustplane bundle merge-source"] PB0["policy bundle<br/>(existing routes/sources)"] --> MERGE NEW["new client: kid + public key<br/>+ route + subject + key-binding"] --> MERGE MERGE --> TM1["trust material (merged, existing preserved)"] MERGE --> PB1["policy bundle (new source appended)"]
Authorize four sources on one route
Section titled “Authorize four sources on one route”A real four-anchor route from the hosted-demo fixture — one route, four different source types:
"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" }, { "issuer": "https://issuer.example.com/spire", "trust_domain": "example.local", "subject_prefix": "spiffe://example.local/ns/default/sa/", "required_key_binding": "attested_workload", "provenance_policy": { "profile": "trustplane-spiffe-spire-k8s-v1", "required_spiffe_trust_domain": "example.local", "required_posture": "spiffe_svid_verified" } }, { "issuer": "https://issuer.acme.demo/ec2-jwks", "trust_domain": "acme.demo.ec2", "subject_prefix": "aws:ec2:us-east-1:", "required_key_binding": "software" }, { "issuer": "https://issuer.acme.demo/eks-n8n-jwks", "trust_domain": "acme.demo.eks-n8n", "subject_exact": "system:serviceaccount:n8n:n8n-runner", "required_key_binding": "software" }]Only the SPIFFE/SPIRE source requires attested_workload; the software/JWKS sources stay at
software and do not claim workload attestation.
Add a client without redeploying
Section titled “Add a client without redeploying”Add a new client without touching the app or adapter image:
trustplane bundle merge-source \ --trust-material trust-material.json \ --policy-bundle trustplane.bundle.json \ --out-trust-material trust-material.merged.json \ --out-policy-bundle trustplane.bundle.merged.json \ --issuer https://issuer.acme.demo/external-jwks \ --trust-domain acme.demo.external \ --kid hosted-demo-client-2 \ --public-key "$CLIENT_PUBLIC_KEY_B64URL" \ --route-id acme.demo.orders.read \ --subject-exact external:jwks:hosted-demo-client-2 \ --required-key-binding softwareThe command writes full merged bundle files; you then publish/mount the reviewed artifacts with your own process (locally, or via the bundle refresh workflow in your deployment process). A full walkthrough is in Brownfield APIs.
What to notice
Section titled “What to notice”- One adapter, one route, many trust sources — each scoped precisely.
- “Configured acceptance” here means policy-based acceptance: a caller matching a configured source rule is accepted. It does not create a persistent principal record — acceptance is decided per request.
- Merge is append-only by default; destructive replacement is an explicit opt-in
(
--replace-existing). - Local source removal exists through
trustplane bundle remove-sourcewith--confirm-remove; add--revokewhen the reviewed output should append local revocation metadata.
Next steps
Section titled “Next steps”- Brownfield adapter — put a verifier in front of an unmodified API.