BuildIdentity
Bind Workload Identity
Bind a capability to a SPIFFE workload: write the match policy, issue the token, and inspect the signed receipt.
You do not install SPIRE through Chio
Prerequisites
- A running chio kernel with a capability authority. See Installation.
- A workload whose SPIFFE ID you know, for example
spiffe://prod.chio/payments/worker. - One of the three delivery paths wired up (Envoy ext_authz, explicit attestation, or a verifier bridge). The policy and verification steps below are identical for all three.
- A capability authority that can mint identity-bound capabilities. A policy carrying
require_workload_identitycompiles to an empty default scope (crates/guards/chio-policy/src/compiler/scope.rs:50-52), so the capability has to come from issuance rather than from the policy file.
Step 1: Write a WorkloadIdentityMatch Rule
require_workload_identity is a field on the tool_access rule block, alongside require_runtime_assurance_tier (crates/guards/chio-policy/src/models/rules.rs:194-215). The match is additive: scheme, trust domain, path prefix, and credential kind each have to pass, and each is skipped when you leave it unset (crates/guards/chio-policy/src/evaluate/matchers.rs:396-413). The evaluator applies it and denies on a miss at crates/guards/chio-policy/src/evaluate/matchers.rs:199-229.
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: attestedchio policy analyze loads it and names both predicates. It marks them not_analyzed because the static analyzer cannot decide a runtime predicate without runtime evidence, not because the predicate is inert:
$ 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)Important field behavior:
- Omitting
credential_kindsaccepts any ofuri,x509_svid, andjwt_svid. Specify the accepted kinds to limit which credentials pass the rule. path_prefixesmatches on whole path segments, not on raw string prefixes. A workload at/payments/worker/shard-3matches/payments/worker; a workload at/payments/worker-shadowdoes not (crates/guards/chio-policy/src/evaluate/matchers.rs:414-435).- Pairing
require_workload_identitywithrequire_runtime_assurance_tiergives you both “which workload” and “how strongly attested”. They take different routes: the tier compiles into aConstraint::MinimumRuntimeAssuranceon the grant (crates/guards/chio-policy/src/compiler/scope.rs:207-209), while the identity match stays in the policy evaluator.
Soft match for gradual rollout
prefer_workload_identity instead of require_ during rollout. A missing or non-matching identity then yields a warning result rather than a deny (crates/guards/chio-policy/src/evaluate/matchers.rs:232-261), which lets you observe the identity population before flipping to a hard requirement. Both spellings empty the compiled default scope, so plan issuance before you add either one.Step 2: Issue an Identity-Bound Capability
Issuance can carry a runtimeAttestation block. The caller supplies the already-normalized assurance tier as part of that evidence; the authority does not derive it from the other fields. The trust policy (trusted_verifiers and the runtime_assurance tiers) is what later decides whether the asserted tier is honored, rebound, or rejected, stamping the capability with a minimum runtime-assurance constraint. Governed execution then re-checks that the presented evidence still clears the stamped tier.
Identity-bound issuance uses this endpoint: POST /v1/capabilities/issue on a trust-control cluster. The JSON body is an IssueCapabilityRequest carrying the subject public key, the scope, a TTL in seconds, and the runtimeAttestation evidence.
curl -X POST https://trust.example.com/v1/capabilities/issue \
-H "Authorization: Bearer $CHIO_TOKEN" \
-H "Content-Type: application/json" \
-d @issuance-request.json{
"subjectPublicKey": "7b0f6f63...",
"scope": {
"grants": [
{ "server_id": "payments", "tool_name": "transfer_funds", "operations": ["invoke"] }
]
},
"ttlSeconds": 300,
"runtimeAttestation": {
"schema": "chio.runtime-attestation.v1",
"verifier": "spire-agent",
"tier": "attested",
"issued_at": 1744537800,
"expires_at": 1744538100,
"evidence_sha256": "…",
"workload_identity": {
"scheme": "spiffe",
"credentialKind": "x509_svid",
"uri": "spiffe://prod.chio/payments/worker",
"trustDomain": "prod.chio",
"path": "/payments/worker"
}
}
}The mixed casing in that body is correct
IssueCapabilityRequest carries rename_all = "camelCase" (crates/platform/chio-control-plane/src/trust_control/service_types/requests.rs:16-24), RuntimeAttestationEvidence carries no rename at all (crates/core/chio-core-types/src/capability/runtime_attestation.rs:26-48), and the nested WorkloadIdentity is camelCase again (crates/core/chio-core-types/src/capability/workload_identity.rs:24-37). Normalizing the body to one casing makes it fail to deserialize.Fail-closed on conflict
workload_identity and a raw runtime_identity that do not agree, or if the SPIFFE URI is malformed, issuance fails. Do not wrap this in a retry. Fix the upstream and reissue.Step 3: Verify the Binding on the Receipt
A governed transaction that carried runtime attestation records the accepted evidence, including the normalized workloadIdentity, in the receipt's governed-transaction metadata. The block is RuntimeAssuranceReceiptMetadata (crates/core/chio-core-types/src/receipt/governance.rs:32-49), it hangs under the governed_transaction metadata key (crates/core/chio-core-types/src/receipt/metadata.rs:209), and both it and the nested WorkloadIdentity serialize in camelCase. So the path a reviewer reads is:
.metadata.governed_transaction.runtimeAssurance.workloadIdentity.uriThe block carries schema, verifierFamily, tier, verifier, evidenceSha256 and workloadIdentity. The schema value is the upstream attestation format, one of chio.runtime-attestation.v1 or a family-specific tag such as chio.runtime-attestation.azure-maa.jwt.v1.
Filter in the reader, not in the query
ReceiptQuery (crates/kernel/chio-kernel/src/receipt_query.rs:95-125) has no workload-identity filter. Its filters are capability id, tool server, tool name, outcome, time bounds, cost bounds and currency, cursor, limit, agent subject, and tenant. To get the set of admissions one workload made, narrow with those and then match the identity in jq over the returned receipts.Runtime-Attestation Appraisal
Raw runtimeAttestation evidence rides on the governed and issuance requests. A separate, adapter-facing contract normalizes it into a runtime-attestation appraisal. It carries the verifier family, an evidence descriptor, normalized assertions, and reason codes for cross-organization verifiers and auditors.
Export an appraisal locally from an evidence payload, optionally passing a HushSpec policy to evaluate its policy-visible outcomes:
chio trust appraisal export \
--input runtime-attestation.json \
--policy-file policy.yamlRemote deployments produce the same report over HTTP at POST /v1/reports/runtime-attestation-appraisal. A signed appraisal result can be exported for a partner (chio trust appraisal export-result or POST /v1/reports/runtime-attestation-appraisal-result) and evaluated on import against local policy (chio trust appraisal import or POST /v1/reports/runtime-attestation-appraisal/import). Imported results are defended by signature and freshness. There is no replay registry on that path, so a signed, in-window result can be presented more than once.
Rebinding to verified via Trusted Verifiers
Raw attestation evidence lands at the attested tier by default. To let a rule require verified, add an explicit trusted_verifiers entry that binds a {schema, verifier} pair to an effective tier. This keeps verifier trust in a policy extension instead of individual rules.
extensions:
runtime_assurance:
tiers:
verified:
minimum_attestation_tier: verified
max_scope:
operations: ["invoke"]
ttl_seconds: 300
trusted_verifiers:
spire_prod:
schema: chio.runtime-attestation.v1
verifier: https://spire.prod.internal
verifier_family: enterprise_verifier
effective_tier: verified
max_evidence_age_seconds: 120
allowed_attestation_types: [x509_svid]With this in place, a rule that sets require_runtime_assurance_tier: verified admits only calls whose attestation matches a trusted-verifier rule and satisfies its freshness and claim constraints.
verifier_family names an appraisal family, not an identity scheme
verifier_family takes an AttestationVerifierFamily, and the vocabulary is exactly four values: azure_maa, aws_nitro, google_attestation, enterprise_verifier (crates/core/chio-core-types/src/runtime_attestation.rs:25-30). There is no spiffe member: a SPIRE deployment presents as enterprise_verifier. Writing the scheme name there makes the whole document fail to parse. The field is optional, so leaving it out is also valid (crates/guards/chio-policy/src/models/extensions.rs:397-406).Verify the Result
The cheapest proof that the requirement is load-bearing is to dry-run the same call against the policy twice: once with the two identity fields removed, once with them present. Take the open form first.
$ 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
Now the policy from Step 1, byte for byte the same except for require_workload_identity and require_runtime_assurance_tier. Nothing in the request carries a SPIFFE identity or an attestation document, so nothing can satisfy them:
$ 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
Read the reason line carefully, because it is not the reason a first reading predicts. The refusal is a capability-selection refusal, not a named identity refusal: a policy carrying either workload-identity key compiles to an empty default scope (crates/guards/chio-policy/src/compiler/scope.rs:50-52), so chio check, which issues its capability from that scope, has nothing to present. In a real deployment the capability arrives from Step 2 instead, and the identity match then runs in the policy evaluator. Either way the call does not reach the tool.
The deny is on the record, and chio receipt explain reads it back with the policy hash that produced it:
$ 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
Failures and Recovery
The kernel denies admission, not warns, when any of the following hold. Matching operator recovery guidance lives in WORKLOAD_IDENTITY_RUNBOOK in the reference tree.
| Condition | Error | What to do |
|---|---|---|
Explicit workload_identity conflicts with raw runtime_identity | WorkloadIdentityError::Conflict | Fix the upstream so both agree; do not mask the conflict at the kernel. |
| SPIFFE URI is malformed (missing trust domain, empty path) | MissingTrustDomain, InvalidPath, MalformedUri | Regenerate the SVID from SPIRE or your attestation source; inspect the exact string. |
Presented identity fails require_workload_identity | Policy deny, reason runtime workload identity is missing or does not satisfy the required mapping | Either add the workload to the allowed prefixes, or accept the deny. Do not relax the match to get past one call. |
Evidence older than max_evidence_age_seconds | AttestationTrustError::EvidenceTooOld | Refresh the upstream attestation and resend. Do not extend the age ceiling to bypass freshness. |
| Attestation window has closed | AttestationTrustError::StaleEvidence | The evidence carries its own issued_at and expires_at; reissue rather than widening the window. |
| Verifier bridge projects a non-SPIFFE identifier | OpaqueRuntimeIdentityConflict, or no identity resolves at all | Fix the projection rule on the bridge. Chio will not invent a typed identity from an opaque string. |
The identity variants are WorkloadIdentityError (crates/core/chio-core-types/src/capability/workload_identity.rs:41-100), raised from normalized_workload_identity (crates/core/chio-core-types/src/capability/runtime_attestation.rs:57-110); the freshness variants are AttestationTrustError (crates/core/chio-core-types/src/capability/trust_policy.rs:62-91).
Summary
- Write the rule. Add
require_workload_identitywith an explicit trust domain, path prefix set, and credential kind set. - Issue with attestation. Pass
runtimeAttestationon issuance so the capability is stamped with the minimum runtime-assurance tier. - Verify on the receipt. The accepted
workloadIdentityappears under the kernel signature at.metadata.governed_transaction.runtimeAssurance. Audit pipelines narrow with the query filters and then match the identity in the reader. - Rebind to verified when needed. Use
trusted_verifiersto promote raw attestation into theverifiedtier under explicit operator policy.
Next Steps
- Workload Identity (concept) · the normalized shape and why SPIFFE is canonical
- Envoy ext_authz · the mesh delivery path for SPIFFE principals
- Policy Schema · the full
WorkloadIdentityMatchcontract - Rotate Keys & Revoke · what to do when a workload's identity material changes