Skip to main content

Trust anchors & allowed_sources

Concept

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. allowed_sources lets a single route authorize multiple distinct sources — each with its own issuer, trust domain, subject, and required key binding — instead of one global trust domain.

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.

Implementation

Multi-anchor route authorization

Each route's allowed_sources is a list of source rules. A source rule names:

  • issuer
  • trust_domain
  • subject_exact or subject_prefix
  • required_key_binding (signer class minimum)
  • optional provenance_policy and context_policy

Source authorization runs before replay consume, with stable denials:

DenyMeaning
source_issuer_mismatchIssuer not in any allowed source
source_trust_domain_mismatchTrust domain not allowed
source_subject_mismatchSubject (exact/prefix) not allowed
insufficient_key_bindingKey class below the route's minimum
missing_provenance / provenance_mismatchSource provenance policy unmet
missing_context / context_mismatchSource 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)

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.

Example

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 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 software

The 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 Add a client, no redeploy.

What to notice

  • One adapter, one route, many trust sources — each scoped precisely.
  • "Auto-enroll" 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-source with --confirm-remove; add --revoke when the reviewed output should append local revocation metadata.

→ Next: Brownfield adapter.