Falco: Runtime Security Monitoring on Kubernetes
Table of Contents
In the article on Trivy and Kyverno, I laid out the framework in three layers: build, admission, runtime. Trivy and Kyverno cover the first two. Falco is the third.
The problem with Kubernetes security is that a clean image can be compromised at runtime. A container that ships to production with no known vulnerability can then:
- Download a malicious script with
curl - Open an interactive shell in a production container
- Read files outside its expected context
- Establish a connection to a C2 server
Trivy doesn’t see that. Kyverno doesn’t see that. Falco does.
What is Falco? #
Falco is an open source runtime threat detection tool, originally created by Sysdig, now an incubating CNCF project. It monitors the Linux kernel’s system calls (syscalls), the lowest observable layer without modifying applications.
Its principle: define rules that describe abnormal behavior. If a container violates a rule, Falco generates an alert.
It’s not a WAF, not a network IDS — it’s a behavior monitor at the kernel level.
Application
│
▼
Syscalls (open, exec, connect, read…)
│
▼
Falco intercepts and evaluates the rules
│
├── Expected behavior → nothing
└── Rule violation → alert
Architecture: eBPF vs kernel module #
Falco can insert itself into the kernel in two ways:
| Mode | Mechanism | Advantages | Disadvantages |
|---|---|---|---|
| Kernel module | .ko module loaded into the kernel | Maturity, broad support | Kernel rebuild if version changes, more invasive |
| eBPF | Sandboxed program in the kernel | Secure, no module, GKE/EKS/AKS compatible | Requires kernel ≥ 4.14 |
| Modern eBPF | eBPF CO-RE (Compile Once, Run Everywhere) | No kernel headers required | Kernel ≥ 5.8 required |
In 2026, the Modern eBPF mode is recommended. The major clouds expose it without special configuration.
┌─────────────────────────────────┐
│ Userspace │
│ │
│ Falco daemon │
│ (rules engine + alerting) │
└────────────────┬────────────────┘
│ events
┌────────────────▼────────────────┐
│ Kernel space │
│ │
│ eBPF probe (Modern eBPF) │
│ intercepts syscalls │
└─────────────────────────────────┘
Installation via Helm #
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco \
--namespace falco \
--create-namespace \
--set driver.kind=modern_ebpf \
--set falcosidekick.enabled=true \
--set falcosidekick.webui.enabled=true
Check that the pods are up:
kubectl get pods -n falco
# NAME READY STATUS
# falco-xxxxx 2/2 Running
# falco-falcosidekick-xxxxx 1/1 Running
# falco-falcosidekick-ui-xxxxx 1/1 Running
The Falco DaemonSet runs on every node — that’s a prerequisite for intercepting all the cluster’s syscalls.
Checking that Falco detects #
# In a test namespace, launch a shell in a container
kubectl run test-pod --image=ubuntu --rm -it -- bash
# In the pod, try something suspicious
cat /etc/shadow
On the Falco side:
kubectl logs -n falco -l app.kubernetes.io/name=falco -f | grep Warning
# 09:14:32.123 Warning Sensitive file opened for reading...
# (user=root command=cat /etc/shadow container=test-pod)
The default rules #
Falco ships with ~100 ready-to-use rules. The most useful in production:
| Rule | Trigger | Priority |
|---|---|---|
| Terminal shell in container | interactive bash/sh in a container | WARNING |
| Sensitive file read | Reading /etc/shadow, /etc/passwd | WARNING |
| Write below root | Writing outside expected paths | ERROR |
| Unexpected network connection | Unplanned network connection | NOTICE |
| Outbound connection to C2 | Known C2 server IPs | CRITICAL |
| Container drift detected | New executable created in a container | CRITICAL |
| Read sensitive file untrusted | Reading K8s secrets/tokens | WARNING |
These rules cover the most common attack patterns: privilege escalation, exfiltration, lateral movement.
List all active rules #
kubectl exec -n falco daemonset/falco -- falco --list
Anatomy of a rule #
A Falco rule looks like this:
- rule: Shell spawned in container
desc: Un shell interactif a été ouvert dans un conteneur. Potentielle intrusion.
condition: >
spawned_process
and container
and shell_procs
and proc.tty != 0
output: >
Shell interactif détecté (user=%user.name cmd=%proc.cmdline
container=%container.name image=%container.image.repository:%container.image.tag
k8s_pod=%k8s.pod.name k8s_ns=%k8s.ns.name)
priority: WARNING
tags: [shell, intrusion]
The key elements:
condition: a filter in the Falco language (close to SQL)output: the alert message with enriched variablespriority:DEBUG,INFO,NOTICE,WARNING,ERROR,CRITICALtags: to filter and route alerts
Macros and lists #
Falco is composable via reusable macros and lists:
# List of known shells
- list: shell_binaries
items: [bash, sh, zsh, fish, ksh]
# Reusable macro
- macro: shell_procs
condition: proc.name in (shell_binaries)
# Container macro (excludes the host)
- macro: container
condition: container.id != host
# spawned_process macro
- macro: spawned_process
condition: evt.type = execve and evt.dir = <
These macros are defined in the default rules and can be extended.
Writing custom rules #
Case 1: detecting a connection to an unexpected external IP #
Your service should only talk to the database and an internal API. Everything else is suspicious:
- list: allowed_outbound_destinations
items:
- 10.0.0.0/8 # internal network
- 192.168.0.0/16 # local network
- rule: Unexpected outbound connection
desc: Connexion réseau vers une destination non autorisée
condition: >
outbound
and container
and not fd.net in (allowed_outbound_destinations)
and container.image.repository = "mon-org/mon-api"
output: >
Connexion inattendue depuis le conteneur (dest=%fd.rip:%fd.rport
container=%container.name pod=%k8s.pod.name ns=%k8s.ns.name)
priority: WARNING
tags: [network, exfiltration]
Case 2: monitoring access to Kubernetes secrets #
ServiceAccount tokens are mounted in /var/run/secrets/kubernetes.io/. A container that reads its token directly is often suspicious (unless it’s intended):
- macro: k8s_token_read
condition: >
open_read
and fd.name startswith /var/run/secrets/kubernetes.io/serviceaccount
- rule: K8s serviceaccount token read
desc: Un processus lit le token ServiceAccount
condition: >
k8s_token_read
and container
and not proc.name in (allowed_k8s_clients)
output: >
Token K8s lu (user=%user.name cmd=%proc.cmdline
file=%fd.name container=%container.name pod=%k8s.pod.name)
priority: WARNING
tags: [k8s, credentials]
Case 3: detecting the execution of curl or wget #
In a production container, nobody needs to download anything:
- list: download_tools
items: [curl, wget, nc, netcat, ncat]
- rule: Download tool executed in container
desc: Un outil de téléchargement a été exécuté dans un conteneur
condition: >
spawned_process
and container
and proc.name in (download_tools)
output: >
Outil de téléchargement détecté (cmd=%proc.cmdline
container=%container.name pod=%k8s.pod.name ns=%k8s.ns.name)
priority: ERROR
tags: [malware, download]
Deploying custom rules via Helm #
# values-falco.yaml
customRules:
custom-rules.yaml: |-
- list: allowed_outbound_destinations
items:
- 10.0.0.0/8
- rule: Unexpected outbound connection
desc: Connexion réseau vers une destination non autorisée
condition: >
outbound and container
and not fd.net in (allowed_outbound_destinations)
output: >
Connexion inattendue (dest=%fd.rip:%fd.rport pod=%k8s.pod.name)
priority: WARNING
tags: [network]
helm upgrade falco falcosecurity/falco \
--namespace falco \
--values values-falco.yaml
Falcosidekick: routing alerts #
Falco generates alerts in its logs. Without Falcosidekick, you won’t see anything in real time. Falcosidekick is an alerting proxy that supports ~50 destinations.
Falco ──► Falcosidekick ──► Slack
├──► PagerDuty
├──► Prometheus (Alertmanager)
├──► Elasticsearch
├──► Generic webhook
└──► Falco UI (web dashboard)
Slack configuration #
# values-falco.yaml
falcosidekick:
enabled: true
config:
slack:
webhookurl: "https://hooks.slack.com/services/T.../B.../..."
minimumpriority: warning # Ignore DEBUG/INFO/NOTICE
messageformat: >
*[{priority}]* {rule} dans `{output_fields.k8s.ns.name}/{output_fields.k8s.pod.name}`
_{output}_
helm upgrade falco falcosecurity/falco \
--namespace falco \
--values values-falco.yaml
The Slack alerts include: rule, priority, timestamp, pod name, namespace, image.
Prometheus + Alertmanager integration #
Falcosidekick exposes Prometheus metrics and can send to Alertmanager:
falcosidekick:
config:
prometheusalertmanager:
hostport: "http://alertmanager.monitoring:9093"
minimumpriority: warning
Or via the exposed metrics:
falcosidekick:
config:
prometheus:
extralabels: "cluster=prod,team=platform"
Available metrics:
falcosidekick_inputs_total— events received by priorityfalcosidekick_outputs_total— alerts sent by destination
Falco UI #
Falcosidekick UI is a web dashboard to visualize alerts in real time:
# Port-forward to the UI
kubectl port-forward -n falco svc/falco-falcosidekick-ui 2802:2802
# Open http://localhost:2802
The dashboard shows a history of events, filterable by priority, rule, namespace, and pod.
Concrete detection scenarios #
Detecting a reverse shell #
An attacker who gains code execution in a container often opens a reverse shell. Falco detects this via several combined rules:
Shell spawned in container— a shell is openedUnexpected outbound connection— an outbound connection to an external IPNetwork activity from non-expected process— a shell process doing networking
All three alerts in less than a second is a strong signal.
Detecting container drift #
Container drift = a new binary is created or modified in a running container. This is never supposed to happen in an immutable image:
# Built-in rule: Container Drift Detected
# Triggers if a new executable file is created in a container
If your image is immutable (which it should be), and an executable appears, it’s an intrusion.
Detecting credential reads #
# /var/run/secrets/ : K8s tokens
# /root/.kube/config : kubeconfig
# /etc/kubernetes/ : cluster certs
# ~/.aws/credentials : AWS
Falco monitors these paths by default. An API container reading /etc/kubernetes/admin.conf is abnormal.
Tuning false positives #
The default rules generate false positives on some legitimate workloads. To manage them:
Exclude known containers #
- rule: Terminal shell in container
# Override the existing rule to exclude debug pods
exceptions:
- name: known_debug_pods
fields: [k8s.pod.name]
comps: [startswith]
values:
- [debug-]
- [toolbox-]
Exclude by namespace #
- macro: not_monitoring_ns
condition: >
k8s.ns.name != "monitoring"
and k8s.ns.name != "kube-system"
and k8s.ns.name != "falco"
Adjust the minimum priority #
To reduce noise, only handle WARNING and above:
# values-falco.yaml
falco:
jsonOutput: true
logLevel: warning
priority: warning # Only log WARNING and above
Limits #
What Falco doesn’t do #
- Doesn’t block: Falco detects and alerts, it doesn’t stop the process. To block, combine it with an admission controller or an isolation mechanism.
- No complete network forensics: Falco monitors connections (IP, port) but not packet content.
- Doesn’t replace a SIEM: Falco generates events, but long-term aggregation and correlation is the job of Elasticsearch/Splunk/Loki.
Kernel overhead #
eBPF has a CPU cost (~1-3% per node depending on the syscall load). On very active nodes (lots of I/O or forks), watch the Falco daemon’s metrics.
kubectl top pod -n falco
# NAME CPU(cores) MEMORY(bytes)
# falco-xxxxx 42m 128Mi
Best practices #
1. Start in observation mode #
Never deploy Falco in production and act on every alert without having calibrated the rules. Spend two weeks in observation mode, note the false positives, and adapt.
2. Version custom rules #
Your rules are code. Git + PR + review before deploying to production.
rules/
├── network-rules.yaml
├── credential-access.yaml
└── container-integrity.yaml
3. Enrich alerts #
Falco can send Kubernetes context in each alert. Enable K8s metadata enrichment:
falco:
plugins:
- name: k8saudit
library_path: libk8saudit.so
Your alerts will automatically include: pod, namespace, image, labels, annotations.
4. Pair with Kubernetes audit logs #
Falco can also ingest the K8s Audit Logs (API server) via the k8saudit plugin. This lets you detect:
- Creation of suspicious ClusterRoleBindings
- Abnormal RBAC access
- Requests to sensitive resources (secrets, configmaps)
# values-falco.yaml
falco:
plugins:
- name: k8saudit
library_path: libk8saudit.so
open_params: "http://0.0.0.0:9765/k8s-audit"
Conclusion #
Falco completes the Kubernetes security triangle:
| Layer | Tool | Moment |
|---|---|---|
| Build | Trivy | Image scan before push |
| Admission | Kyverno | Validation at deployment |
| Runtime | Falco | Continuous monitoring |
None of these three layers is sufficient on its own. A clean image can be compromised. A compliant deployment can drift. Falco is the last line of defense — it detects what slips past the first two.
The overhead is low (eBPF), the deployment is simple (Helm in 5 minutes), and the default rules cover the essentials. The real work is the calibration: reducing false positives, writing the business rules, wiring the alerts into what your team already monitors.