A container is the most predictable thing in your infrastructure. It runs one program. It opens the same files every time. It dials the same handful of destinations. It uses maybe sixty of the kernel's four hundred syscalls, and it will use exactly those sixty for as long as the image is deployed.
Pahlevan watches a container do all of that, once, and then refuses everything
else — in the kernel, at the moment of the attempt, with EPERM.
Nobody writes a rule. There is no rule to write. The container already told you what it does; the only question was whether anything was listening.
learning window enforcement
┌─────────────────────┐ ┌──────────────────────────┐
│ open /etc/nginx/* │──┐ │ open /etc/nginx/* ok │
│ open /var/log/* │ │ becomes │ open /var/log/* ok │
│ connect 10.0.1.7:5432│ ├───────────▶│ connect 10.0.1.7:5432 ok │
│ exec nginx │ │ the │ ─────────────────────────│
│ 61 syscalls │──┘ allow-set │ open /etc/shadow EPERM │
└─────────────────────┘ │ exec /tmp/xmrig EPERM │
│ connect 45.9.1.4:80 EPERM │
└──────────────────────────┘
An attacker inherits your baseline, not root. A command-injection bug in a
Python service gets an attacker python3 — because the service runs python3
constantly and it is in the allow-set. What it does not get them is
/etc/shadow, an egress to an address the workload never dialed, a binary
dropped in /tmp, or CAP_SYS_ADMIN. Every one of those is refused before the
syscall returns.
The policy is never out of date. A learned baseline describes the image that is actually running. Redeploy with a new dependency and the next learning window picks it up. There is no rule set to review, no detection content to subscribe to, and no gap between "we deployed something new" and "somebody updated the rules".
A denial is a fact, not a score. There is no threshold, no anomaly model, no
confidence percentage. The kernel either found the path in a hash map or it did
not. When Pahlevan says a container tried to read /etc/shadow, it did, and it
did not succeed.
A baseline narrow enough to stop an attacker is narrow enough to stop you.
If you kubectl exec into a production pod and run curl, Pahlevan will deny
it, because the workload never ran curl and Pahlevan cannot tell your hands
from someone else's. That is not a bug being worked around — it is the same
mechanism working correctly, and every honest evaluation of this tool has to
start there.
Three things exist because of it: Monitoring mode, which learns and reports
without ever denying; self-healing, which returns a container to learning if
enforcement breaks it; and policy exceptions, which let you widen the set
deliberately and in writing. Use the first one until you believe the baseline.
Seven eBPF programs, all CO-RE, all scoped to a single cgroup so nothing leaks across containers or reaches the rest of the node.
| Program | Sees | Does |
|---|---|---|
lsm/file_open |
Every open, path resolved in-kernel by bpf_d_path |
EPERM on an unlearned path |
lsm/socket_connect |
Every connect, IPv4 and IPv6, named against cluster Services | EPERM on an unlearned destination |
lsm/bprm_check_security |
Every exec: binary, argv, cwd, four levels of ancestry | EPERM or SIGKILL on an unlearned binary |
lsm/capable |
Every capability check, plus the task's effective/permitted/inheritable sets | EPERM on a capability never exercised |
kprobe/commit_creds |
The moment privilege actually changes | SIGKILL when privilege is gained with no execve to explain it |
tracepoint/sys_enter |
Every syscall, with its six arguments | Becomes the generated seccomp profile |
uretprobe/readline |
Commands typed at an interactive prompt | Records what somebody with a shell actually did |
Two of those are worth pointing at directly.
commit_creds is the single function through which any task's credentials
change. A local-root exploit that overwrites a cred struct and calls it
directly makes no syscall a syscall monitor could see — but it lands here, and
it lands with no execve underway to explain it, which is what separates it
from sudo doing its job.
readline catches what the kernel cannot. cd /root, export KUBECONFIG=…, history -c are shell builtins: they change what a session is
doing and produce no exec, no open, no connect. An exec-based monitor watches
somebody work through them and reports nothing but the shell's own process.
A per-node agent DaemonSet owns the eBPF data plane and enforces locally in
the kernel. A leader-elected operator Deployment drives the policy
lifecycle, status aggregation and CEL admission, with no host access and no
mutating webhook. If the operator is down, enforcement already installed in the
kernel keeps working. Detail: docs/architecture.md.
kubectl apply -f https://github.com/obsernetics/pahlevan/releases/latest/download/install.yamlPoint a policy at a labelled workload:
apiVersion: policy.pahlevan.io/v1alpha1
kind: PahlevanPolicy
metadata:
name: nginx-security
spec:
selector:
matchLabels:
app: nginx
learningConfig:
duration: 5m # watch normal behaviour
autoTransition: true # then enforce, automatically
enforcementConfig:
mode: Monitoring # Blocking denies in-kernel; start here
selfHealing:
enabled: true # return to learning if enforcement breaks the workloadkubectl get pahlevanpolicy nginx-security -wBefore you switch to Blocking, read what the baseline actually says:
pahlevan policy explain -f nginx-security.yaml # what this policy will and will not do
pahlevan profile show nginx-abc123 # what was learned, offlinepolicy explain runs without a cluster and tells you which fields translate
into kernel state and which are ignored — including the ones that look like they
work. More in examples/ and
docs/policy-reference.md.
Every number Pahlevan publishes comes from
test/benchmark/run.sh, run inside a kernel-isolated
VM, twice: once with no agent installed at all, then with Pahlevan learning and
enforcing.
The control pass is the part that makes the rest mean anything. Without it, a
scenario that silently failed to execute is indistinguishable from one that was
prevented, and a CPU figure has no idle node to subtract from it. Scenarios are
mapped to MITRE ATT&CK for Containers techniques and committed alongside the
harness. Methodology and recorded runs:
docs/benchmarks/.
eBPF is never loaded on a developer machine. hack/vm/ provisions a kernel with
the BPF LSM active, and that is where every load, attach and enforcement test
runs.
- Kubernetes 1.24+ — the user-namespace operator needs 1.30+.
- Linux 5.8+ for observation: CO-RE, ring buffer,
CAP_BPF. CONFIG_BPF_LSMwithlsm=bpfon the kernel command line for in-kernel enforcement. Without it, the LSM hooks do not attach and Pahlevan runs as an observability tool; thecommit_credskprobe and the syscall tracepoint still work, because kprobes need no boot parameter.
Details: docs/system-requirements.md,
docs/lsm-support.md.
helm repo add pahlevan https://obsernetics.github.io/pahlevan/charts
helm install pahlevan pahlevan/pahlevan-operator -n pahlevan-system --create-namespaceThe distroless image ghcr.io/obsernetics/pahlevan ships the agent, operator
and CLI. Tags, chart usage and manifest pinning:
docs/packages.md. Release notes:
CHANGELOG.md.
make build # agent, operator and CLI binaries
make test # unit tests, generated CRDs and manifests, fmt, vet
make lint # golangci-lint + yamllint
make ebpf-build # regenerate CO-RE objects and Go bindings (needs clang)
hack/vm/up.sh # bring up the eBPF-capable VM, then: make vm-testAny change to bpf/*.c, to the loader, or to a map layout needs a make vm-test run: the verifier accepts or rejects a program at attach time, and a
change that passes go build can still fail to load. make help lists every
target.
One maintainer. A v1alpha1 API. No public production adopters yet. The
learning model has a conceptual limit no amount of engineering removes — a
workload that is already compromised when learning begins gets its malicious
behaviour baselined along with everything else.
ROADMAP.md says what exists, what is in progress, and what is
merely planned, and marks each one. Nothing in this README describes something
that is not in the tree.
Contributions are welcome. Open an issue for substantial changes, run make test lint before submitting, and keep eBPF changes verifiable with make vm-test.
See CONTRIBUTING.md.
Licensed under the Apache License 2.0.
