Behavioral gates
Find the gate that exercises a changed security contract, and learn whether its failure blocks a merge.
On this page
Eleven behavioral gates guard Chio's security contracts. Each is a script that fails when its contract breaks, and most pair with a self-test that fails when the gate stops noticing. Four gates block every merge. The rest run after a merge, in a trusted Linux container, or by an operator.
Choose a gate by the contract that changed and the observation that would show it broke. A source inventory, a pure decision, a durable store, and a physical confinement boundary each need a different check, and the gate inventory below names which gate reaches which.
Every named test runs and the gate exits zero
The gate runs its committed inventory against the contract it guards. A test that is missing, renamed, or filtered out fails the inventory check, so a quiet run cannot pass as a full one. Threat coverage.
A gate exercises one contract, and its self-test exercises the gate
A gate is a script under scripts/ that inspects source, runs a targeted test suite, or drives a fixture, then exits non-zero when it finds a violation. Its self-test is a second script that checks the gate's own contract. Most self-tests hand the gate, or a mutated copy of it, an input built to fail and require the refusal.
The runner has failure modes of its own: a filter that selects no tests, a renamed case that drops out of an inventory, a script that discards a failing exit status. Self-tests catch those. A passing self-test says the verifier works; the gate run against the candidate says the behavior holds. Retain each with its own command, source, output, and exit status.
A passing gate covers the layer it reaches
Each kind of check observes a different part of the implementation:
- Structural gate. A required field or construction site is still present in source.
- Unit test. A decision holds for the selected inputs.
- Durable-state exercise. Behavior survives a restart against the durable store.
- Kernel run. An operating-system restriction is in force on the host.
The tests the gates run assert typed refusals from library functions. The flow gate's engine tests expect FlowDenial::TopSource ("top-labeled data cannot egress") and FlowDenial::DeclassificationReplay ("declassification grant is already consumed"); the deception gate's lifecycle tests expect LifecycleError::IllegalEdge ("decoy lifecycle edge is illegal") and LifecycleError::ReplacementRequired ("an armed replacement is required"). A tool caller sees a signed Deny instead: on the Linux native process host, a top-labeled egress denial arrives with the reason security dispatch outcome requires reconciliation: security native dispatch capture callback failed; authoritative recovery required.
Keep each full gate run's command, trigger, source, stdout, stderr, and exit. A post-push qualification run is reported as post-push, and an interrupted compile is an incomplete attempt even when another self-test succeeded.
Four gates block every merge; the rest report after it
The required Build, lint, test job in ci.yml runs on every push to main and every pull request to main, so its failures block merging. For several gates that job runs only the self-test, and the gate itself runs after a push to main or when an operator starts it.
| Gate | Self-test runs in | Gate runs in | Blocks merging |
|---|---|---|---|
temporal | Required job, sha256-checked, bash -p | Required job, sha256-checked, bash -p | Yes |
adapter- | No workflow | Required job; release qualification after push | Yes |
domain- | Required job | Required job | Yes |
negative- | Required job | Required job | Yes |
flow | Required job | After a push to main | Self-test only |
deception | Required job | After a push to main | Self-test only |
response- | Required job | After a push to main | Self-test only |
cage- | Required job | --release in the trusted container; --portable by an operator | Self-test only |
keyring- | None | Trusted container; an operator | No |
secret- | None | --release in the trusted container; --portable by an operator | No |
authenticated- | None | An operator, for manual qualification | No |
The gates that check behavior of the installed product ship with it. An operator runs them on the serving host with chio security selftest, one suite per gate: cage, restart, caller-delivery, deception, temporal, keyring, and broker. Each run is retained and reported as host qualification, and every release records the full gate run behind it, which chio release verify --evidence qualification shows for the release you run.
What each gate asserts, and where its workflow calls it
- flow. Runs Apalache over the information-flow lattice model in
formal/tla/InformationFlowLattice.tlawith a reader-direction mutation that must fail, type-checks the flow and security-types crates forwasm32, and checks an exact test inventory with a canonical-flow or flow-sidecar test for each protocol edge and provider adapter it lists. Self-test:check-flow-security.test.sh. - deception. Checks decoys, signed watermarks, tripwires, the sealed registry, and canary and honey-tool conformance. The flow and deception gates run after a push to main in the active defense job of the
enterprise-hardening.ymlworkflow thatci.ymlcalls. Self-test:check-deception-security.test.sh. - temporal. Checks temporal rules, event-time correlation, signed security events, and verified event ingress. The Temporal security gate step in the required job checks the sha256 of the runner and of the self-test before running each under
bash -p. Self-test:check-temporal-security.test.sh. - response-recovery. Checks the response authority daemon, its state machine, fences, freeze, quorum, the crash matrix, and the scheduler's TTL dispatch and lease fencing. The gate runs after a push to main alongside the flow gate. Self-test:
check-response-recovery.test.sh. - keyring-transparency. Runs RFC 6962 proof vectors plus checkpoint, witness, split-view, and atomic enterprise receipt tests; the witness tests require witness signatures to bind the checkpoint hash and come from a distinct known quorum before a rotation is accepted. It has no self-test. On your host:
chio security selftest keyring. - secret-broker-boundary. Checks broker capability and proof binding, secret isolation, provisioning, and native kernel custody. It defaults to
--release, which runs only on the designated enterprise Linux x86_64 runner and adds the confined broker MCP tests with process death and terminal cage receipts;chio security selftest broker --portableruns the portable inventory on any host. - cage-enforcement. In
--releasemode, draws a fresh 64-character hex random challenge and requires the same value back in the Linux evidence marker. Its self-test,check-cage-enforcement.test.sh, drives that mode against a fake toolchain;chio security selftest cage --portableruns on any host, and on Linuxchio security selftest cageruns the random-challenge mode by default. Inside a trusted execution container built from an authorized security source, the Linux enforcement job ofenterprise-hardening.ymlandenterprise-linux-capture.ymlrun the keyring gate and the broker and cage gates in--release(see capture authority). - authenticated-caller-delivery. Checks the caller delivery exchange. An operator runs
chio security selftest caller-deliveryas part of host qualification. - adapter-no-bypass. Checks the source inventory for a bypass. The Durable consumer boundaries step in the required job runs it through
check-consumer-boundaries.sh, and release qualification runs it after a push to main throughci-workspace.sh. Its self-test,check-adapter-no-bypass.test.sh, checks the gate's policy markers and then runs the gate on the current tree; no workflow runs it. - domain-separation. Requires every byte-string signing or hashing domain constant under
crates/, meaning a&[u8]constant named for a domain or signature, to be declared once and, in production code, to be shapedchio.<area>.<payload>.v<N>with a NUL terminator. Existing exceptions are listed as expiring debt. Self-test:check-domain-separation.test.sh. - negative-assertions. Bars a new weak negative assertion in
crates/security,chio-kernel, orchio-control-plane; existing sites are pinned one by one by identity. Self-test:check-negative-assertions.test.sh. For both ratchets, renewal through--ratchetcan only hold or lower the cap or baseline.
Read more in Engineering standard, which states the rule each gate checks.
The temporal gate keeps a forged event out of every durable table
An attacker keeps a well-shaped detector event and signs it with an untrusted key. The temporal ingress verifier must return an integrity failure before the event reaches any durable correlation or advisory table, while the legitimate neighbor, signed by the configured producer under the same tenant and time expectations, still enters verified ingress. A gate that refused both would show nothing useful.
The temporal suite runs ten test inventories covering rule validation, event-time correlation, producer and receipt provenance, and seven ingress mutations, and it checks the inventory's exact count and digest before any test executes. Run it on the host that ingests detector events:
chio security selftest temporal --evidence ./temporal-evidencesuite result temporal pass 10 inventories, 40 tests 1 suite passed; evidence in ./temporal-evidence
./temporal-evidence keeps the suite log, its exit status, and a SHA-256 manifest. The suite's database oracle confirms the refused event is absent from security_verified_events, security_correlation_ingress, security_correlation_events, and security_advisory_events, then admits a current valid event and observes one verified row and one ingress row. Those rows say nothing about a network receiver or a physical containment effect.
Seven altered events stop at ingress, and the neighbor still lands
In the pricing project, a forged detector event claims the fixer touched /secrets. The forged case signs the original event body under another producer, so the request reaches signature verification instead of failing in a parser:
#[test]
fn verifier_ingress_rejects_forged_event_without_persistence() {
assert_ingress_mutation_rejected(
"forged",
PortErrorKind::IntegrityFailure,
|trusted, _receipt_signer, untrusted| {
let event = current_ingress_event(trusted);
let signed: SignedSecurityEvent =
serde_json::from_slice(event.source_evidence.as_bytes())
.unwrap_or_else(|error| panic!("decode forged event body: {error}"));
resign_event(event, signed.body().clone(), untrusted)
},
);
}The full helper builds a fresh SQLite ingress for each case and inspects its tables after the refusal. Forged, unsigned, untrusted, cross-tenant, stale, future-dated, and invalid-receipt events each refuse at ingress, and each case preserves the valid neighbor that still inserts.
Read the three results together. The suite shows the ten inventories ran under their pinned names and refuses missing, renamed, or filtered cases; the typed failure shows the intended boundary refused the altered event; the inserted neighbor shows the boundary still admits useful work. None of them qualifies a Linux cage, a secret, or an external effect, which have their own gates.
Read more in Security events, which covers producer keys and freshness at ingress.