Skip to content

The conclusive test — all three bricks, for real

End-to-end proof that NaZelo Trust does what it claims: PimaTika decides each egress from a signed BIM, NaZelo enforces the decision on the wire, Litatoli seals it all into one signed chain, joined by one run_id. Run as root with eBPF on 2026-08-11.

Everything here is committed and reproducible — the BIM, the probe, the runner, and a captured sample chain live in examples/conclusive/.

Sibling conclusive runs (also root, also non-KVM). The same examples/conclusive/ directory now carries three more, each proving a V2 enforcement end-to-end at the kernel:

  • run_sg.shservice-graph narrowing: a contract-permitted target that is not an out-edge of this service is dropped by the eBPF filter, not merely un-projected.
  • run_l7.sh + l7_in_sandbox.pyL7 in-sandbox: the workload reaches an allowed host ONLY through the stream proxy (bypass lock), a direct connection is dropped, a forbidden host is dropped by the proxy.
  • The process-governance and secrets-broker runs are non-privileged (bwrap only).

Deterministic variant for CI

The run above uses example.com and its public IP — fine as an internet smoke test, but not reproducible. examples/ci/run-deterministic.sh (driven by the test_deterministic_local_e2e test) removes both the internet and DNS dependencies:

  • ALLOW = a local HTTP server on a free port, reachable from the sandbox's netns via the veth gateway. The BIM is signed at test time against the local address (discovered without DNS, via the interface that holds the default route), so it is reproducible on any host that has a default route — not a literally-any-host claim.
  • DENY = 192.0.2.123 (TEST-NET-1, RFC 5737 — reserved for docs), dropped by the eBPF filter before it leaves the host, so the deny is deterministic without it being reachable. It is deliberately non-symmetric (reversed: 123.2.0.192) so the enforcer's sealed drop evidence doubles as an end-to-end endianness guard — the test asserts sandbox.network.blocked names 192.0.2.123 exactly.

Same three-brick guarantee, same pinned + head-verified chain, no network flake. It needs the same privileged runner (root + eBPF + keys) plus pimatika-cli to sign; the test skips cleanly when those are absent (and NAZELO_TRUST_REQUIRE_E2E=1 turns that skip into a failure, for a dedicated privileged CI job).

Head-receipt caveat. The receipt records the trusted head separately from the chain, which catches truncation. It is not itself signed: an attacker who can rewrite BOTH the chain and the receipt could make them agree. For a hostile setting, publish the head to an independent location (CI logs, a witness, a second host) — litatoli witness-sign produces a signed WitnessedHead for exactly this.

What it needs

  • Linux, root (a per-run network namespace needs CAP_SYS_ADMIN).
  • eBPF tooling on PATH: bpftool, tc, clang, nsenter.
  • The litatoli binary on PATH, and its keys: LITATOLI_KEY_FILE (BLAKE3 chain) and LITATOLI_ED25519_KEY_FILE (Ed25519 signer). The BIM is signed with the same keys (pimatika-cli sign-bim).
  • A workspace under a real filesystem (bwrap cannot bind a tmpfs path).

The policy — a signed BIM

examples/conclusive/bim.signed.yaml declares ONE usage contract and default_policy: DENY:

intent_profile:
  usage_contracts:
  - name: probe-example
    target: https://example.com
    purpose: conclusive-network-probe
    data_class: public
    allowed_methods: [CONNECT, GET]
    max_payload_kb: 64

Run it

sudo env \
  LITATOLI_KEY_FILE=~/.config/litatoli/signing.key \
  LITATOLI_ED25519_KEY_FILE=~/.config/litatoli/ed25519.key \
  PATH="/path/to/litatoli/dir:$PATH" \
  examples/conclusive/run.sh

The result

