Receipts: evidence of how a run was caged

Child::receipt gives, for every finished run, the policy digest, preset, backend and tier, OS build or kernel, everything accepted short of the policy, the violation summary and the exit: a record a harness can attach to a command's output.

A receipt records that a command ran caged, and in which cage. Once the child has exited, Child::receipt() returns a Receipt (None before). Reading it ends the run, as kill_all does, so its audit summary covers a finished run.

use procage::native::{Cage, Grants};
use procage::policy::presets;

let cage = Cage::new(&presets::minimal::v3())?;
let mut child = cage.spawn(&mut std::process::Command::new("/bin/ls"), &Grants::new())?;
child.wait()?;
let receipt = child.receipt().expect("the child exited");
std::fs::write("/var/log/my-harness/run-42.receipt.json", receipt.to_json())?;

What a receipt holds#

FieldMeaning
policy_digestsha256:… of the policy’s canonical serialised form: equal policies have equal digests on every system
preset, baseThe preset tag and base profile version
backendmacOS: apply method, OS release and build. Linux: tier, Landlock ABI and errata, kernel release
approximations, relaxations, waiversEverything the cage accepted short of the policy; a relaxation says what would lift it
enforced_as_writtentrue only when all three lists are empty
violationsThe record’s coverage (off, observed, system-calls, unavailable), whether it was complete, and the count (absent when nothing was observed)
exitThe exit code or signal, and whether the caller ended the run (kill_all)
auditThe audits’ mode, finding counts and completeness; their paths only with the policy included
grants, procageThe grant names bound for this run, and the procage version
policyThe policy itself, only with CageOptions::with_policy_in_receipt()

What a receipt is not#

  • Not an attestation. It is the caller’s own unsigned record, built in the calling process. Keep it outside every path a later run may write.
  • Not proof that nothing was denied. macOS violation reports are best effort, so a complete, empty record proves nothing. With reporting off the receipt says off, never “none”.
  • Not anonymous. It names the OS release and build or the kernel release of the machine.
  • Not for Cage::exec, which leaves no handle.

Privacy and format#

A receipt holds no environment value, command argument, file content or process name. Paths appear only with CageOptions::with_policy_in_receipt(), which adds the whole policy, the subjects of relaxations and the audits’ paths.

Receipt::to_json writes a versioned form ("version": 1); Receipt::from_json refuses another version and any unknown field, at any depth, rather than show a receipt it understood only in part.

Edit this page on GitHub