Bind Workload Identity
Restrict a payments tool to one workload and accepted runtime assurance, then read the identity the kernel signed into the receipt.
On this page
A payments agent can move money only from the workload you name, running under evidence that a verifier you trust has accepted. The kernel checks both on every call routed through it, before transfer_funds runs.
Three bindings meet at the kernel. The capability authorizes the agent's public key and the operation. The runtime trust policy decides which verifier evidence counts and what tier it earns. The workload rule restricts which SPIFFE identity may present the request. You configure all three in the same execution path.
Start1. Require the payments workload
Save this HushSpec file as payments-policy.yaml. Its tool-access rule permits the two payments operations and requires a payments worker or reconciler with at least attested runtime assurance.
Before you begin
- The CLI,
jq, and a trust-control deployment configured to issue capabilities. - An authenticated runtime integration that produces fresh attestation evidence for
spiffe://prod.chio/payments/worker. Chio consumes this identity; your mesh or verifier establishes its authenticity. - An authorization bridge that supplies the normalized workload identity and accepted assurance to policy evaluation, and a governed client that carries the evidence on execution requests. Envoy's adapter exposes an upstream principal, and the bridge maps that principal into the typed identity policy consumes. The identity-source comparison covers the choices.
- A payments tool server exposing
transfer_fundsandreconcile_ledger, or your own equivalent tool names. - The agent's full public key, the issuer's authenticated URL, and permission to change the deployment's policy. The policy must be active at both issuance and the execution boundary.
hushspec: "0.1.0"
name: payments-workload-identity
rules:
tool_access:
enabled: true
default: block
allow:
- transfer_funds
- reconcile_ledger
require_workload_identity:
scheme: spiffe
trust_domain: prod.chio
path_prefixes:
- /payments/worker
- /payments/reconciler
credential_kinds:
- x509_svid
- jwt_svid
require_runtime_assurance_tier: attestedRun the analyzer. It reports both predicates as not_analyzed, because their result depends on runtime evidence that only arrives with a request.
$ chio policy analyze ./payments-policy.yamlpolicy_sha256 38c484dc787234cd2839a9e61682ad3028d0875a98f8fec8bbc396fbe2d89e53
ID SEVERITY KIND BLOCK RULE
- notice not_analyzed tool_access require_runtime_assurance_tier
runtime attestation predicate
- notice not_analyzed tool_access require_workload_identity
runtime identity predicate
summary: 0 error(s), 0 warning(s), 2 notice(s)A path prefix matches whole segments. /payments/worker and its child /payments/worker/shard-3 match; /payments/worker-shadow is a different segment and does not.
4 of 8 paths admitted. /payments/worker-shadow shares the letters of /payments/worker but not a whole segment, so it is excluded.
In Rust · matchers.rs:39627 lines
fn workload_identity_matches(
expected: &crate::models::WorkloadIdentityMatch,
actual: &chio_core::capability::workload_identity::WorkloadIdentity,
) -> bool {
// … scheme
&& expected
.trust_domain
.as_deref()
.is_none_or(|trust_domain| trust_domain == actual.trust_domain)
&& (expected.path_prefixes.is_empty()
|| expected
.path_prefixes
.iter()
.any(|prefix| workload_identity_path_matches_prefix(prefix, &actual.path)))
// … credential kinds
}
fn workload_identity_path_matches_prefix(expected_prefix: &str, actual_path: &str) -> bool {
if !actual_path.starts_with('/') || actual_path.contains("//") {
return false;
}
// … a trailing slash is trimmed; the prefix "/" matches every path
actual_path == prefix
|| actual_path
.strip_prefix(prefix)
.is_some_and(|suffix| suffix.starts_with('/'))
}
Scheme, trust domain, path, and credential kind must each satisfy their restriction. The credential kind describes the presented record, so labeling a JSON document x509_svid authenticates no certificate. An identity match has no representation in the compiler's default scope: require_workload_identity and prefer_workload_identity both produce an empty default scope, and the capability in step 3 supplies the authority.
2. Configure the verifier you trust
Append this extension to the top level of the same file, with your integration's verifier URL. Matching, fresh evidence from the enterprise verifier earns an effective tier of attested, and the tier caps what issuance may grant.
extensions:
runtime_assurance:
tiers:
payments:
minimum_attestation_tier: attested
max_scope:
operations: [invoke]
max_invocations: 100
max_delegation_depth: 0
ttl_seconds: 300
constraints_required: true
trusted_verifiers:
payments_verifier:
schema: chio.runtime-attestation.enterprise-verifier.json.v1
verifier: https://verifier.halden.example
verifier_family: enterprise_verifier
effective_tier: attested
max_evidence_age_seconds: 120
allowed_attestation_types: [x509_svid, jwt_svid]You should see two separate checks in the configuration. The verifier's evidence supplies claims.enterpriseVerifier.attestationType, which the trust rule compares with allowed_attestation_types; the workload rule compares workload_identity.credentialKind. Configure the bridge to derive both from the authenticated credential.
The local trust rule decides the effective tier, so a raw tier field in the evidence earns nothing by itself. Use the schema your integration emits; the generic chio.runtime-attestation.v1 schema is outside the supported appraisal formats.
3. Issue the constrained capability
Save the verifier-produced evidence as runtime-attestation.json, with its own timestamps, digest, verifier, claims, and identity. Set CHIO_SUBJECT_PUBLIC_KEY to the agent's 64-character hexadecimal public key, CHIO_TRUST_URL to your trust-control base URL, and CHIO_TOKEN to its authorized bearer token.
jq -n \
--arg subject "$CHIO_SUBJECT_PUBLIC_KEY" \
--slurpfile evidence runtime-attestation.json \
'{
subjectPublicKey: $subject,
ttlSeconds: 300,
scope: {
grants: [{
server_id: "payments",
tool_name: "transfer_funds",
operations: ["invoke"],
max_invocations: 100,
constraints: [{
type: "minimum_runtime_assurance",
value: "attested"
}]
}]
},
runtimeAttestation: $evidence[0]
}' > issuance-request.json
curl --fail-with-body "$CHIO_TRUST_URL/v1/capabilities/issue" \
-H "Authorization: Bearer $CHIO_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @issuance-request.json \
--output issued-capability.json
jq -e '.capability' issued-capability.json > capability.jsonYou should see a response that contains capability. Check that its subject, tool grant, invocation limit, lifetime, and minimum-assurance constraint match the request. The JSON envelope uses camelCase, the evidence record uses snake_case, and its nested workload identity uses camelCase; keep the verifier's field names intact.
This grant requires accepted assurance on execution through its constraint. Issuance stamps assurance automatically only on economically sensitive grants under a configured assurance policy, so the workload path stays a runtime policy requirement.
4. Run one call from the workload and one from outside it
Use the issued capability in your governed client and carry fresh runtime evidence on each request. Run a permitted payments operation from /payments/worker, then repeat it from a workload outside the allowed paths, with the capability still valid so the second call reaches the identity rule.
You should see the first call reach the tool and the second refused before dispatch. If the second call is refused for scope instead, the capability expired or was never presented; the refusal reference names each cause.
5. Read the identity in the receipt
Export the permitted call's receipt and verify it against the kernel key you trust. Find it with the tool, subject, tenant, or time filters of a receipt query, then read the signed runtime-assurance block.
jq '.metadata.governed_transaction.runtimeAssurance |
{schema, verifierFamily, tier, verifier, evidenceSha256, workloadIdentity}' \
receipt.json{
"schema": "chio.runtime-attestation.enterprise-verifier.json.v1",
"verifierFamily": "enterprise_verifier",
"tier": "attested",
"verifier": "https://verifier.halden.example",
"evidenceSha256": "5e0c…a91d",
"workloadIdentity": {
"scheme": "spiffe",
"credentialKind": "x509_svid",
"uri": "spiffe://prod.chio/payments/worker",
"trustDomain": "prod.chio",
"path": "/payments/worker"
}
}You should see your verifier, the effective tier, the evidence digest, and the payments workload URI; the digest above is illustrative. The kernel writes this block only for evidence it accepted locally, so a receipt without it carries no accepted assurance. The signature proves what the kernel recorded, and your upstream integration supplies the basis for trusting the workload claim.
Read more in Workload identity, which covers normalization and the trust boundaries behind each binding.
Refusals, appraisals, and sources
Try scope, stale, schema, appraisal, or conflict.
No capability covers the request
Use the issued grant. The policy's empty default scope cannot authorize a workload-dependent rule, so a preflight without an issued capability is refused at capability selection. The capture below runs the step 1 rule file, before the verifier extension, with no issued capability or runtime identity; a policy with the extension has a different policy hash.
$ chio --session-db ./admission-2.db --receipt-db ./receipts.db \
check --policy ./payments-policy.yaml \
--tool transfer_funds --params '{"amount": 100}'verdict: DENY tool: transfer_funds server: * reason: requested tool transfer_funds on server * is not in capability scope receipt_id: 6b9544b7f38daec5d85c20c52fd18bf22de65ff5e49c036afce10e997c108678 policy: 9484ede27e47ea7cf5ed0bd0f22757156ea0e4a3bd09e392f6c8dadecfbd6130 source: a06c6ed5b7ea03e60f224fb101118c560485a07c5982f7cf50c5766cb977aea1 mode: preflight fixture: false
Exit code 2 accompanies a capability-selection refusal. An identity-specific refusal needs a valid capability and an integration that passes the workload context to policy evaluation. Set DENY to the refused receipt id to read the same refusal from the receipt database.
$ chio --receipt-db ./receipts.db receipt explain "$DENY" --admin-allreceipt: 6b9544b7f38daec5d85c20c52fd18bf22de65ff5e49c036afce10e997c108678 schema: chio.receipt.v1 identity: 6b9544b7f38daec5d85c20c52fd18bf22de65ff5e49c036afce10e997c108678 decision: deny reason: requested tool transfer_funds on server * is not in capability scope guard: kernel policy_hash: 9484ede27e47ea7cf5ed0bd0f22757156ea0e4a3bd09e392f6c8dadecfbd6130 scope_diff: requested scope vs granted scope is not embedded in this receipt parents: 0 repair_hint: inspect the guard and policy_hash, then mint or narrow a matching capability
For comparison, save the rule without identity requirements as payments-open.yaml. Its default scope is representable, so the same preflight is admitted. These are preflight evaluations and transfer no funds.
hushspec: "0.1.0"
name: payments-open
rules:
tool_access:
enabled: true
default: block
allow:
- transfer_funds
- reconcile_ledger$ chio --session-db ./admission-1.db --receipt-db ./receipts.db \
check --policy ./payments-open.yaml \
--tool transfer_funds --params '{"amount": 100}'verdict: ALLOW tool: transfer_funds server: * receipt_id: 92a63c5f60df32983311daf7ebd088313a8c5f31a2e3f067c5ab4efbd2817a30 policy: 8207ea76098d0750fa524b2244fddd1a7a9b4e329f3c0e0b6849d8e53ef9be41 source: 104d3a2e123436652356f069bd335fa15665efabe6eaaca07f704a39608b1c5b mode: preflight fixture: false
Unsupported attestation schema
Use the supported evidence schema emitted by the configured verifier bridge.
No accepted runtime assurance
Check the local trust rule, schema, verifier, required assertions, and freshness. Raising the caller's tier field leaves local acceptance unchanged.
Identity conflict or malformed URI
Correct the evidence source. An explicit identity and a SPIFFE runtime identity must agree on scheme, trust domain, and path.
Required workload mapping failed
Check the normalized identity delivered to policy, the exact trust domain, whole path segments, and credential kind.
Evidence too old or outside its validity window
Obtain fresh evidence. Both the evidence's validity window and the trust rule's maximum age apply.
Requested scope exceeds its tier
Reduce the operation set, invocation cap, or lifetime to the tier's issuance ceiling.
Inspect or exchange an appraisal
The chio trust appraisal command group exports the normalized appraisal, exports a signed result, and imports a partner's result under local policy. Trust control exposes the same operations under /v1/reports/runtime-attestation-appraisal; the CLI reference gives their arguments. An imported signed result is checked for signature, freshness, and local policy, and stays acceptable for its whole validity window, including when presented more than once.
Implementation sources
crates/guards/chio-policy/src/evaluate/matchers.rs: required and preferred identity matching.crates/economy/chio-appraisal/src/appraisal.rs: supported schemas and local trust acceptance.crates/platform/chio-control-plane/src/issuance/scope.rs: scope ceilings and assurance constraints.crates/kernel/chio-kernel/src/receipt_support/receipt_metadata.rs: accepted assurance in receipts.