Node floors made by tools, one per network variant
Status: in progress, 2026-11-08. The numbers below are measured; what is not done yet is marked as upcoming.
What a node floor is
A Kubernetes node image (here: tunnr, Rusternetes on its own minimal OS) puts one seccomp filter on its PID 1 at boot. Every process on the node inherits it and can never remove it: the node's own programs, the network plugins, and every pod, privileged pods included. So the floor is the outer wall for the whole node. Each pod also gets its own, much tighter profile on top (64 to 96 calls for the bench pods); the floor is the ceiling above all of them.
Until now the floor was a hand-written list of 361 system calls, close to everything Linux has. It blocked nothing the node did, and protected little.
How the floor is made now: only from tools
- The profiler measures. The node runs in log mode under Heimdall's profiler (
heimdall profile probe-log): every system call of every process, put on the process that made it, with no record lost. Runs cover boot, join, pods, network, network policy, DNS, drain, reboot, certificate rotation, log rotation, link failures and long soaks. - The graphs check. Mímir's executable graphs of the node's own programs (Rust, with C start-up and thread start modelled) say which calls each program can reach at all, including paths no run took.
- The generator writes the floor.
cargo xtask floor genjoins the profiler's evidence, the graphs, the kernel-implied base set and the pods' measured profiles. The policy is data (strict, or general with a listed margin of common workload calls). The evidence control refuses a floor the generator did not write ([hand-made-floor]) and records the sha256 of every input in the receipt. - The enforce run proves it. The generated floor goes on, the whole suite runs, and no kill is allowed. A kill means missing evidence: more profiling, never a hand edit.
One floor per network variant
The node image is built in network variants, and Build Thing picks the variant when it builds the image. So each variant gets its own floor, generated from its own evidence and graphs, instead of one floor that must cover all of them:
- bridge: the standard CNI plugins (bridge, host-local, loopback);
- aya: our eBPF network (
bpfandperf_event_openbelong only here); - OVN: ovn-kubernetes, the setup of the two-node benchmark.
The matrix: static filter and graph filter, per variant
| Variant | Profiled | Calls seen | Generated floor (strict) | Enforce run, static filter | Graph filter |
| bridge | Yes: 7 runs + situations + a 4-hour soak (96/96 pods, 0 records lost) | 174 | 197 | Upcoming | Upcoming: waits for the in-kernel follower |
| aya | Yes: 1 run | 137 | 182 | Upcoming | Upcoming: waits for the in-kernel follower |
| OVN | Upcoming | — | 180, provisional: no OVN run yet, so not usable before it | Upcoming | Upcoming: waits for the in-kernel follower |
- Static filter: the floor above, a fixed allowlist for the whole run. Its cost is measured: about 20 ns per allowed call (docs/v2/graph-filter.md §8).
- Graph filter: the same floor plus "only the calls that can come next" per thread, from the programs' graphs. Its parts are released; its in-kernel follower is not built yet, so a graph-mode run falls back to the static filter today and says so in its record (docs/v2/graph-filter.md §12). The same suites will run under it when the follower exists, so the two columns can be compared run for run.
Results so far
- The hand-written floor: 361 calls. The tool-made strict floors: bridge 197, aya 182 (tunnr main 4e24359). Too narrow: 0 (nothing the node did fell outside it). Too wide in the hand-written list: 166 calls that no run made and no graph reaches, among them
ptrace,userfaultfd,keyctl,add_key, SysV IPC,process_vm_readv/writev, swap and clock setting. - New situations found new calls (
execveat,memfd_create,mremap): the graphs had already predicted all three. - One pod call came from outside everything:
sendmmsgfromwget(glibc's resolver) in the aya run. The strict floor would have stopped it, so it was added from that evidence by the generator. Programs without a graph (C programs in pods, the Go CNI plugins) are covered by the runs alone: the policy for unknown workloads is a choice (strict, or general with a margin).