Chio/Docs
LOGIN · JOIN

Bind Workload Identity

Restrict a payments tool to one workload and accepted runtime assurance, then read the identity the kernel signed into the receipt.

Guide chio@5f63e6e

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.

Start

1. 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_funds and reconcile_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.
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

Run the analyzer. It reports both predicates as not_analyzed, because their result depends on runtime evidence that only arrives with a request.

Analyze the payments policytranscript
$ 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

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.

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.Workload identity

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.

payments-policy.yaml · appendedyaml
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.

Terminalbash
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.json

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

Terminalbash
jq '.metadata.governed_transaction.runtimeAssurance |
  {schema, verifierFamily, tier, verifier, evidenceSha256, workloadIdentity}' \
  receipt.json
Output
{
  "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

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.

No issued 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

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.

Inspect the captured refusaltranscript
$ chio --receipt-db ./receipts.db receipt explain "$DENY" --admin-all
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

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.

payments-open.yamlyaml
hushspec: "0.1.0"
name: payments-open
rules:
  tool_access:
    enabled: true
    default: block
    allow:
      - transfer_funds
      - reconcile_ledger
A policy with a representable default scopetranscript
$ 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

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.

Next: plan for the workload losing trust