Skip to content

NaZelo Trust — the integration contract

Version 0.1.0, like every engine it composes. What follows is what the three of them guarantee together, what they need, and what they do not do.

What NaZelo Trust is (and is not)

NaZelo Trust is the integrated trust offering that composes three independent products so a run is decided, enforced, and proven under one correlation id:

  • PimaTika decides — from a signed BIM — what egress is allowed.
  • NaZelo isolates the run, enforces the decision, and observes its own drops.
  • Litatoli seals every step into a signed, chain-linked evidence log and verifies it.

NaZelo Trust is not a rename of NaZelo, which remains the critical-runtime product in its own right (nazelo.dev). NaZelo, PimaTika and Litatoli each remain usable on their own. NaZelo Trust is the value of the three fitting together.

How this is validated

Continuously, by .github/workflows/composition.yml, on every push.

The job clones the three engines, builds the two Rust ones, generates the key sealing needs, re-signs the example manifests with it, and then demands the whole stack be present: NAZELO_TRUST_REQUIRE_COMPOSITION_COVERAGE=1 turns a missing engine into a collection error rather than a quiet skip. It runs the suite, then the three shipped commands, and fails if doctor ever reports a capability blocked without naming what blocks it.

This page used to pin four commit SHAs and a validation date instead. The rule it stated was right — a green run on one four-tuple says nothing about a different four-tuple — but nothing enforced it: every component moved and no new baseline entry followed, so the page claimed a validation that no longer described anything. The CI does what the pin was pretending to do, against whatever revision each engine is actually at.

nazelo-trust doctor prints the versions it is speaking to, read at runtime.

Prerequisites

  • Linux, root (a per-run network namespace needs CAP_SYS_ADMIN).
  • Kernel with eBPF + tc (clsact / BPF classifier). Validated on 6.6.87.2-microsoft-standard-WSL2.
  • Tools on PATH: bpftool, tc (iproute2), clang, nsenter, and CAP_SYS_ADMIN to enter the sandbox's network namespace. Having the four binaries without the privilege refuses the run, naming the privilege.
  • The litatoli binary + its keys (LITATOLI_KEY_FILE BLAKE3, LITATOLI_ED25519_KEY_FILE Ed25519).
  • The pimatika-cli binary (to sign the BIM).

Reproducible command

The portable, deterministic reproducer (local server as ALLOW, 192.0.2.123 as DENY — no internet, no DNS):

sudo env \
  LITATOLI_KEY_FILE=~/.config/litatoli/signing.key \
  LITATOLI_ED25519_KEY_FILE=~/.config/litatoli/ed25519.key \
  PATH="/path/to/litatoli/dir:$PATH" \
  PIMATIKA_CLI=/path/to/pimatika-cli \
  examples/ci/run-deterministic.sh

Driven in CI by tests/test_ci_deterministic.py (set NAZELO_TRUST_REQUIRE_E2E=1 so a privileged job fails instead of skipping). Full walk-through: docs/CONCLUSIVE-TEST.md.

Guaranteed behaviour

For a run under this baseline:

  1. PimaTika decides. A signed BIM is loaded and verified; each egress intent yields a sealed EGRESS_ALLOWED / EGRESS_DENIED with a semantic reason (TARGET_NOT_IN_USAGE_CONTRACTS, PURPOSE_MISMATCH, …).
  2. NaZelo enforces and observes. The allowed targets are projected into the eBPF filter; a non-allowed IPv4 L3/L4 destination is dropped in-kernel and the enforcer's own per-destination drop count is sealed (sandbox.network.blocked{dst_ip, dropped_packets}).
  3. Litatoli seals and verifies. One chain holds the PimaTika decisions and the NaZelo lifecycle; verify-chain accepts it pinned to the signer and a head receipt.
  4. Causal lifecycle order. The sandbox lifecycle is sealed in causal order: create → grant → exec → blocked → revoke → destroy.
  5. One correlation id. A single run_id joins all of it; the chain is queryable by it (litatoli query --run-id).

Reference values from NaZelo Trust-TP-0.1: DENY 192.0.2.1237 dropped_packets; order as above; overall_ok: true, 10 signed entries.

Explicit limits

  • Drop counters are IPv4 only. IPv6 egress is still enforced but its drops are not counted.
  • The drop-evidence key is IP-only. It does not distinguish port or protocol, so it cannot separate two policies toward the same IP.
  • nazelo.workload.output is a self-report. It is the workload's own account of what it observed (OPEN/BLOCKED), sealed — not an independent enforcer observation. The enforcer's independent evidence is sandbox.network.blocked.
  • The head receipt is unsigned. It catches truncation, but an attacker able to rewrite BOTH the chain and the receipt could make them agree. For a hostile setting, publish the head independently (CI logs, a witness, a second host).
  • Seal order ≠ strict temporal order for the tail. nazelo.workload.output and nazelo.run.sealed are sealed at collection time (after destroy). The causal guarantee is the lifecycle segment (create → … → destroy).

Compatibility rule

This is a contract between three moving components plus the harness. A green run on one four-tuple says nothing about a different four-tuple: never mix an engine from one build with the others from another without re-validating.

Re-validating is the composition job. A red one means the current four-tuple is not validated — not that a test is flaky.