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.sh— service-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.py— L7 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 assertssandbox.network.blockednames192.0.2.123exactly.
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-signproduces a signedWitnessedHeadfor 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
litatolibinary on PATH, and its keys:LITATOLI_KEY_FILE(BLAKE3 chain) andLITATOLI_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
tmpfspath).
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.comALLOW (it is the contract),1.1.1.1DENY with reasonTARGET_NOT_IN_USAGE_CONTRACTS— a judgement the IP/port layer cannot make. (PimaTika also denies a contracted target used for the wrongpurpose/data_classwithPURPOSE_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 order —
create → 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.