Chio/Docs
LOGIN · JOIN

Workload Identity

Qualify an agent’s authority by the workload that presents it, with one SPIFFE identity model across meshes, services, and attested runtimes.

Concept chio@5f63e6e

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.

A normalized workload identityjson
{
  "scheme": "spiffe",
  "credentialKind": "x509_svid",
  "uri": "spiffe://prod.chio/payments/worker",
  "trustDomain": "prod.chio",
  "path": "/payments/worker"
}
FieldWhat policy can distinguish
trustDomainThe identity namespace, matched exactly.
pathA workload or a subtree of workloads, matched on whole path segments.
credentialKindThe 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.

IntegrationWhat reaches ChioTrust boundary
Envoy ext_authzAn 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 attestationThe 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 bridgeA 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.

payments-policy.yamlyaml
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: attested

A 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.

SPIFFE path tree under prod.chio with the subtrees admitted by the policy prefixes
match(p,x)  ⟺  x=p  ∨  x=p / s\mathrm{match}(p, x) \iff x = p \;\lor\; x = p\,\htmlClass{cor-relation}{\texttt{/}}\,s
admit(x)  ⟺  td(x)=prod.chio  ∧  ∃ p∈P: match(p,x)\mathrm{admit}(x) \iff \htmlClass{cor-parent}{\mathrm{td}(x) = \texttt{prod.chio}} \;\land\; \exists\, \htmlClass{role-call}{p} \in P:\ \mathrm{match}(\htmlClass{role-call}{p}, x)
spiffe://prod.chiopaymentsworkershard-3shard-7worker-shadowreconcilerbillinginvoicerx, the identity path/payments/payments/worker/payments/worker/shard-3/payments/worker/shard-7/payments/worker-shadow/payments/reconciler/billing/billing/invoicer
x=px = \htmlClass{role-call}{p}
x=p / shard-3x = \htmlClass{role-call}{p}\,\htmlClass{cor-relation}{\texttt{/}}\,\texttt{shard-3}
x=p / shard-7x = \htmlClass{role-call}{p}\,\htmlClass{cor-relation}{\texttt{/}}\,\texttt{shard-7}
x=p -shadow,  -≠/x = \htmlClass{role-call}{p}\,\texttt{-shadow}, \ \ \htmlClass{role-deny}{\texttt{-} \ne \texttt{/}}
x=px = \htmlClass{role-call}{p}
also presentedspiffe://staging.chio/payments/worker
td(x)≠prod.chio\htmlClass{role-deny}{\mathrm{td}(x) \ne \texttt{prod.chio}}
spiffe://prod.chio/payments//worker
// in x⇒rejected\htmlClass{role-deny}{\texttt{//} \text{ in } x \Rightarrow \text{rejected}}
P={/payments/worker, /payments/reconciler}P = \{\texttt{/payments/worker},\ \texttt{/payments/reconciler}\}
click a node to add or remove it as a prefix
SPIFFE path tree under prod.chio with the subtrees admitted by the policy prefixesspiffe://prod.chiopaymentsworkerx = pshard-3p/…shard-7p/…worker-shadowp-…≠ /reconcilerx = pbillinginvoiceralso presentedstaging.chio /payments/worker≠ prod.chio/payments//workercontains //click a node to add or remove it as a prefix

4 of 8 paths admitted. /payments/worker-shadow shares the letters of /payments/worker but not a whole segment, so it is excluded.

match(p,x)  ⟺  x=p  ∨  x=p / sadmit(x)  ⟺  td(x)=prod.chio∧ ∃ p∈P: match(p,x)\begin{aligned} \mathrm{match}(p, x) &\iff x = p \;\lor\; x = p\,\htmlClass{cor-relation}{\texttt{/}}\,s \\ \mathrm{admit}(x) &\iff \htmlClass{cor-parent}{\mathrm{td}(x) = \texttt{prod.chio}} \\ &\qquad \land\ \exists\, \htmlClass{role-call}{p} \in P:\ \mathrm{match}(\htmlClass{role-call}{p}, x) \end{aligned}
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('/'))
}
An illustrative path tree under one trust domain, with the policy prefixes from the page. Paths match on whole segments and the trust domain matches exactly. Matching establishes the shape of an identity, not why the deployment should believe it, and a trust domain is not a tenant identifier.

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.

