Introducing Pahlevan: Runtime Security That Blocks, Not Just Alerts

Most Kubernetes runtime security tells you the breach already happened. An alert fires, you read that /etc/shadow was opened inside a container, and then you start the incident. The read already returned. Pahlevan is an open-source project that takes the other position: it learns what a workload normally does, then denies everything else in the kernel, so the syscall never succeeds.

There are no rules to write. No YAML rule language, no policy DSL to learn. It watches a container during a learning window, builds the baseline itself, and then enforces exactly that baseline.

Why not just use Falco?

Falco is an excellent detection engine, and this is not a criticism of it. It is alert-only by design. It will tell you, accurately and quickly, that a sensitive file was read. What it will not do is make the read fail. That is a deliberate architectural choice, not a gap.

Tetragon can block, and blocks well, but it needs a hand-written TracingPolicy describing what to stop. That means you have to know the attack shape in advance and encode it. It is the same rule-authoring burden, moved to a different file.

Pahlevan's bet is that the allow-list is easier to obtain than the deny-list, because the workload can generate it for you. A container that serves HTTP opens a knowable set of files, dials a knowable set of destinations, and executes a knowable set of binaries. Anything outside that set is, by definition, worth stopping.

The benchmark

I ran all three in an isolated k3s VM: one tool at a time, the same nginx:1.27 workload, the same four attacks, vendor-default configuration.

  • Pahlevan: blocked 4 of 4 in the kernel, with EPERM.
  • Falco: blocked 0 of 4. It is alert-only, and its default ruleset detected 2 of the 4.
  • Tetragon: blocked 0 of 4 with default configuration. It detected all four as exec telemetry.

Two honest notes on that result. First, Tetragon can block with a hand-written policy, and I verified that it does: a Sigkill-on-/etc/shadow TracingPolicy stopped the attack. But an unscoped node-wide version of that policy also froze new process creation on the node and needed an out-of-band reset. Blocking by hand-written rule is powerful and sharp in both directions. Second, in that particular run Pahlevan stopped the egress attack because curl was an unlearned binary, not because it inspected the connection. Network enforcement landed after that benchmark ran.

How it works

Pahlevan ships as two components. A privileged DaemonSet agent runs on every node and owns the eBPF data plane, which is the shape any node-level kernel instrumentation has to take. A leader-elected operator handles policy lifecycle, status aggregation, and admission; it needs no host access at all and runs inside a user namespace.

The programs are CO-RE eBPF, so they compile once and run across kernels without per-node toolchains:

  • Observation. A single raw_tracepoint/sys_enter sees every syscall, deduplicated in the kernel per (cgroup, syscall) so userspace is not flooded.
  • Files. An lsm/file_open hook resolves the full path with bpf_d_path and, under enforcement, returns -EPERM for any path outside the learned set.
  • Network. An lsm/socket_connect hook denies egress to destinations that were never seen during learning.
  • Process. An lsm/bprm_check_security hook denies execve of an unlearned binary, which is where reverse shells and dropped miners die.

Two details matter more than they sound. Attribution uses bpf_get_current_cgroup_id() mapped to the pod through cgroupfs, which stays correct when the workload runs with hostUsers: false, where host-PID-based attribution gets confused. And the learned syscall set is emitted as a seccomp profile, so new pods of that workload can start already confined rather than waiting to learn again.

When the baseline is wrong

Any system that blocks by learned behavior has one obvious failure mode: it learns an incomplete baseline and then breaks the workload. A code path that only runs at month-end never appears in a five-minute window.

So enforcement is not one-way. Policies start in Monitoring, the learned baseline is written to an inspectable ContainerProfile resource you can read and keep in git before you trust it, and self-healing watches the workload after enforcement begins. If enforcement correlates with crashlooping or failing readiness, the policy rolls back automatically. Blocking that cannot be reversed is worse than not blocking.

Honest caveats

The agent currently uses more memory than Falco or Tetragon did in the same test: roughly 327 MiB against 106 MiB and 67 MiB. Some of that is debug logging that was on during the run, but not all of it. That is the next thing I am tuning, and I would rather publish the number than quietly omit it.

Enforcement also requires a kernel with the BPF LSM available and enabled, meaning 5.7 or newer with CONFIG_BPF_LSM and lsm=bpf on the kernel command line. Without it, Pahlevan still runs and observes, but it cannot block. Observation needs 5.8 or newer for CO-RE and ring buffers, and the user-namespace operator wants Kubernetes 1.30 or newer.

Try it

Pahlevan is Apache-2.0 and v2.0.0 is out. Install it, point a policy at a workload with a label selector, watch it learn, then flip the mode to Blocking.

Get started

Code, benchmark methodology, and the full architecture write-up: github.com/obsernetics/pahlevan. Docs and quick start: obsernetics.github.io/pahlevan. Issues and early feedback are very welcome.

← Back to Blog