Skip to main content
  1. Articles/

Falco: Runtime Security Monitoring on Kubernetes

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:

ModeMechanismAdvantagesDisadvantages
Kernel module.ko module loaded into the kernelMaturity, broad supportKernel rebuild if version changes, more invasive
eBPFSandboxed program in the kernelSecure, no module, GKE/EKS/AKS compatibleRequires kernel ≥ 4.14
Modern eBPFeBPF CO-RE (Compile Once, Run Everywhere)No kernel headers requiredKernel ≥ 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:

RuleTriggerPriority
Terminal shell in containerinteractive bash/sh in a containerWARNING
Sensitive file readReading /etc/shadow, /etc/passwdWARNING
Write below rootWriting outside expected pathsERROR
Unexpected network connectionUnplanned network connectionNOTICE
Outbound connection to C2Known C2 server IPsCRITICAL
Container drift detectedNew executable created in a containerCRITICAL
Read sensitive file untrustedReading K8s secrets/tokensWARNING

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 variables
  • priority: DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL
  • tags: 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 priority
  • falcosidekick_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:

  1. Shell spawned in container — a shell is opened
  2. Unexpected outbound connection — an outbound connection to an external IP
  3. Network 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:

LayerToolMoment
BuildTrivyImage scan before push
AdmissionKyvernoValidation at deployment
RuntimeFalcoContinuous 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.