Falco & behavioral detection

Syscall rules that catch a shell in a container.

Advanced14 min · lesson 22 of 24

Every domain up to here tries to prevent; runtime security assumes prevention eventually fails and watches for the moment it does. Falco is the CNCF tool for that job: it taps the kernel — via an eBPF probe or a kernel module — and evaluates a stream of syscalls against behaviour rules in real time. A shell spawned in a container, a write under a binary directory, an outbound connection from a pod that should only ever listen — these fire the instant they happen, not on the next scan.

A Falco rule is three things: a condition (a boolean over syscall fields and reusable macros/lists), an output (the alert string, with %fields interpolated), and a priority. The shipped ruleset already covers the classics; the skill is reading a rule well enough to understand and tune it. This one fires when a shell is started inside any container with a terminal attached.

falco_rules.yaml (shipped)
- rule: Terminal shell in container
desc: A shell was used as the entrypoint/exec into a container with a tty
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
output: >
Shell in container (user=%user.name container=%container.name
image=%container.image.repository cmd=%proc.cmdline)
priority: WARNING
tags: [container, shell, mitre_execution]

Tune with local rules, never the shipped file

The shipped rules file is overwritten on upgrade, so you never edit it in place — you override in falco_rules.local.yaml, which is loaded after and wins for a rule of the same name. Redefine a rule to change its output or raise its priority, add an exception for a known-good process, or write a brand-new rule. After editing, give Falco up to a minute to reload before the new alerts appear.

falco_rules.local.yaml
# raise the shipped rule’s severity and reshape its output
- rule: Write below binary dir
priority: CRITICAL # was a lower priority in the shipped ruleset
output: >
File below a known binary directory opened for writing
(user_id=%user.uid file=%fd.name command=%proc.cmdline
container=%container.name)

What is worth alerting on

Key rules to behaviours an attacker cannot cheaply avoid: a shell in a container (especially a distroless one, where there should be no shell at all), writes under /bin or /usr/bin, package-manager execution inside a running container, reads of sensitive files like /etc/shadow, and unexpected outbound connections. These are high-signal because a healthy workload never does them — which is exactly why immutability (next lesson) makes them even sharper.

terminal
$ kubectl exec -it payments-api -- bash # an operator (or attacker) drops a shell
# Falco, watching the kernel, emits immediately:
15:02:31.4 Warning Shell in container (user=root container=payments-api
image=registry.internal/payments-api cmd=bash) k8s.ns=payments k8s.pod=payments-api-7d4b9c
Noise is the enemy of detection
Default rulesets alert on everything — package managers, init scripts, debug sessions — and the one real alert drowns in the rest. Tune before you roll out: route low-signal rules to a dashboard, page only on a short, rehearsed list of behaviours, and add exceptions for the legitimate processes that would otherwise cry wolf.