Someone opened a shell in production
Every preventive layer can fail. When someone does get inside a running container, how long until you know?
- Weakness
- MITRE ATT&CK T1059 · Command and scripting interpreter (containers)
- Target
- ShopLite containers on a Linux host
Run these techniques only against the lab you own. Using them on systems you don't have permission to test is illegal.
Lab setup
01Install Falco (Linux host with a recent kernel)
Falco watches system calls using eBPF and matches them against rules describing suspicious behaviour. On Docker Desktop (macOS/Windows), run this lab in a Linux VM or a kind cluster instead.
terminal$ docker run -d --name falco --privileged \-v /var/run/docker.sock:/host/var/run/docker.sock -v /proc:/host/proc:ro -v /etc:/host/etc:ro \falcosecurity/falco:0.41.3 falco -o engine.kind=modern_ebpf── output ──3f2a91c0e4…Falco itself needs elevated privileges to observe the kernel. Run it as a dedicated, tightly controlled component, never alongside application code.
The threat
01What post-compromise activity looks like
After gaining code execution in a container, attackers typically spawn a shell, look around (read environment variables and mounted secrets, list processes), try to download tools, and reach out to other services. In an immutable, single-process container, every one of these is abnormal: the application never spawns a shell, never writes to
/usr/bin, and never reads/etc/shadow.02Simulate the signal safely
You don't need an exploit to test detection. An operator opening an interactive shell in the container produces the same system calls Falco is watching for. (Use the pre-distroless
shopliteimage, which still has a shell.)terminal$ docker exec -it shoplite sh -c 'id; exit'── output ──uid=1000(node) gid=1000(node) groups=1000(node)
What's at risk
- Without runtime detection, an intruder inside a container is invisible until they cause visible damage.
Detect
01Falco's default rules catch it immediately
terminal$ docker logs falco 2>&1 | grep -i 'shell was spawned'── output ──Notice A shell was spawned in a container with an attached terminal (evt_type=execve user=node user_uid=1000 user_loginuid=-1 process=sh proc_exepath=/bin/busybox parent=runc command=sh -c id; exit terminal=34816 exe_flags=EXE_WRITABLE container_id=5e1c0a7b9d2f container_name=shoplite)02Route alerts where people will see them
Falcosidekick forwards Falco events to Slack, PagerDuty, Alertmanager, Loki, and dozens of other outputs. Sending them to Loki puts security events next to the application logs from the Observability course; Critical-priority rules also page.
falco.yaml (excerpt)add to fileyaml json_output: true http_output: enabled: true url: http://falcosidekick:2801/ # falcosidekick env: LOKI_HOSTPORT=http://loki:3100 # ALERTMANAGER_HOSTPORT=http://alertmanager:9093 # ALERTMANAGER_MINIMUMPRIORITY=critical
Defend
01Custom rules for YOUR app's normal behaviour
The strongest rules describe what your service should never do. ShopLite only ever runs
node; any other process in its container is suspicious. Tune rules until they're quiet in normal operation, because a noisy detector gets ignored just like a noisy pager.Hardened
falco_rules.local.yamlwhole fileyaml - rule: Unexpected process in shoplite desc: ShopLite containers must only ever run node condition: > spawned_process and container.name startswith "shoplite" and not proc.name = "node" output: > Unexpected process in shoplite (proc=%proc.cmdline parent=%proc.pname user=%user.name container=%container.name image=%container.image.repository) priority: CRITICAL tags: [shoplite, mitre_execution]02Make the unusual impossible, too
Detection and prevention reinforce each other. The distroless image from Lab 2.3 has no shell to spawn, and the read-only filesystem from Lab 2.1 prevents downloading tools. What remains possible is short and specific enough that any Falco alert is worth waking up for.
Vulnerable
Dockerfileadd to filedocker FROM node:22-alpine # has /bin/sh, wget, apk ...Hardened
Dockerfileadd to filedocker FROM gcr.io/distroless/nodejs22-debian12:nonroot # node only
Verify
01The custom rule fires on anything but node
Repeat the safe simulation against the alpine-based image and confirm a CRITICAL event reaches Loki and Alertmanager. Against the distroless image there's no shell to run at all, and
docker execfails, which is the goal.terminal$ docker exec shoplite-distroless sh── output ──OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH: unknown
The concepts
Defence in depth, ending with detection
Every chapter of this course added a layer: no secrets to steal, trusted dependencies and images, minimal, unprivileged containers, secure pipelines and code. Runtime detection is the last layer. It assumes the others can fail and minimises how long an intruder can operate unnoticed, which is often what separates a minor incident from a breach.
Your turn
Which Falco default rule would fire if something inside a container read /etc/shadow, and what's its priority?
Interview questions
What does runtime security add on top of image scanning and hardening?