Skip to content

Latest commit

 

History

230 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pahlevan

Your workload writes its own security policy. The kernel enforces it.

CI Go Report Card License: Apache 2.0 Go 1.25 Latest release GitHub stars
Kubernetes 1.24+ eBPF CO-RE LSM BPF enforcement PRs welcome

Pahlevan learn-then-enforce demo: a workload is profiled during a learning window, then an attacker's read of /etc/shadow, exec of nc, and IPv6 egress are each denied in-kernel with EPERM

The idea

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 │
                                       └──────────────────────────┘

What that buys you

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.

The trade, stated plainly

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.

What it watches

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.

Architecture

Pahlevan architecture: a leader-elected operator and the PahlevanPolicy/ContainerProfile/AttackSurface CRDs drive per-node agents that load eBPF programs which observe and deny in-kernel

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.

Quick start

kubectl apply -f https://github.com/obsernetics/pahlevan/releases/latest/download/install.yaml

Point 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 workload
kubectl get pahlevanpolicy nginx-security -w

Before 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, offline

policy 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.

Measured, not asserted

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.

Requirements

  • Kubernetes 1.24+ — the user-namespace operator needs 1.30+.
  • Linux 5.8+ for observation: CO-RE, ring buffer, CAP_BPF.
  • CONFIG_BPF_LSM with lsm=bpf on the kernel command line for in-kernel enforcement. Without it, the LSM hooks do not attach and Pahlevan runs as an observability tool; the commit_creds kprobe and the syscall tracepoint still work, because kprobes need no boot parameter.

Details: docs/system-requirements.md, docs/lsm-support.md.

Install

helm repo add pahlevan https://obsernetics.github.io/pahlevan/charts
helm install pahlevan pahlevan/pahlevan-operator -n pahlevan-system --create-namespace

The 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.

Development

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-test

Any 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.

Honest status

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.

Contributing

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.

License

Licensed under the Apache License 2.0.

About

An eBPF-based Kubernetes Operator for Proactive Attack Surface Minimization

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages