Most eBPF "runtime security" setups stop at detection: Falco fires an alert, you read it the next morning, and the attacker is already gone. This guide takes you to enforcement. You will build a Tetragon TracingPolicy that returns -EPERM from inside the kernel and kills any process touching /etc/shadow, then prove it blocks instead of merely logging.

Two things shifted in 2026 that an AI trained earlier gets wrong, and both are the kind of thing you only learn by hitting the wall. Falco 0.44.0 (May 26, 2026) fully removed the legacy eBPF probe, the gVisor engine, and the entire gRPC output, so startup now fails outright if your old config references them. And Tetragon's Override/Signal enforcement silently does nothing on the wrong kernel unless one config flag is set. This is written for platform and security engineers running Kubernetes 1.30+ who already have detection and want enforcement. We use Tetragon v1.7.0 (released April 29, 2026) as the enforcer.

Prerequisites

  • A Kubernetes cluster (1.30+) with kubectl admin access. A single kind or minikube node is fine for testing.
  • Helm 3.x.
  • A worker-node kernel >= 5.7 for security_ hook override, or >= 5.8 if you also run Falco's modern eBPF probe (it needs BTF plus a BPF ring buffer). Check with uname -r.
  • The real blocker: the node kernel must be built with CONFIG_BPF_KPROBE_OVERRIDE=y. Most stock cloud distros (GKE COS, Amazon Linux 2023, Ubuntu 22.04+) ship it, but some hardened or minimal kernels do not. We verify this in Step 1. Do not skip it.

Step-by-step

1. Confirm your kernel can actually enforce

# Is kprobe override compiled in?
grep CONFIG_BPF_KPROBE_OVERRIDE /boot/config-$(uname -r)
# expect: CONFIG_BPF_KPROBE_OVERRIDE=y

# Which functions are even override-able on this kernel?
sudo cat /sys/kernel/debug/error_injection/list | grep security_file_permission

Override rides on the kernel's error-injection framework. A function is override-able only if it is annotated ALLOW_ERROR_INJECTION() in the kernel source and therefore shows up in /sys/kernel/debug/error_injection/list. Since kernel 5.7 the security_ LSM hooks are on that list, which is the hook we target. If CONFIG_BPF_KPROBE_OVERRIDE is missing, Tetragon installs fine and your policy applies cleanly, but the kill never happens and there is no loud error. This is the single most common "why isn't enforcement working" cause.

2. Install the Tetragon CLI

GOOS=$(uname -s | tr '[:upper:]' '[:lower:]')
GOARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
curl -L "https://github.com/cilium/tetragon/releases/download/v1.7.0/tetra-${GOOS}-${GOARCH}.tar.gz" | tar -xz
sudo mv tetra /usr/local/bin
tetra version

tetra is how you stream events and inspect loaded policies. Pinning v1.7.0 keeps you on the release this guide was tested against.

3. Install Tetragon via Helm

helm repo add cilium https://helm.cilium.io
helm repo update
helm install tetragon cilium/tetragon \
  --version 1.7.0 \
  -n kube-system
kubectl rollout status -n kube-system ds/tetragon -w

This deploys Tetragon as a DaemonSet, one agent per node, attaching eBPF programs to kernel hooks. Enforcement capability is on by default in the agent. Your TracingPolicy decides what actually gets enforced.

4. Start in observe-only mode first

Create sensitive-files-observe.yaml:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "sensitive-files"
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    return: true
    args:
    - index: 0
      type: "file"
    - index: 1
      type: "int"
    returnArg:
      index: 0
      type: "int"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "/etc/passwd"
        - "/etc/shadow"
kubectl apply -f sensitive-files-observe.yaml

We hook the security_file_permission LSM function rather than the open syscall. Hooking the LSM hook catches the access regardless of which syscall opened the file. There is no matchActions block yet, so this only reports. Validate your match arguments before arming the kill, otherwise you risk killing the wrong thing.

5. Watch the events to confirm the match fires

In one terminal:

kubectl exec -it -n kube-system ds/tetragon -c tetragon -- \
  tetra getevents -o compact --pods <your-test-pod>

In another, trigger access from inside a test pod:

kubectl exec -it <your-test-pod> -- cat /etc/passwd

You should see a process_kprobe event referencing security_file_permission and /etc/passwd. If nothing appears, your matchArgs path is wrong. Fix it here, not after you have armed enforcement.