example.com resolved host-side to 172.66.147.243
allowed  example.com 172.66.147.243:443 -> OPEN
denied   1.1.1.1:443          -> BLOCKED(TimeoutError)
PimaTika (bim blake3:7c396d578912d…) decided:
  ALLOW  https://example.com
  DENY TARGET_NOT_IN_USAGE_CONTRACTS  https://1.1.1.1
run_id: RUN-CONCLUSIVE · exit=0 · PimaTika-decided, NaZelo-enforced · sealed

Three bricks, each doing its own job:

  • PimaTika decided, semantically, from the signed BIM: example.com ALLOW (it is the contract), 1.1.1.1 DENY with reason TARGET_NOT_IN_USAGE_CONTRACTS — a judgement the IP/port layer cannot make. (PimaTika also denies a contracted target used for the wrong purpose/data_class with PURPOSE_MISMATCH / DATA_CLASS_MISMATCH.)
  • NaZelo enforced that decision on the wire: the allowed target OPEN, the denied one BLOCKED. Not luck — a control run with no sandbox lets both through.
  • Litatoli sealed both into ONE chain.

The evidence — one chain, both bricks, verified

examples/conclusive/sample-chain.jsonl is the actual chain from the run above:

seq 1  PIMATIKA_EGRESS_DECISION   (ALLOW example.com)      run_id=RUN-CONCLUSIVE
seq 2  PIMATIKA_EGRESS_DECISION   (DENY  1.1.1.1)          run_id=RUN-CONCLUSIVE
seq 3  sandbox.create
seq 4  sandbox.network.grant      (filter installed)
seq 5  sandbox.exec               (the command ran)
seq 6  sandbox.network.blocked    (enforcer's own drop counts)
seq 7  sandbox.egress.allowed     (enforcer's own allowed-egress volume)
seq 8  sandbox.network.revoke     (filter removed)
seq 9  sandbox.destroy
seq 10 nazelo.workload.output        (the probe's OPEN/BLOCKED, sealed)
seq 11 nazelo.run.sealed

Seq 7 is the counterpart to seq 6: the enforcer's own count of what it let through per destination (the eBPF egress_allowed byte counter), sealed beside what it blocked. Both are the enforcer's observation, not the workload's.

The sandbox lifecycle (seq 3–9) is sealed in causal ordercreate → grant → exec → blocked → allowed → revoke → destroy — whose relative ordering the test_deterministic_local_e2e test asserts directly. Note the tail: the lifecycle entries are sealed as the run happens, but nazelo.workload.output and nazelo.run.sealed (seq 10–11) are sealed by NaZelo Trust at collection time — after destroy, when it has the retrieved result in hand. So the chain is not a claim that all eleven entries occurred in strict temporal order; it is a signed, ordered record whose lifecycle segment matches causality.

PUB=$(litatoli export-pubkey)
HEAD=$(python3 -c 'import json,sys;print(json.loads([l for l in open(sys.argv[1]) if l.strip()][-1])["blake3_signature"])' examples/conclusive/sample-chain.jsonl)
litatoli verify-chain --log-file examples/conclusive/sample-chain.jsonl --expected-pubkey "$PUB" --expected-head "$HEAD"
# → overall_ok: True · pinned-leaf · head pinned (end-truncation would fail)

The PimaTika decisions and the NaZelo lifecycle are in one signed chain, every entry Ed25519-signed, seq-ordered, prev_hash-linked, pinned to the signer.

The run_id is a structured, signed, queryable top-level field — not buried in the payload. One query returns the whole run, across both engines:

litatoli query --log-file examples/conclusive/sample-chain.jsonl --run-id RUN-CONCLUSIVE
# seq=1..2 … PIMATIKA_EGRESS_DECISION   (the ALLOW and the DENY)
# seq=3..10 … the NaZelo lifecycle, the enforcer's blocked count, and the output
# 10 entries with run_id=RUN-CONCLUSIVE

That is the ADR-0005 correlation key, end to end: PimaTika's semantic decisions and NaZelo's enforcement, selected by one signed field, verifiable in one chain.