Your container scanner said clean. Your firewall said fine. Your image passed every security gate. And then someone exec’d into your postgres container and ran bash. You’d never know—not until it was too late.
This is where most container security strategies fall apart. You can scan images for vulnerabilities all day long, but the real danger happens at runtime. A zero-day exploit. A compromised CI/CD pipeline. An insider running commands they shouldn’t. Your scanner won’t catch any of that. Falco does.
What Falco Is (and Why You Need It)
Falco is an open-source runtime security tool that watches every syscall your containers make and alerts you when something sketchy happens. It’s built on eBPF (extended Berkeley Packet Filter)—the same technology that powers observability tools like Cilium and Datadog. Instead of just looking at what’s inside a container, Falco watches what a container is doing.
Here’s the thing: once a container is running, image scanning is basically meaningless. You need visibility into runtime behavior. Is your nginx container suddenly trying to read /etc/shadow? Did your application spawn a shell it shouldn’t? Did someone escalate privileges? Falco catches all of it and throws an alert your way.
The default ruleset covers the common attack patterns:
- Interactive shell inside a container (
docker exec -it) - Sensitive file reads (
/etc/passwd,/etc/shadow, SSH keys) - Privilege escalation attempts
- Writing to system directories
- Spawning suspicious processes
- Network outbound connections to unexpected IPs
And honestly, you can trust it. Falco isn’t some noisy security theater tool. The rules are tight, the community maintains them, and you can tune them to your environment.
Installing Falco (Docker Approach)
For local testing or small setups, the easiest path is Docker. If you’re running Kubernetes, use Helm (I’ll mention that, but Docker Compose is simpler to demo).
Here’s a docker-compose.yml that runs Falco and monitors your other containers:
version: '3.8'
services: falco: image: falcosecurity/falco:latest container_name: falco privileged: true volumes: - /var/lib/docker/containers:/var/lib/docker/containers:ro - /var/run/docker.sock:/var/run/docker.sock:ro - /sys:/sys:ro - /dev:/dev:ro - /proc:/proc:ro - ./falco-rules.yaml:/etc/falco/rules.d/custom.yaml:ro environment: - FALCO_LOG_LEVEL=info command: /usr/bin/falco -o json_output=true
nginx: image: nginx:latest container_name: web ports: - "8080:80"A few things to note here:
- Falco needs
privileged: truebecause it’s instrumenting syscalls at the kernel level - The volumes give Falco access to the host’s
/proc,/sys, and Docker socket - The
json_output=trueflag makes alerts structured (way easier to parse)
Fire it up:
docker-compose up -dFalco will start streaming logs. You should see it loading the default ruleset and listening for container activity.
Testing Falco (Making It Scream)
Here’s where it gets fun. Falco is useless if you don’t trust it, so let’s trigger an alert.
In another terminal, exec into the nginx container and try to read a sensitive file:
docker exec -it web cat /etc/passwdNow check the Falco logs:
docker logs falcoYou should see a wall of JSON alerts. Falco caught:
- The
docker execcommand itself (interactive shell in container) - The
cat /etc/passwdattempt (sensitive file read) - Every syscall in between
Each alert includes:
- The rule that triggered
- The container name and ID
- The process name and args
- The user running it
- The severity level
That’s Falco doing its job. At 3 AM, when someone’s actually trying to get into your postgres container, this is the alert that wakes you up.
Writing a Custom Rule
The default rules are great, but sometimes you need to catch something specific. Let’s create a rule that detects any container reading /etc/shadow:
- rule: Unauthorized Shadow File Read desc: Detect attempts to read /etc/shadow inside a container condition: > open_read and fd.name = /etc/shadow and container output: > CRITICAL: Container attempted to read /etc/shadow (user=%user.name command=%proc.name container=%container.name) priority: CRITICAL tags: [privilege_escalation, sensitive_file_access]Drop this into the volume mount path (we mapped ./falco-rules.yaml:/etc/falco/rules.d/custom.yaml), restart Falco, and now any container reading /etc/shadow triggers a CRITICAL alert.
The rule language is readable. You’re matching:
open_read: a file open for readingfd.name = /etc/shadow: the specific file pathcontainer: only in containers (not the host)
Falco comes with ~200 default rules, and the syntax is documented. You can get as granular as you want.
Routing Alerts Somewhere Useful
Falco can output to:
- stdout (useful for demo, awful for production)
- A file (with logrotate, this works)
- HTTP webhook (POST JSON to your endpoint)
- syslog (traditional but limited)
- gRPC (for Falco Sidekick)
For real setups, use Falco Sidekick—a little sidecar that takes Falco alerts and routes them to Slack, PagerDuty, email, etc.
Add this to your Compose stack:
falco-sidekick: image: falcosecurity/falcosidekick:latest container_name: falco-sidekick ports: - "2801:2801" environment: SLACK_WEBHOOK_URL: https://hooks.slack.com/services/YOUR/WEBHOOK/URL command: /usr/bin/falcosidekickThen configure Falco to send alerts to Sidekick via HTTP, and Sidekick forwards to Slack. Your team gets Slack notifications when something sketchy happens. You wake up to a red alert at 3 AM instead of discovering a breach in the morning.
The Reality Check
Falco isn’t magic. It can’t prevent attacks—it only detects them. But detection at runtime, when an attack is actually happening, is infinitely more valuable than scanning images after they’re built. By the time an attacker is exec’ing into your containers, they’ve already bypassed your other defenses. Falco is your last line of sight.
Set it up. Tune the rules. Route the alerts somewhere you’ll actually see them. Because your 2 AM self will appreciate it when Slack wakes you up before the data is gone.