DimensionMatches when
schemeThe identity’s scheme equals the configured one.
trust_domainThe trust domain is exactly equal. Domains are not compared by suffix.
path_prefixesThe path equals a listed prefix or continues it with a slash.
credential_kindsThe declared credential kind is in the list.
require_runtime_assurance_tierThe 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:

workload-identity · analyzetranscript
$ chio policy analyze ./payments-policy.yaml
policy_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)
exit 0

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.

Without identity requirements · captured preflighttranscript
$ 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
exit 0allow

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
crates/economy/chio-appraisal/src/appraisal.rs791-836rust
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.

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.

Scope, tier, identityChoose a request.
A governed transfer request passes three ordered checks: capability scope, the effective runtime tier against the attested floor, and the workload identity against the policypresented record rspiffe://prod.chio/payments/worker-shadowx509_svid · asserts attested1g1capability covers the tool1g2effective tier meets the floor0g3identity matches the policytransfer_funds∈scope(c)τ_eff = attested≥attestedprod.chio=prod.chio/payments/worker-shadow∉Px509_svid∈KDenyruntime workload identity is missing or does notsatisfy the required mappingrules.tool_access.require_workload_identityruntime assurance tiernonebasicattestedverifiedfloorassertedeffectiveappraisalaccepted by local trust policy
admit(r)  ⟺  t∈scope(c)  ∧  τeff(r)≥τmin⁡  ∧  id(r)⊨P\mathrm{admit}(\htmlClass{role-call}{r}) \iff \htmlClass{role-call}{t} \in \htmlClass{cor-parent}{\mathrm{scope}(c)} \;\wedge\; \tau_{\mathrm{eff}}(\htmlClass{role-call}{r}) \ge \htmlClass{cor-parent}{\tau_{\min}} \;\wedge\; \mathrm{id}(\htmlClass{role-call}{r}) \models \htmlClass{cor-parent}{P}
A governed transfer request passes three ordered checks: capability scope, the effective runtime tier against the attested floor, and the workload identity against the policypresented record rspiffe://prod.chio/payments/worker-shadowx509_svid · asserts attested1capability covers the tool1effective tier meets the floor0identity matches the policytransfer_funds∈scope(c)τ_eff = attested≥attestedprod.chio=prod.chio/payments/worker-shadow∉Px509_svid∈KDenyruntime workload identity is missing ordoes not satisfy the required mappingrules.tool_access.require_workload_identityruntime assurance tiernonebasicattestedverifiedfloorassertedlocal trust policy accepted the recordeffective tier attested

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.

admit(r)  ⟺  t∈scope(c)  ∧  τeff(r)≥τmin⁡  ∧  id(r)⊨P\mathrm{admit}(\htmlClass{role-call}{r}) \iff \htmlClass{role-call}{t} \in \htmlClass{cor-parent}{\mathrm{scope}(c)} \;\wedge\; \tau_{\mathrm{eff}}(\htmlClass{role-call}{r}) \ge \htmlClass{cor-parent}{\tau_{\min}} \;\wedge\; \mathrm{id}(\htmlClass{role-call}{r}) \models \htmlClass{cor-parent}{P}
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))
}
The payments policy from this page, with illustrative requests. Scope is checked before any rule; the 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:

Missing issuance authority · captured preflighttranscript
$ 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 2deny

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:

Terminalbash
DENY=$(chio receipt list --tool-name transfer_funds --outcome deny --order newest --limit 1 --admin-all | jq -r .id)
chio receipt explain "$DENY" --admin-all
Signed denial · captured explanationcaptured output
receipt: 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
exit 0

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:

check-transfer.tscheck_transfer.pysrc/main.rs · in mainTerminal
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);
Output
authorized true
tier       attested azure_maa
workload   spiffe://prod.chio/payments/worker/shard-3 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.

Put workload identity into policy