Workload Identity
Qualify an agent’s authority by the workload that presents it, with one SPIFFE identity model across meshes, services, and attested runtimes.
On this page
A capability names a subject and the operations it may perform. It does not name the process that presents the request, and in a shared environment that process is a separate fact: a payments worker, a shadow copy of it, and a reconciler can each run under one operator and each present authority. Workload identity supplies that fact as a SPIFFE ID, which the platform running the workload authenticates.
Chio lets policy require both facts. Before a transfer is dispatched, the kernel checks that a grant covers the tool, that a verifier you configured accepted the workload's runtime at the required tier, and that the accepted identity lies inside the workloads the policy names. Authority for the operation and requirements on the workload stay separate, and the signed receipt records the identity the kernel evaluated.
Every source of identity becomes one SPIFFE record
Chio normalizes supported workload identities to a SPIFFE ID: a URI with a trust domain and a workload path. This identity names the payments worker in the prod.chio trust domain. Chio derives the domain and path from the URI and rejects a record whose fields disagree with it.
{
"scheme": "spiffe",
"credentialKind": "x509_svid",
"uri": "spiffe://prod.chio/payments/worker",
"trustDomain": "prod.chio",
"path": "/payments/worker"
}| Field | What policy can distinguish |
|---|---|
trustDomain | The identity namespace, matched exactly. |
path | A workload or a subtree of workloads, matched on whole path segments. |
credentialKind | The declared presentation kind: uri, x509_svid, or jwt_svid. |
The credential kind describes the record. Writing x509_svid in JSON does not verify a certificate. Normalization establishes the shape of an identity; authentication establishes why the deployment should believe it. Chio consumes the identity. The runtime, mesh, or attestation integration supplies its evidence under the operator’s trust configuration.
Choose the source of identity
Each integration delivers the identity through a different trust boundary. Policy uses the same typed fields once the mapping is complete.
| Integration | What reaches Chio | Trust boundary |
|---|---|---|
| Envoy ext_authz | An upstream principal in the adapter’s caller identity. | The deployment’s authorization bridge maps and evaluates the principal; the adapter relies on Envoy’s authentication. |
| Explicit runtime attestation | The workload identity supplied with the issuance or governed request. | A supplied record must meet the local trust policy before it contributes accepted runtime assurance. |
| Azure MAA verifier bridge | A configured SPIFFE projection from the selected workload claim. | Verifier configuration determines acceptance; the selected claim must produce a valid SPIFFE identity. |
AWS Nitro and Google Confidential VM bridges also supply runtime attestation evidence, but they do not construct a typed workload identity. Configure the workload identity integration separately when policy requires one.
Legacy identity records
A legacy runtimeIdentity string produces a typed workload identity when it parses as a valid SPIFFE URI. When no typed identity is present, Chio preserves a non-SPIFFE string as opaque metadata. If an explicit typed identity and a SPIFFE runtime identity disagree, normalization fails.
Restrict a payment operation to its workload
This policy allows two payment tools and blocks everything else. It requires a workload under the worker or reconciler paths, one of two credential kinds, and at least attested runtime assurance. The preflights below run it, and one variant without its two requirements.
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: attestedA path prefix is a subtree, not a string prefix. /payments/worker covers itself and everything below it, such as /payments/worker/shard-3. It does not cover /payments/worker-shadow, even though that path begins with the same characters: the character after the prefix must be a slash.
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('/'))
}
Configured dimensions must all match, and an omitted dimension adds no restriction. Within path_prefixes, any one listed prefix is enough; a trailing slash on a prefix is ignored, and the prefix / covers every path. A path that does not begin with a slash, or that contains an empty segment, matches no prefix.
| Dimension | Matches when |
|---|---|
scheme | The identity’s scheme equals the configured one. |
trust_domain | The trust domain is exactly equal. Domains are not compared by suffix. |
path_prefixes | The path equals a listed prefix or continues it with a slash. |
credential_kinds | The declared credential kind is in the list. |
require_runtime_assurance_tier | The effective tier is at least the floor, in the order none, basic, attested, verified. |
Static analysis cannot settle either requirement, because both depend on the record a request carries. chio policy analyze reports them as runtime predicates:
$ 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)Identity requirements change what the policy can issue. Both require_workload_identity and prefer_workload_identity empty the policy’s compiled default scope, so issuance must supply the required authority and runtime evidence. Changing require_ to prefer_ turns an identity mismatch into a policy warning; it does not restore the default scope. The same preflight shows the difference: payments-open.yaml is the policy above without its two requirements, and it admits the transfer.
$ 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
Identity and assurance answer different questions
Workload identity asks which workload presents the request. Runtime assurance asks what the local verifier accepts about the environment that workload runs in. The payments policy requires both: a matching identity and an effective tier of at least attested.
A caller’s asserted tier does not determine the effective tier. Local appraisal checks the attestation record against the configured trust policy. When no trust policy with rules is configured, the evidence is not accepted and the effective tier is none, with the reason that it did not cross a local verified trust boundary. A request that asserts verified then still fails an attested floor. Policy reads the workload identity from the same accepted record, so a record the verifier did not accept contributes neither a tier nor an identity.
How appraisal sets the effective tier
fn verify_runtime_attestation_policy_outcome(
evidence: &RuntimeAttestationEvidence,
trust_policy: Option<&AttestationTrustPolicy>,
now: u64,
) -> Result<VerifiedRuntimeAttestationPolicyVerification, RuntimeAttestationVerificationError> {
let trust_policy_configured = trust_policy.is_some_and(|policy| !policy.rules.is_empty());
if trust_policy_configured {
let resolved = evidence
.resolve_effective_runtime_assurance(trust_policy, now)
.map_err(RuntimeAttestationVerificationError::TrustPolicy)?;
let matched_trust_rule = resolved.matched_rule.clone();
return Ok(VerifiedRuntimeAttestationPolicyVerification {
outcome: RuntimeAttestationPolicyOutcome {
trust_policy_configured: true,
accepted: true,
effective_tier: resolved.effective_tier,
reason: matched_trust_rule
.as_ref()
.map(|rule| format!("matched attestation trust rule `{rule}`")),
},
matched_trust_rule,
});
}
evidence.validate_workload_identity_binding()?;
if !evidence.is_valid_at(now) {
return Err(RuntimeAttestationVerificationError::StaleEvidence {
now,
issued_at: evidence.issued_at,
expires_at: evidence.expires_at,
});
}
Ok(VerifiedRuntimeAttestationPolicyVerification {
outcome: RuntimeAttestationPolicyOutcome {
trust_policy_configured: false,
accepted: false,
effective_tier: RuntimeAssuranceTier::None,
reason: Some(
"runtime attestation evidence did not cross a local verified trust boundary"
.to_string(),
),
},
matched_trust_rule: None,
})
}Where a capability carries a minimum runtime assurance constraint, governed execution checks local acceptance and the effective tier, and identity matching remains an additional policy check. The Assurance model separates these runtime checks from the kernel’s other guarantees.
A refusal names the first check that failed
The kernel checks the grant's scope before any policy rule runs. The tool_access rule then checks the tier floor, and only then the identity. Evaluation stops at the first failure and records its reason, so the reason tells you which of three different fixes applies: issue a grant, configure the verifier, or move the workload.
A string prefix is not a subtree
/payments/worker-shadow begins with the characters of /payments/worker, but the next character is not a slash, so no listed prefix covers it.
In Rust · matchers.rs:41017 lines
fn workload_identity_matches(
expected: &crate::models::WorkloadIdentityMatch,
actual: &chio_core::capability::workload_identity::WorkloadIdentity,
) -> bool {
expected.scheme.is_none_or(|scheme| scheme == actual.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)))
&& (expected.credential_kinds.is_empty()
|| expected.credential_kinds.contains(&actual.credential_kind))
}
tool_access rule then checks the tier floor before the identity, and reads both only from a record the local verifier accepted. The reasons are the kernel's own strings. A grant that carries a minimum runtime assurance constraint is checked again in governed validation, which is left out.The first case is the one a new deployment meets. The same preflight under the payments policy, with no identity-bound issuance, is refused at capability scope validation and exits with code 2:
$ 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
The compiled default scope is empty, so no grant covers transfer_funds, and the identity was never read. chio receipt explain reads the signed denial back and names the kernel as the deciding check:
DENY=$(chio receipt list --tool-name transfer_funds --outcome deny --order newest --limit 1 --admin-all | jq -r .id)
chio 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
The preflight exercises the issuance prerequisite. It does not run a live SPIFFE authentication exchange or execute a payment. Bind Workload Identity walks through policy analysis, issuance, and receipt inspection.
Inspect the identity the kernel accepted
For a locally accepted runtime record, receipt metadata can carry the normalized workload identity and its assurance information under governed_transaction.runtime_assurance. That binds the identity the kernel evaluated to its signed decision and gives an auditor the fields the decision used. Verify the receipt against the kernel key you pinned, then read the identity from the verified record:
import { readFileSync } from "node:fs";
import { parseReceiptJson, verifyReceiptWithTrustedSigners } from "@chio-protocol/sdk/invariants";
import { TrustFile } from "@chio-protocol/sdk/trust";
const trust = await TrustFile.load();
const json = readFileSync("transfer-receipt.json", "utf8");
const check = verifyReceiptWithTrustedSigners(parseReceiptJson(json), trust.keys("receipt-kernel"));
if (!check.ok) throw new Error("receipt integrity or signer trust failed");
const assurance = JSON.parse(json).metadata.governed_transaction.runtime_assurance;
const { tier, verifierFamily, workloadIdentity } = assurance;
console.log("authorized", check.authorized);
console.log("tier ", tier, verifierFamily);
console.log("workload ", workloadIdentity.uri, workloadIdentity.credentialKind);authorized true tier attested azure_maa workload spiffe://prod.chio/payments/worker/shard-3 x509_svid
import json
from chio.invariants import parse_receipt_json, verify_receipt_with_trusted_signers
raw = open("transfer-receipt.json").read()
signers = json.load(open("larkfield-signers.json"))
check = verify_receipt_with_trusted_signers(parse_receipt_json(raw), signers)
if not check["ok"]:
raise ValueError("receipt integrity or signer trust failed")
assurance = json.loads(raw)["metadata"]["governed_transaction"]["runtime_assurance"]
identity = assurance["workloadIdentity"]
print("authorized", json.dumps(check["authorized"]))
print("tier ", assurance["tier"], assurance["verifierFamily"])
print("workload ", identity["uri"], identity["credentialKind"])authorized true tier attested azure_maa workload spiffe://prod.chio/payments/worker/shard-3 x509_svid
use chio_binding_helpers::verify_receipt_json_with_trusted_signer_hex;
let json = std::fs::read_to_string("transfer-receipt.json")?;
let key = std::fs::read_to_string("trusted-kernel-key.hex")?;
let check = verify_receipt_json_with_trusted_signer_hex(&json, &[key.trim()])?;
if !check.ok {
return Err("receipt integrity or signer trust failed".into());
}
let receipt: serde_json::Value = serde_json::from_str(&json)?;
let assurance = &receipt["metadata"]["governed_transaction"]["runtime_assurance"];
let identity = &assurance["workloadIdentity"];
println!("{:<10} {}", "authorized", check.authorized);
println!("{:<10} {} {}", "tier", assurance["tier"].as_str().unwrap_or("none"), assurance["verifierFamily"].as_str().unwrap_or("-"));
println!("{:<10} {} {}", "workload", identity["uri"].as_str().unwrap_or("-"), identity["credentialKind"].as_str().unwrap_or("-"));authorized true tier attested azure_maa workload spiffe://prod.chio/payments/worker/shard-3 x509_svid
chio receipt list --tool-name transfer_funds --outcome allow --limit 1 --admin-all > transfer.jsonl
chio receipt verify --input transfer.jsonl --trusted-kernel-pubkey 4f1c…a2e9
jq -c '.metadata.governed_transaction.runtime_assurance
| {tier, verifierFamily, workload: .workloadIdentity.uri, kind: .workloadIdentity.credentialKind}' transfer.jsonlverified 1 receipt from kernel 4f1c…a2e9
{"tier":"attested","verifierFamily":"azure_maa","workload":"spiffe://prod.chio/payments/worker/shard-3","kind":"x509_svid"}The tier is the one the verifier accepted, after any trust-policy rebinding, and the identity is the one policy matched. A record whose verification fails, or whose signer is not pinned, says nothing about the workload.
A receipt authenticates the kernel’s record of what it accepted. Assessing whether the identity claim was true also needs the integration’s authentication boundary and the verifier policy. The receipt format documents the fields, and Receipts explains what their verification establishes.
Receipt read access uses a separate tenant tag. A SPIFFE trust domain is not a tenant identifier; see Tenancy and read scoping.
The deployment authenticates the workload
Chio checks that a normalized identity matches the policy and that the effective tier meets its floor, and it records what it accepted. The deployment establishes that the identity is authentic, configures the verifier that sets the tier, and issues capabilities that carry the required authority. Configure the identity source, the accepted runtime evidence, and the permitted workload paths together.