6. Arm enforcement: Override plus Signal together

Now create sensitive-files-enforce.yaml. The change is the matchActions block:

    selectors:
    - matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "/etc/shadow"
      matchActions:
      - action: Override
        argError: -1          # return -EPERM to the caller
      - action: Signal
        argSig: 9             # SIGKILL the offending process
kubectl apply -f sensitive-files-enforce.yaml

Here is the part the docs underplay. Signal alone does not reliably stop the operation. Sending SIGKILL is asynchronous, so the read can complete before the process dies and the secret has already leaked. Override returns -EPERM from inside the hook, so the access fails synchronously and the data never reaches userspace. Use Override to block the operation and Signal to also kill the process. The two together are the correct pattern. Either one alone leaves a gap.

In practice, the step that bites people is this exact one: they reach for Signal: 9 because killing the process feels like the strong move, ship it, and a pentester later shows them the file contents printed cleanly right before the kill landed. Override is the part that actually closes the door.

Verify it works

From inside the test pod:

kubectl exec -it <your-test-pod> -- cat /etc/shadow

Expected result:

cat: /etc/shadow: Operation not permitted
command terminated with exit code 137

Operation not permitted is the Override (-EPERM) landing. Exit code 137 (128 + 9) is the SIGKILL from Signal. Seeing both is proof that enforcement is live, not just detection. The corresponding Tetragon event now carries action: KPROBE_ACTION_OVERRIDE. If you instead get the file contents back, go straight to the first pitfall below.

Common pitfalls

Policy applies but nothing is blocked. Ninety percent of the time it is CONFIG_BPF_KPROBE_OVERRIDE missing (Step 1), or you are hooking a function that is not in /sys/kernel/debug/error_injection/list. Override only works on syscalls, functions annotated ALLOW_ERROR_INJECTION(), and security_ hooks on kernels >= 5.7. You can attach a kprobe to any function for observation, but you can only Override an error-injectable one, and Tetragon will not error loudly when you pick a bad target.

Equal on a file path misses the access. The path argument to security_file_permission must match exactly, including no trailing slash and the right absolute path. Use operator: "Prefix" for directories (for example /etc/) rather than expecting Equal to match children.

Falco 0.44.0 will not start after upgrade. If you are modernizing detection alongside this, note that 0.44.0 removed the legacy eBPF probe, the gVisor engine, and gRPC output entirely. Falco now fails at startup if your config still has engine.kind: ebpf, an engine.gvisor block, or grpc_output. Migrate to engine.kind: modern_ebpf (or kmod), and if you consumed alerts over gRPC, switch to the HTTP output or Falcosidekick. The driver schema also had a major bump, so 0.43.0 kernel drivers are incompatible with 0.44.0 userspace. Redeploy matching drivers, do not mix versions.

Tetragon FollowFD/UnfollowFD/CopyFD examples fail to load. Those actions were deprecated in 1.4 and are gone in current releases. Old blog snippets and AI-suggested policies still reference them. Use file-path matching on the LSM hook as shown here instead.

Test pod gets killed in a crashloop. If your workload itself reads the protected path, Signal: 9 kills it repeatedly. Scope the policy with matchPIDs (for example operator: NotIn for PIDs 0 and 1 with followForks: true) or matchBinaries, so only unexpected processes are terminated.

Wrap-up

You now have kernel-level enforcement rather than alerting: a TracingPolicy that returns -EPERM and kills any process touching /etc/shadow, verified by exit code 137. The two decisive lessons are the ones an AI will not currently surface for you. Enforcement silently no-ops without CONFIG_BPF_KPROBE_OVERRIDE and an error-injectable hook, and Override (not Signal) is what blocks the operation before data leaks.

A sensible next step is to extend the same pattern to network egress (hook tcp_connect with a matchArgs CIDR block) and to binary execution, promoting every policy through observe-only before enforce in each environment. Keep Tetragon for enforcement and a modern-probe Falco 0.44+ for broad detection. They are complementary layers, not competitors.

Sources

  • https://tetragon.io/docs/concepts/tracing-policy/selectors/
  • https://tetragon.io/docs/concepts/enforcement/
  • https://github.com/cilium/tetragon/releases
  • https://falco.org/blog/falco-0-44-0/
  • https://falco.org/docs/concepts/event-sources/kernel/