Falco & behavioral detection
Syscall rules that catch a shell in a container.
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.
- rule: Terminal shell in containerdesc: A shell was used as the entrypoint/exec into a container with a ttycondition: >spawned_process and containerand shell_procs and proc.tty != 0and container_entrypointoutput: >Shell in container (user=%user.name container=%container.nameimage=%container.image.repository cmd=%proc.cmdline)priority: WARNINGtags: [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.
# raise the shipped rule’s severity and reshape its output- rule: Write below binary dirpriority: CRITICAL # was a lower priority in the shipped rulesetoutput: >File below a known binary directory opened for writing(user_id=%user.uid file=%fd.name command=%proc.cmdlinecontainer=%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.
$ 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-apiimage=registry.internal/payments-api cmd=bash) k8s.ns=payments k8s.pod=payments-api-7d4b9c